rusting_engine 2.5.6

Vulkan 3D game engine with GPU-accelerated physics for massive physics-heavy scenes
# Tutorial 5: Physics sandbox

You will throw balls into a pile of crates. Starting from the sandbox
template, a pile of crates and balls in a walled pen, you make one ball
bouncy and add a key that throws balls from the camera. Each change is checked by a scenario test, and the last
step proves the simulation gives the same result in debug and release
builds.

In the commands below, `rusting` means `./target/debug/rusting` from the
engine checkout (build it with `cargo build --bin rusting`). Add `--json` to
any command for machine-readable output.

## 1. Create the game

```bash
mkdir -p ~/Code/games && cd ~/Code/games
rusting new . "Crate Drop" --template sandbox
```

The scene holds three kinds of objects:

- **Floor** and **Wall 1** to **Wall 4**: `Fixed` rigid bodies with box
  colliders. Fixed bodies never move; other bodies collide with them.
- **Box 1** to **Box 9** and **Ball 1**, **Ball 2**: `Dynamic` rigid bodies.
  Gravity pulls them down and they push each other.
- **Sun**, **Game Camera**, and a help HUD.

Every body has a `rigid_body` (its kind, mass, and velocities) and a
`collider` (its shape, friction, and restitution). Look at one:

```bash
rusting scene query "Crate Drop/scenes/main.rscene" --name "Ball 2" --fields collider --json
```

```json
"collider": {
    "friction": 0.5,
    "restitution": 0.0,
    "sensor": false,
    "shape": {"Sphere": {"radius": 0.5}}
}
```

## 2. Run the test that comes with it

The template ships `tests/drop.json`, a scenario that runs 300 fixed ticks
(5 seconds) without a window and checks that both balls fall but stay above
the floor:

```bash
rusting test "Crate Drop" "Crate Drop/tests/drop.json"
```

```text
Game exit code: 0, timed out: false
Scenario "drop: the balls fall and land on the pile, not through the floor": passed after 300 ticks, seed 0
```

The first run builds the engine and takes a few minutes.

## 3. Make a ball bouncy

Restitution is how much speed a body keeps when it bounces: 0 stops dead,
1 bounces back as fast as it came. Save this patch as `bounce.json`:

```json
{
  "operations": [
    {"op": "set", "name": "Ball 2", "path": "/collider/restitution", "expected": 0.0, "value": 0.8}
  ]
}
```

```bash
rusting scene patch "Crate Drop/scenes/main.rscene" bounce.json
```

```text
Patched 23b18ed3b4b3b2f0 -> a47d66c5957c001a: 0 created, 0 deleted, 1 field changes
  Ball 2/collider/restitution: 0.0 -> 0.800000011920929
```

`expected` makes the patch safe to run twice: the second time, the value is
no longer 0.0, so the patch refuses instead of overwriting someone else's
change. Object IDs and revisions are random per project, so yours differ:

```text
error [SCENE_CONFLICT]: operation 0: 6129d249-.../collider/restitution is 0.800000011920929, patch expected 0.0 (bounce.json at /operations/0, object `Ball 2` 6129d249-...)
```

## 4. Add a throw action

Actions name an input, so code and tests say `throw` instead of a key. Save
this patch as `throw-input.json`. It creates an object that binds Space and
the left mouse button to `throw`:

```json
{
  "operations": [
    {"op": "create", "entity": {
      "name": "Controls",
      "components": {
        "rusting.input_action": {"action": "throw", "inputs": ["Space", "MouseLeft"]}
      }
    }}
  ]
}
```

```bash
rusting scene patch "Crate Drop/scenes/main.rscene" throw-input.json
```

```text
Patched a47d66c5957c001a -> 21f3fc2d87140fa1: 1 created, 0 deleted, 0 field changes
```

## 5. Write the test first

Save this as `Crate Drop/tests/throw.json`. It taps `throw` at tick 30, then
checks that exactly one ball was thrown, that it starts in front of the
camera (the camera is at z = 10), and that two seconds later it has flown
into the pen:

```json
{
  "name": "throw: Space throws a ball from the camera into the pile",
  "ticks": 120,
  "steps": [
    {"tick": 30, "tap": "throw"},
    {"tick": 30, "expect": {"counter": "throws", "equals": 1}},
    {"tick": 30, "expect": {"entity": "Throw 1", "path": "/transform/position/2", "less_than": 9.5}},
    {"tick": 120, "expect": {"entity": "Throw 1", "path": "/transform/position/2", "less_than": 3.0}},
    {"tick": 120, "expect": {"entity": "Throw 2", "exists": false}}
  ]
}
```

Run it. Nothing throws yet, so it fails at the first check:

```bash
rusting test "Crate Drop" "Crate Drop/tests/throw.json"
```

```text
error [SCENARIO_FAILED]: tick 30 step 1: `counter:throws` /components/rusting.counter/value is 0, expected 1 ± 0 (inputs at this tick ran before the check: {"tap":"throw"})
```

## 6. Throw balls from the camera

Replace `Crate Drop/src/main.rs`:

```rust
use rusting_engine::prelude::*;

const THROW_SPEED: f32 = 15.0;

fn update(scene: &mut GameScene<'_>, _time: &FrameTime) {
    if !scene.pressed("throw") {
        return;
    }
    let Some((origin, forward)) = scene.camera_ray() else {
        return;
    };
    let n = scene.add_to_counter("throws", 1);
    let name = format!("Throw {n}");
    // Start one metre in front of the camera, so the ball is not inside it.
    let start = [
        origin[0] + forward[0],
        origin[1] + forward[1],
        origin[2] + forward[2],
    ];
    scene.spawn_copy("Ball 1", name.as_str(), start);
    scene.set_linear_velocity(&name, forward.map(|v| v * THROW_SPEED));
}

rusting_game!(update);
```

- `pressed("throw")` is true on the one tick the action goes down, so
  holding Space throws one ball, not one per frame.
- `camera_ray` gives the active camera's position and the unit direction it
  looks along.
- `add_to_counter` creates the `throws` counter on first use and returns the
  new value, which gives every ball a unique name.
- `spawn_copy` clones `Ball 1`, with its mesh, collider, and rigid body, at
  the start point. `set_linear_velocity` then launches it.

Run both tests by naming the folder:

```bash
rusting test "Crate Drop" "Crate Drop/tests"
```

```text
PASS Crate Drop/tests/drop.json: "drop: the balls fall and land on the pile, not through the floor" after 300 ticks
PASS Crate Drop/tests/throw.json: "throw: Space throws a ball from the camera into the pile" after 120 ticks
```

Play it with `rusting run "Crate Drop"` and press Space or click.

## 7. Check determinism

The engine steps physics at a fixed rate, in a fixed body order, so the same
inputs give the same simulation. `determinism` runs a scenario in a debug and
a release build and compares a hash of the whole state at every tick:

```bash
rusting determinism "Crate Drop" --scenario "Crate Drop/tests/throw.json"
```

```text
determinism: building and running the debug build...
determinism: building and running the release build...
deterministic: debug == release on all 120 ticks (the hash at each tick covers the whole state), final hash 0xe3c8e9c6af7d175b
```

Keep it that way: take randomness from the scene's seed, not a global random
generator, and do not read the wall clock in gameplay code. See
[Determinism](../determinism.md).

## What you learned

- `Fixed` and `Dynamic` rigid bodies, colliders, and restitution.
- `scene patch` with `expected` values that refuse a stale edit.
- Input actions from `rusting.input_action`, and `pressed` in code.
- Spawning and launching bodies with `spawn_copy`, `camera_ray`, and
  `set_linear_velocity`.
- Test-first scenarios with `tap`, `counter`, and `exists` checks.
- `rusting determinism` to prove debug and release builds agree.

Next, [Tutorial 6](06-multiplayer-physics.md) turns this sandbox into a
multiplayer game.