bevy_map_scatter
Bevy plugin for rule-based scattering with asset loading, async execution, and ECS-friendly results.

Features
Asset-driven scattering for Bevy with async execution and ECS-friendly results:
- Asset-based authoring of scatter plans (RON): load
*.scatterfiles viaAssetServer. - Texture integration: snapshot Bevy
Images to CPU textures with configurable domain mapping and typed errors for unsupported or unavailable CPU data. - Asynchronous execution: runs scatter jobs on
AsyncComputeTaskPooland cancels unfinished jobs when their request entity is despawned. - ECS-friendly: placements become entities; components can be attached for rendering, gameplay, or tooling.
- Streaming helper: manage chunked scatter around moving anchors (optional plugin).
- Diagnostics: forward core events as Bevy messages (
ScatterMessage) with configurable filtering; completed runs emitScatterFinishedas anEntityEvent. - Reflection support: scatter assets, Bevy-facing definitions, streaming components, and
Handle<ScatterPlanAsset>are registered for Bevy reflection and handle serialization workflows.
Use cases
Use cases: decorative dressing, resource distribution, or any placement driven by textures and rules.
Workflow notes:
- Plans are assets and can hot-reload
- Tweak textures, thresholds, or layer order and rerun
- Deterministic seeds by default; vary them per run when needed
Examples:
- Stylized forest: trees sparse, mushrooms where tree probability is low, grass fills gaps.
- Town dressing: benches in plaza masks, lamp posts along roads with spacing, clutter where nothing else landed.
- Dungeon encounters: camps in large rooms, enemies avoid camp influence, rare loot in dead ends with minimum spacing.
Integration details:
- Assets:
*.scatterplans loaded byAssetServer - ECS: placements become entities you can tag and decorate
- Async: jobs run on
AsyncComputeTaskPool - Determinism: same seed + plan + textures = identical placements
- Events: listen to
ScatterFinishedorScatterMessageto drive gameplay or tooling
Examples
See the example crate for curated demos you can run locally.

Quick start
Add the crates to a Bevy application:
# Cargo.toml
[]
= "0.19"
= "0.5"
= "0.4"
Create a scatter plan in assets/simple.scatter:
(
layers: [
(
id: "dots",
kinds: [
(
id: "dots",
spec: (
nodes: {
"probability": Constant(
params: ConstantParams(value: 1.0),
),
},
semantics: {
"probability": Probability,
},
),
),
],
sampling: JitterGrid(
jitter: 1.0,
cell_size: 1.0,
),
selection_strategy: WeightedRandom,
),
],
)
Trigger a single scatter run once the asset is ready:
use *;
use *;
;
/// Loads the scatter plan asset on startup.
/// Triggers a scatter request once the plan asset is loaded.
/// Observes the `EntityEvent` when a scatter run has finished.
Run the application with cargo run. After the scatter job completes, a summary appears in the log; continue with placement logic as needed.
Saving edited scatter plans
Runtime saving is available for editor and tooling workflows that modify
ScatterPlanAsset values and want to write .scatter RON back to disk. It is
not required for normal scattering. ScatterPlanAssetSaver is available when
the default ron feature is enabled.
use ;
use AssetPath;
use *;
use IoTaskPool;
use *;
Async job lifetime
Each ScatterRequest is tied to a request entity. MapScatterPlugin stores the in-flight Task<RunResult> on that entity, and the entity owns the job lifetime.
- If the job finishes, the plugin removes the internal job component and triggers
ScatterFinished. - If the request entity is despawned before the job finishes, the task is dropped and the scatter work is cancelled.
- Cancelled jobs do not emit
ScatterFinished. - The plugin does not call
Task::detach(), so jobs do not outlive their request entity.
This is intentional for streaming: chunk entities own their scatter work, and despawning a chunk because it left the view or because its settings/assets changed cancels any in-flight scatter run for that chunk.
Migration from 0.4 / Bevy 0.18
bevy_map_scatter 0.5 targets Bevy 0.19. Update application dependencies and Bevy APIs together:
[]
= "0.19"
= "0.5"
= "0.4"
Breaking migration notes:
- Scatter completion is an
EntityEvent; observeScatterFinishedwithOn<ScatterFinished>and trigger requests withCommands::trigger. - Forwarded diagnostics use Bevy messages; read
ScatterMessagewithMessageReader<ScatterMessage>. - In-flight scatter jobs are no longer detached. Despawning the request entity cancels unfinished work and no
ScatterFinishedis emitted for that job. ImageTexture::try_from_imagereturnsResult<ImageTexture, ImageTextureError>with explicit errors such asUnsupportedFormat,MissingImageData, invalid dimensions, invalid domain extents, or unexpected buffer lengths. The olderfrom_imagehelpers remain asOptionconvenience wrappers.ScatterPlanAsset, Bevy-facing plan definition types, streaming components, andHandle<ScatterPlanAsset>are reflected for Bevy 0.19 serialization and editor/tooling workflows.- Runtime saving of edited
.scatterassets is available throughScatterPlanAssetSaverand Bevy'ssave_using_saverAPI when theronfeature is enabled. - Bevy 0.19's BSN support is code-driven in upstream Bevy. This crate loads
*.scatterRON assets; it does not provide.bsnasset loading.
Streaming (optional)
For moving worlds or endless maps, use MapScatterStreamingPlugin and attach
ScatterStreamSettings to an anchor entity. The plugin will spawn/despawn chunks
around the anchor and emit placement entities tagged with ScatterStreamPlacement.
Alternatives
bevy_feronia: more opinionated, art-focused scattering with built-in wind/material/LOD workflows; likely a better fit if you want an end-to-end visual pipeline rather than a low-level, data-driven scatter core.
Compatibility
bevy_map_scatter |
map_scatter |
bevy |
|---|---|---|
0.5 |
0.4 |
0.19 |
0.4 |
0.4 |
0.18 |
0.3 |
0.3 |
0.17 |
0.2 |
0.2 |
0.17 |
Local verification
Before opening a release PR, run the same meaningful Bevy modes covered by CI: