rusting_engine 1.4.0

Vulkan 3D game engine with GPU-accelerated physics for massive physics-heavy scenes
# Tutorial 3: Gameplay plugins

The `rusting_game!` API from Tutorial 1 is one function per frame. Real
games want ECS systems: code that queries components, reads input, and
runs at a chosen stage. This tutorial adds two systems to the Coin Run game
from [Tutorial 2](02-coin-run-cli.md):

- **Reset**: pressing `R` puts the player back at the start.
- **Spin**: a new `coin_run.spin` component, saved in the scene like any
  built-in one, that makes the flag spin.

The engine's ECS is [`bevy_ecs`](https://docs.rs/bevy_ecs/0.19). If you know
Bevy, systems, queries, and resources work the same way.

## 1. Add the dependencies

Your game uses `bevy_ecs` types directly, and `serde` to save your component.
Add both to `Coin Run/Cargo.toml` under `[dependencies]`:

```toml
bevy_ecs = "0.19"
serde = { version = "1", features = ["derive"] }
```

Keep `bevy_ecs` on the same minor version as the engine (0.19), so both use
one copy of the crate.

## 2. Write the plugin

Replace `Coin Run/src/main.rs`:

```rust
use bevy_ecs::prelude::*;
use rusting_engine::prelude::*;
use rusting_engine::project_runner::{resolve_game_scene_path, run_project};
use rusting_engine::runtime::{
    ActionMap, App, AppError, InputBinding, KeyCode, Name, Plugin,
    RuntimeInput, ScheduleStage,
};
use serde::{Deserialize, Serialize};

/// Spins an object around its Z axis. Saved in scenes as `coin_run.spin`.
#[derive(Component, Clone, Debug, Default, Serialize, Deserialize)]
struct Spin {
    speed: f32,
}

// Describes Spin's fields for scenes, the Inspector, and `rusting schema`.
rusting_engine::reflect! {
    struct Spin {
        speed: f32 { unit: "rad/s", doc: "around the Z axis" },
    }
}

fn spin(time: Res<FrameTime>, mut objects: Query<(&Spin, &mut Transform)>) {
    for (spin, mut transform) in &mut objects {
        transform.rotation[2] += spin.speed * time.delta_seconds();
    }
}

/// Puts the player back at the start when `game.reset` is pressed.
fn reset_player(
    input: Res<RuntimeInput>,
    actions: Res<ActionMap>,
    mut objects: Query<(&Name, &mut Transform)>,
) {
    if !actions.just_pressed(&input, "game.reset") {
        return;
    }
    for (name, mut transform) in &mut objects {
        if name.0 == "Player" {
            transform.position = [-9.5, 0.5, 0.0];
        }
    }
}

struct CoinRunPlugin;

impl Plugin for CoinRunPlugin {
    fn build(&self, app: &mut App) -> Result<(), AppError> {
        app.register_scene_component::<Spin>("coin_run.spin")
            .map_err(|error| AppError::PluginSetup {
                plugin: self.name(),
                message: error.to_string(),
            })?;
        app.world_mut()
            .resource_mut::<ActionMap>()
            .bind("game.reset", InputBinding::Key(KeyCode::KeyR));
        app.add_systems(ScheduleStage::Update, (spin, reset_player));
        Ok(())
    }
}

fn main() -> GameResult {
    let scene = resolve_game_scene_path(
        "build/main.rscene.bin",
        env!("CARGO_MANIFEST_DIR"),
    );
    run_project("Coin Run", scene, CoinRunPlugin)
}
```

What each part does:

- **`main`** replaces `rusting_game!`. `resolve_game_scene_path` finds the
  cooked scene next to an exported executable, or in the project during
  development. `run_project` takes a window title and your plugin; it also
  handles headless runs, scenario tests, and replays.
- **`Plugin::build`** runs once at startup. It registers the component,
  binds the action, and adds the systems.
- **`register_scene_component`** lets scenes store `Spin` under the name
  `coin_run.spin`. The type needs `Component`, `Serialize`, `Deserialize`,
  `Default`, and a `reflect!` description. Pick a prefix for your game;
  `rusting.` is the engine's.
- **`reflect!`** lists the fields scenes save, with optional hints: `unit`,
  `min` and `max` (floats), `doc`, and `color: true` for `[f32; 3]` or
  `[f32; 4]` colors. Mark `#[serde(skip)]` fields `#[skip]`. The macro checks
  the list against the struct, so a field added to one but not the other
  does not compile. Enums, nested structs, `Vec`, `Option`, string-keyed
  maps, `Entity` and `Handle<TextureAsset>` fields work too; an `Entity`
  saves as the target's object ID and a handle as `{"$asset": path}`. A
  reference to an object that was deleted saves as `null` and loads as
  `None` in an `Option<Entity>`, or as `Entity::PLACEHOLDER` otherwise.
- **Actions.** `ActionMap::bind` adds a binding to an action name, and
  `just_pressed(&input, name)` is true on the frame the key goes down.
  Also available: `held` and `just_released`. Binding more keys to the same
  action is fine; any of them triggers it.
- **Stages.** Both systems run in `Update`, once per frame, reading
  `FrameTime::delta_seconds()`. Put logic that must replay exactly (anything
  that feeds physics) in `ScheduleStage::FixedUpdate` and use
  `FrameTime::fixed_delta` instead. See [Core concepts]../concepts.md.

## 3. Put the component in the scene

Find the flag's ID and patch a `coin_run.spin` onto it:

```bash
rusting scene query "Coin Run/scenes/main.rscene" --name Flag
```

```json
{
  "operations": [
    {"op": "set", "id": "FLAG-ID", "path": "/components/coin_run.spin", "value": {"speed": 2.0}}
  ]
}
```

```bash
rusting scene patch "Coin Run/scenes/main.rscene" spin.json
```

```text
  Flag/components/coin_run.spin: null -> {"speed":2.0}
warning [PATCH_UNVALIDATED_COMPONENT]: component `coin_run.spin` is not built in; the game validates it on load
```

The CLI does not link your game code, so it cannot check your component's
fields. The game does, when it loads the scene: a wrong field or a component
you forgot to register stops it with an error that names the component.

The editor and `rusting capture` also run without your code. They keep your
components as saved data, so opening and saving the scene in the editor
leaves `coin_run.spin` intact, but the Inspector does not show it yet.

## 4. Run and test it

```bash
rusting run "Coin Run"
```

The flag spins. Walk right, press `R`, and the player is back at the start.

Scenarios press actions by name, including your own. Save this as
`Coin Run/reset.scenario.json`:

```json
{
  "name": "R puts the player back at the start",
  "seed": 1,
  "ticks": 60,
  "steps": [
    {"tick": 1, "press": "player.right"},
    {"tick": 40, "expect": {"entity": "Player", "path": "/transform/position/0", "greater_than": -8.0}},
    {"tick": 41, "press": "game.reset"},
    {"tick": 41, "expect": {"entity": "Player", "path": "/transform/position/0", "equals": -9.5, "tolerance": 0.01}},
    {"tick": 60, "expect": {"entity": "Flag", "path": "/transform/rotation/2", "greater_than": 1.0}}
  ]
}
```

```bash
rusting test "Coin Run" "Coin Run/reset.scenario.json"
```

```text
Scenario "R puts the player back at the start": passed after 60 ticks, seed 1
```

## Going further

- **Other stages**: `Startup` runs once before the first frame;
  `PostUpdate` runs after `Update`.
- **Resources**: `app.insert_resource(MyState::default())` in `build`, then
  `Res<MyState>` or `ResMut<MyState>` in a system.
- **Events**: read a frame's events with `Res<EventQueue<CollisionEvent>>`
  and `.iter()`. Other built-in events include `HudButtonPressed`,
  `SoundEvent`, and `ClickEvent`. Declare your own with
  `app.add_event::<MyEvent>()` and send them through
  `ResMut<EventQueue<MyEvent>>`.
- **Built-in components** you can query: `Transform`, `RigidBody`,
  `Collider`, `Counter`, `Pickup`, `HudElement`, `Tween`, and the rest in
  `rusting_engine::runtime`. `rusting schema` lists their scene fields.
- **Renaming a field**: scenes saved with the old name now fail to load
  with an error naming the component, the field's path, and the saved
  version. Register the change instead:
  `app.migrate_scene_component("coin_run.spin", FieldMigration::rename("speed", "turn_rate"))`
  (or `FieldMigration::remove("old_field")`), from `rusting_engine::reflect`.
  Each migration raises the component's version; newer saves record it as
  `"$version"`, and older ones run the migrations they missed.
- **Random numbers**: read the `RandomSeed` resource and draw with
  `seed.value(tick, stream)` or `seed.unit(tick, stream)`, so a replay or
  scenario with the same seed gets the same numbers.

Next, [Tutorial 4](04-gpu-cube-rain.md) moves thousands of bodies to the
GPU.