1use 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#[derive(Clone, Copy, Default)]
24struct Controls {
25 throttle: f32,
26 steer: f32,
27 handbrake: bool,
28}
29
30struct Player {
33 car: Entity,
34 steer: f32,
35 wheels: Vec<Entity>,
36}
37
38fn 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}
58fn 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 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}
95fn speed_of(world: &World, car: Entity) -> f32 {
98 world.get::<Velocity3D>(car).map_or(0.0, |v| v.linear.length())
99}
100
101fn 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
109fn 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
127fn 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}
144fn 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
203fn 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
222fn 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#[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
254fn 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 Game::new("Crabby Engine: vehicle", 1280, 720)
285 .sim_system(keys)
286 .sim_system(drive) .physics3d(vec3(0.0, -9.81, 0.0)) .system(place_wheels)
289 .system(chase)
290 .run(|ctx| {
292 ctx.clear(Color::hex(0x9CC4E4));
293 build_world(ctx, true);
294 });
295}