rusting_engine 2.4.1

Vulkan 3D game engine with GPU-accelerated physics for massive physics-heavy scenes
# Tutorial 4: GPU cube rain

You will drop 2,000 cubes simulated on the GPU, get an event back from the
GPU when each one falls past a line, and show a counter on screen. This is
the pattern for debris, swarms, and crowds: the GPU owns the bodies, and the
CPU hears only about the moments it cares about.

You need a project made as in [Getting started](../getting-started.md). This
page calls it `Cube Rain`. It needs a Vulkan device: GPU bodies do not move
in headless runs.

## 1. The code

Replace `src/main.rs`:

```rust
use std::sync::atomic::{AtomicUsize, Ordering};

use rusting_engine::prelude::*;

/// Drops that fell past Y = -20 so far.
static FALLEN: AtomicUsize = AtomicUsize::new(0);

fn update(scene: &mut GameScene<'_>, time: &FrameTime) {
    scene.once("spawn_rain", |scene| {
        let drop = CubeSpawn::new().class("rain");
        for index in 0..2_000 {
            let x = (index % 50) as f32 - 25.0;
            let z = (index / 50) as f32 - 40.0;
            let y = 10.0 + (index % 7) as f32 * 2.0;
            scene.spawn_cube(
                format!("Drop {index}"),
                Transform::new([x, y, z]),
                &drop,
            );
        }
        let count =
            scene.apply_gpu_physics_to_class("rain", &GpuBodySettings::default());
        println!("{count} drops on the GPU");

        scene.watch_gpu_class(
            "rain",
            GpuPhysicsRule::new(
                "drop_fell",
                GpuCondition::position_y().less_than(-20.0),
            )
            .mode(GpuEventMode::OnEnter)
            .payload(GpuEventPayload::Position),
        );
    });

    let events = scene.gpu_events("drop_fell");
    let fallen = FALLEN.fetch_add(events.len(), Ordering::Relaxed) + events.len();
    if let Some(first) = events.first() {
        println!("{:?} fell past Y = -20 at {:?}", first.physics_id, first.payload);
    }

    rusting_engine::egui::Window::new("Cube rain").show(&scene.ui(), |ui| {
        ui.label(format!("{:.0} FPS", 1.0 / time.delta_seconds().max(1e-6)));
        ui.label(format!("{fallen} of 2000 drops fell past Y = -20"));
    });
}

rusting_game!(update);
```

Run it with **Play** in the editor or `rusting run "Cube Rain"`. The cubes
fall, and the counter climbs as they pass Y = -20.

## 2. What each part does

**Classes.** `CubeSpawn::new().class("rain")` tags every spawned cube with
the class `rain`. A class names a group of objects so one call can act on
all of them.

**Moving bodies to the GPU.** `apply_gpu_physics_to_class` gives every
object in the class a GPU body and returns how many it changed. From then
on, the Vulkan compute solver moves them, not the CPU. `GpuBodySettings`
chooses how:

| Field | Default | Meaning |
| --- | --- | --- |
| `solver` | `Full` | `Full`, `Simplified`, `NoCollision`, `Space` (mutual gravity), or `Custom` |
| `custom_shader` | `None` | project-relative GLSL when `solver` is `Custom`; see [GPU condition shaders]../gpu-condition-shaders.md |
| `rigid_body` | `RigidBody::default()` | mass, starting velocity, gravity |
| `collider` | `Collider::default()` | shape, friction, restitution |
| `collision_layers` | all | which groups collide |

`rusting docs show api/GpuBodySettings` lists the same fields.

**Dense piles.** The GPU solver has no damping setting; `collider.friction`
is what decides how a packed pile moves. In a ball pit of 40,000 spheres
pushed through a 1.4 m tube, friction 0.3 jammed the tube (0 balls through
in 60 s) and 0.05 moved 38,948 balls in the same time. Start near 0.05
for plastic balls or grain that should flow. A dense pile also has a sharp
jamming threshold for the force that drives it: below some push it stops
completely rather than flowing slower, so pace a flow by how many bodies
move, not by a weaker push.

You can set the same thing in a scene: set an object's
`physics_body.simulation` to `Gpu`. See [Core concepts](../concepts.md#physics-cpu-and-gpu).

**Rules instead of readback.** The CPU never downloads the 2,000
transforms. `watch_gpu_class` attaches a rule to every body in the class.
The GPU tests the rule each tick and writes an event only when it matches:

- `GpuCondition` picks the test: `position_y()`, `velocity_y()`,
  `colliding()`, `sleeping()`, `timer_elapsed(seconds)`. Compare values with
  `less_than`, `greater_than`, or `inside`, and combine conditions with
  `and`, `or`, and `inverted`.
- `GpuEventMode::OnEnter` fires once when the condition becomes true, not
  on every tick while it stays true. Add `.cooldown(seconds)` to limit how
  often a rule can fire again.
- `GpuEventPayload` chooses the four numbers sent with the event:
  `Position`, `Velocity`, `AngularVelocity`, `Contact`, or `Custom`.

**Reading events.** `scene.gpu_events("drop_fell")` returns this frame's
events for that rule. Each `GpuPhysicsEvent` has the `entity`, its
`physics_id`, the `tick` it happened on, and the `payload` (`[x, y, z, 0]`
for `Position`). Events reach the CPU one to three frames after the tick
that caused them, because the CPU does not wait for the GPU.

**UI.** `scene.ui()` returns the frame's
[egui](https://docs.rs/egui) context; `rusting_engine::egui` is the matching
egui version. `update` is a plain function with no state of its own, so the
running total lives in a `static`. For real game state, use a plugin and a
resource, as in [Tutorial 3](03-gameplay-plugin.md).

## 3. Watch one object

To follow one body instead of a class, use `watch_gpu_object`:

```rust
scene.watch_gpu_object(
    "Drop 0",
    GpuPhysicsRule::new("first_drop_landed", GpuCondition::colliding())
        .mode(GpuEventMode::OnEnter),
);
```

Conditions combine with `&` and `|`, for example
`GpuCondition::colliding() & GpuCondition::position_y().less_than(0.0)`.

Add a floor for it to land on: a cube scaled to `[60, 1, 60]` at
`[0, -2, -20]` whose `physics_body.simulation` is `Static`. Static bodies
collide with both the CPU and the GPU solver. With the floor in place the
drops stop above Y = -20, so `drop_fell` no longer fires.

`scene.set_linear_velocity(name, [x, y, z])` sets a GPU body's velocity
from code, for example to launch a drop upward.

## 4. Testing GPU games

`rusting run --ticks` has no renderer, so GPU bodies stay where they started
and GPU rules never fire. `rusting test` runs them when the scenario has
`"gpu": true` or a `capture` step; a scenario without either leaves them
where they spawned. A scenario with `"gpu": true` and a check on a counter
that your `update` raises from `drop_fell` events tests the GPU side. Test
the CPU side with no Vulkan device, such as spawning and counting the class:

```bash
rusting run "Cube Rain" --ticks 30
```

The run exits with code 0 once the spawn code has worked. Check GPU
behaviour with a `"gpu": true` scenario, by playing the game, or with the engine's GPU tests
(`cargo test --features gpu-tests`), which run the solvers on a real or
software (lavapipe) Vulkan device.

## Going further

- `src/examples/hybrid_10k.rs` in the engine repository runs 10,000 GPU
  cubes beside CPU bodies. Cook `testGame` first
  (`rusting cook testGame`), then start it with
  `cargo run --release --example hybrid_10k`.
- `testGame/src/main.rs` uses the `Space` solver for an orbiting cloud.
- [Determinism]../determinism.md explains why CPU code that reacts to GPU
  events does not replay exactly yet.