bevy_map_scatter 0.5.0

Bevy plugin that integrates the `map_scatter` core crate for object scattering with field-graph evaluation and sampling
Documentation

bevy_map_scatter

License: MIT or Apache 2.0 Docs Crate Build Status

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

logo

Features

Asset-driven scattering for Bevy with async execution and ECS-friendly results:

  • Asset-based authoring of scatter plans (RON): load *.scatter files via AssetServer.
  • 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 AsyncComputeTaskPool and 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 emit ScatterFinished as an EntityEvent.
  • 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:

  1. Stylized forest: trees sparse, mushrooms where tree probability is low, grass fills gaps.
  2. Town dressing: benches in plaza masks, lamp posts along roads with spacing, clutter where nothing else landed.
  3. Dungeon encounters: camps in large rooms, enemies avoid camp influence, rare loot in dead ends with minimum spacing.

Integration details:

  • Assets: *.scatter plans loaded by AssetServer
  • ECS: placements become entities you can tag and decorate
  • Async: jobs run on AsyncComputeTaskPool
  • Determinism: same seed + plan + textures = identical placements
  • Events: listen to ScatterFinished or ScatterMessage to drive gameplay or tooling

Examples

See the example crate for curated demos you can run locally.

Forest Trail streaming example

Quick start

Add the crates to a Bevy application:

# Cargo.toml
[dependencies]
bevy = "0.19"
bevy_map_scatter = "0.5"
map_scatter = "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 bevy::prelude::*;
use bevy_map_scatter::prelude::*;

#[derive(Resource, Default)]
struct PlanHandle(Handle<ScatterPlanAsset>);

fn main() {
    App::new()
        .init_resource::<PlanHandle>()
        .add_plugins(DefaultPlugins)
        .add_plugins(MapScatterPlugin)
        .add_systems(Startup, load_plan)
        .add_systems(Update, trigger_request)
        .add_observer(log_finished)
        .run();
}

/// Loads the scatter plan asset on startup.
fn load_plan(mut handle: ResMut<PlanHandle>, assets: Res<AssetServer>) {
    handle.0 = assets.load("simple.scatter");
}

/// Triggers a scatter request once the plan asset is loaded.
fn trigger_request(
    mut commands: Commands,
    mut once: Local<bool>,
    handle: Res<PlanHandle>,
    assets: Res<Assets<ScatterPlanAsset>>,
) {
    // Only run once.
    if *once {
        return;
    }
    // Wait until the asset is loaded.
    if assets.get(&handle.0).is_none() {
        return;
    }

    // The domain size for scattering.
    let domain = Vec2::new(10.0, 10.0);

    // Create run configuration and seed for (deterministic) randomness.
    let config = RunConfig::new(domain)
        .with_chunk_extent(domain.x)
        .with_raster_cell_size(1.0);

    // Spawn an entity to track the request (attach your own components if needed).
    let entity = commands.spawn_empty().id();

    // Trigger the scatter run.
    commands.trigger(ScatterRequest::new(entity, handle.0.clone(), config, 42));

    // Mark as done.
    *once = true;
}

/// Observes the `EntityEvent` when a scatter run has finished.
fn log_finished(finished: On<ScatterFinished>, mut commands: Commands) {
    info!(
        "Scatter run {} finished: placements={} evaluated={} rejected={}",
        finished.entity,
        finished.result.placements.len(),
        finished.result.positions_evaluated,
        finished.result.positions_rejected
    );

    // Clean up the entity used for the request (keep it if you need it later).
    commands.entity(finished.entity).despawn();
}

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 bevy::asset::saver::{save_using_saver, SavedAsset};
use bevy::asset::AssetPath;
use bevy::prelude::*;
use bevy::tasks::IoTaskPool;
use bevy_map_scatter::prelude::*;

fn save_edited_plan(
    asset_server: Res<AssetServer>,
    plans: Res<Assets<ScatterPlanAsset>>,
    handle: Res<PlanHandle>,
) {
    let Some(plan) = plans.get(&handle.0).cloned() else {
        return;
    };

    let asset_server = asset_server.clone();
    IoTaskPool::get()
        .spawn(async move {
            let asset_path: AssetPath<'static> = "simple.scatter".into();
            let saved_asset = SavedAsset::from_asset(&plan);
            let saver = ScatterPlanAssetSaver;

            save_using_saver(
                asset_server,
                &saver,
                &asset_path,
                saved_asset,
                &(),
            )
            .await
            .expect("scatter plan should save");
        })
        .detach();
}

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:

[dependencies]
bevy = "0.19"
bevy_map_scatter = "0.5"
map_scatter = "0.4"

Breaking migration notes:

  • Scatter completion is an EntityEvent; observe ScatterFinished with On<ScatterFinished> and trigger requests with Commands::trigger.
  • Forwarded diagnostics use Bevy messages; read ScatterMessage with MessageReader<ScatterMessage>.
  • In-flight scatter jobs are no longer detached. Despawning the request entity cancels unfinished work and no ScatterFinished is emitted for that job.
  • ImageTexture::try_from_image returns Result<ImageTexture, ImageTextureError> with explicit errors such as UnsupportedFormat, MissingImageData, invalid dimensions, invalid domain extents, or unexpected buffer lengths. The older from_image helpers remain as Option convenience wrappers.
  • ScatterPlanAsset, Bevy-facing plan definition types, streaming components, and Handle<ScatterPlanAsset> are reflected for Bevy 0.19 serialization and editor/tooling workflows.
  • Runtime saving of edited .scatter assets is available through ScatterPlanAssetSaver and Bevy's save_using_saver API when the ron feature is enabled.
  • Bevy 0.19's BSN support is code-driven in upstream Bevy. This crate loads *.scatter RON assets; it does not provide .bsn asset 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:

cargo fmt --all -- --check
cargo check --workspace --all-targets
cargo test --workspace
cargo check -p bevy_map_scatter --no-default-features
cargo check -p bevy_map_scatter --all-features
cargo check -p bevy_map_scatter_examples --bins