Skip to main content

vehicle/
vehicle.rs

1//! Raycast-wheel vehicles: drive a box car over hilly terrain. The chassis
2//! is a dynamic body on four `Wheel3D`s; this file is the game's half:
3//! throttle, brakes and steering turned into wheel torques and angles.
4//!
5//!   `cargo run --release --example vehicle --features physics3d`
6//!
7//! W accelerates, S brakes (and reverses once stopped), A and D steer,
8//! Space is the handbrake, R puts the car back on its wheels.
9//!
10//! `HEADLESS=1` drives it by script instead, without a window, and checks
11//! that it speeds up, turns and stops.
12
13use ponko::*;
14
15const N: usize = 129;
16const SIZE: f32 = 512.0;
17
18fn height(x: f32, z: f32) -> f32 {
19    (x * 0.03).sin() * 3.0 + (z * 0.025).cos() * 4.0 + (x * 0.011 + z * 0.007).sin() * 6.0
20}
21
22/// What the driver asks for this tick, each -1..1 (steer: positive right).
23#[derive(Clone, Copy, Default)]
24struct Controls {
25    throttle: f32,
26    steer: f32,
27    handbrake: bool,
28}
29
30/// The car being driven, its steering (which turns smoothly) and the
31/// entities that draw its wheels.
32struct Player {
33    car: Entity,
34    steer: f32,
35    wheels: Vec<Entity>,
36}
37
38// ANCHOR: car
39/// A 1200 kg box car facing -Z, its center of mass low between the axles,
40/// front wheels first.
41fn spawn_car(ctx: &mut Ctx, at: Vec3) -> Entity {
42    let mut car = Vehicle3D::new();
43    for (x, z) in [(-0.8, -1.3), (0.8, -1.3), (-0.8, 1.3), (0.8, 1.3)] {
44        let mut wheel = Wheel3D::new(vec3(x, -0.3, z), 0.35);
45        wheel.sideways_friction.stiffness = 1.3;
46        car = car.with_wheel(wheel);
47    }
48    ctx.world.spawn((
49        Transform3D::at(at),
50        RigidBody3D::dynamic()
51            .mass(1200.0)
52            .center_of_mass(vec3(0.0, -0.35, 0.0)),
53        Collider3D::cuboid(vec3(1.8, 0.6, 4.2)),
54        Velocity3D::default(),
55        car,
56    ))
57}
58// ANCHOR_END: car
59
60// ANCHOR: drive
61/// The game's half of driving, before each physics step: rear-wheel
62/// drive, brakes on all four, steering that eases in and tightens less at
63/// speed.
64fn drive(ctx: &mut Ctx) {
65    let controls = *ctx.world.resource::<Controls>();
66    let dt = ctx.dt();
67    let car = ctx.world.resource::<Player>().car;
68    let forward = forward_speed(&ctx.world, car);
69    let wanted = controls.steer * 0.6 / (1.0 + forward.abs() * 0.08);
70    let player = ctx.world.resource_mut::<Player>();
71    player.steer += (wanted - player.steer).clamp(-2.0 * dt, 2.0 * dt);
72    let steer = player.steer;
73    let Some(vehicle) = ctx.world.get_mut::<Vehicle3D>(car) else {
74        return;
75    };
76    // Pressing against the way it rolls brakes; from (almost) still it drives.
77    let braking = controls.throttle * forward < 0.0 && forward.abs() > 0.5;
78    for (i, wheel) in vehicle.wheels_mut().iter_mut().enumerate() {
79        let front = i < 2;
80        wheel.steer_angle = if front { steer } else { 0.0 };
81        wheel.motor_torque = if front || braking {
82            0.0
83        } else {
84            controls.throttle * 900.0
85        };
86        wheel.brake_torque = if braking {
87            3000.0
88        } else if controls.handbrake && !front {
89            5000.0
90        } else {
91            0.0
92        };
93    }
94}
95// ANCHOR_END: drive
96
97fn speed_of(world: &World, car: Entity) -> f32 {
98    world.get::<Velocity3D>(car).map_or(0.0, |v| v.linear.length())
99}
100
101/// Speed along where the car faces (negative backing up).
102fn forward_speed(world: &World, car: Entity) -> f32 {
103    match (world.get::<Velocity3D>(car), world.get::<Transform3D>(car)) {
104        (Some(v), Some(t)) => v.linear.dot(t.forward()),
105        _ => 0.0,
106    }
107}
108
109/// Keys to controls (windowed), in the same tick as `drive`.
110fn keys(ctx: &mut Ctx) {
111    let axis = |plus: bool, minus: bool| plus as i32 as f32 - minus as i32 as f32;
112    let controls = Controls {
113        throttle: axis(ctx.key_down(Key::W), ctx.key_down(Key::S)),
114        steer: axis(ctx.key_down(Key::D), ctx.key_down(Key::A)),
115        handbrake: ctx.key_down(Key::Space),
116    };
117    *ctx.world.resource_mut::<Controls>() = controls;
118    if ctx.key_pressed(Key::R) {
119        let car = ctx.world.resource::<Player>().car;
120        if let Some(t) = ctx.world.get_mut::<Transform3D>(car) {
121            let p = t.position;
122            *t = Transform3D::at(vec3(p.x, height(p.x, p.z) + 1.5, p.z));
123        }
124    }
125}
126
127// ANCHOR: wheels
128/// Every frame: each wheel drawn where its car's last step put it (steer,
129/// spin and suspension travel), following the chassis's smoothed pose.
130fn place_wheels(ctx: &mut Ctx) {
131    let player = ctx.world.resource::<Player>();
132    let car = player.car;
133    let (Some(&vehicle), Some(&chassis)) = (ctx.world.get::<Vehicle3D>(car), ctx.world.get::<Transform3D>(car)) else {
134        return;
135    };
136    for i in 0..player.wheels.len() {
137        let e = ctx.world.resource::<Player>().wheels[i];
138        let pose = vehicle.wheels()[i].pose(&chassis);
139        if let Some(t) = ctx.world.get_mut::<Transform3D>(e) {
140            *t = Transform3D { scale: t.scale, ..pose };
141        }
142    }
143}
144// ANCHOR_END: wheels
145
146/// Every frame: the camera trails the car.
147fn chase(ctx: &mut Ctx) {
148    let car = ctx.world.resource::<Player>().car;
149    let Some(t) = ctx.world.get::<Transform3D>(car).copied() else {
150        return;
151    };
152    let back = -t.forward().with_y(0.0).normalize_or(Vec3::Z);
153    let eye = t.position + back * 9.0 + Vec3::Y * 3.5;
154    let k = (ctx.dt() * 4.0).min(1.0);
155    ctx.camera3d.position = ctx.camera3d.position.lerp(eye, k);
156    let look = Transform3D::at(ctx.camera3d.position).looking_at(t.position + Vec3::Y);
157    ctx.camera3d.rotation = look.rotation;
158}
159
160fn build_world(ctx: &mut Ctx, draw: bool) {
161    let corner = vec3(-SIZE / 2.0, 0.0, -SIZE / 2.0);
162    let step = SIZE / (N - 1) as f32;
163    let heights: Vec<f32> = (0..N * N)
164        .map(|i| height(corner.x + (i % N) as f32 * step, corner.z + (i / N) as f32 * step))
165        .collect();
166    let ground = ctx.physics3d.heightfield(&heights, SIZE);
167    let terrain = ctx
168        .world
169        .spawn((Transform3D::at(corner), RigidBody3D::fixed(), Collider3D::mesh(ground)));
170    let car = spawn_car(ctx, vec3(0.0, height(0.0, 0.0) + 1.5, 0.0));
171    ctx.world.insert_resource(Player {
172        car,
173        steer: 0.0,
174        wheels: Vec::new(),
175    });
176    ctx.world.insert_resource(Controls::default());
177    if !draw {
178        return;
179    }
180    let look = grass(ctx);
181    let tile = ctx.create_terrain_tile(&heights, SIZE, look);
182    ctx.world.insert(terrain, tile);
183    let paint = ctx.create_material(MaterialDesc::color(Color::hex(0xC8402E)));
184    let body = ctx.create_mesh(&MeshData::cube(1.0), paint);
185    if let Some(t) = ctx.world.get_mut::<Transform3D>(car) {
186        t.scale = vec3(1.8, 0.6, 4.2);
187    }
188    ctx.world.insert(car, Model::new(body));
189    let rubber = ctx.create_material(MaterialDesc::color(Color::hex(0x202020)));
190    let tyre = ctx.create_mesh(&MeshData::cube(1.0), rubber);
191    for _ in 0..4 {
192        let wheel = ctx.world.spawn((
193            Transform3D {
194                scale: vec3(0.3, 0.7, 0.7),
195                ..Transform3D::default()
196            },
197            Model::new(tyre),
198        ));
199        ctx.world.resource_mut::<Player>().wheels.push(wheel);
200    }
201}
202
203/// A one-layer terrain material: grass everywhere.
204fn grass(ctx: &mut Ctx) -> Material {
205    let green: Vec<u8> = (0..16 * 16)
206        .flat_map(|i| {
207            let shade = 200 + (i * 37 % 56) as u8;
208            [
209                (70 * shade as u32 / 255) as u8,
210                (120 * shade as u32 / 255) as u8,
211                45,
212                255,
213            ]
214        })
215        .collect();
216    let layers = ctx.create_texture_array(16, 16, &[&green], TextureKind::Color);
217    let weights = ctx.create_texture_array(1, 1, &[&[255, 0, 0, 0]], TextureKind::Data);
218    let corner = vec2(-SIZE / 2.0, -SIZE / 2.0);
219    ctx.create_terrain_material(&TerrainMaterialDesc::new(layers, weights, corner, vec2(SIZE, SIZE)))
220}
221
222/// The script for `HEADLESS=1`: full throttle for 4 s, then steer right
223/// for 2 s, then brake to a stop and hold it with the handbrake.
224fn autopilot(ctx: &mut Ctx) {
225    let t = ctx.sim_tick() as f32 * ctx.dt();
226    let car = ctx.world.resource::<Player>().car;
227    let rolling = forward_speed(&ctx.world, car) > 0.3;
228    *ctx.world.resource_mut::<Controls>() = Controls {
229        throttle: if t < 6.0 {
230            1.0
231        } else if rolling {
232            -1.0
233        } else {
234            0.0
235        },
236        steer: if (4.0..6.0).contains(&t) { 1.0 } else { 0.0 },
237        handbrake: t >= 6.0 && !rolling,
238    };
239}
240
241/// What the car did, second by second.
242#[derive(Default)]
243struct Log(Vec<(f32, Vec3)>);
244
245fn record(ctx: &mut Ctx) {
246    if ctx.sim_tick().is_multiple_of(50) {
247        let car = ctx.world.resource::<Player>().car;
248        let speed = speed_of(&ctx.world, car);
249        let facing = ctx.world.get::<Transform3D>(car).map_or(Vec3::NEG_Z, |t| t.forward());
250        ctx.world.resource_mut::<Log>().0.push((speed, facing));
251    }
252}
253
254/// Runs the script and checks what the car did.
255fn headless() {
256    let world = Game::new("vehicle", 1, 1)
257        .sim_system(autopilot)
258        .sim_system(drive)
259        .physics3d(vec3(0.0, -9.81, 0.0))
260        .sim_system(record)
261        .run_headless(500, |ctx| {
262            build_world(ctx, false);
263            ctx.world.insert_resource(Log::default());
264        });
265    let log = &world.resource::<Log>().0;
266    for (second, (speed, facing)) in log.iter().enumerate() {
267        println!("{:2} s: {speed:5.2} m/s, facing {facing:.2}", second + 1);
268    }
269    let (accelerated, _) = log[3];
270    let (_, turned) = log[5];
271    let (stopped, _) = log[log.len() - 1];
272    assert!(accelerated > 8.0, "accelerates: {accelerated} m/s after 4 s");
273    assert!(turned.x > 0.3, "steers right: facing {turned}");
274    assert!(stopped < 0.5, "stops: {stopped} m/s");
275    println!("accelerates, steers and stops");
276}
277
278fn main() {
279    if std::env::var("HEADLESS").is_ok() {
280        headless();
281        return;
282    }
283    // ANCHOR: setup
284    Game::new("Crabby Engine: vehicle", 1280, 720)
285        .sim_system(keys)
286        .sim_system(drive) // sets the wheels' inputs...
287        .physics3d(vec3(0.0, -9.81, 0.0)) // ...for the step that drives them
288        .system(place_wheels)
289        .system(chase)
290        // ANCHOR_END: setup
291        .run(|ctx| {
292            ctx.clear(Color::hex(0x9CC4E4));
293            build_world(ctx, true);
294        });
295}