Skip to main content

collision_perimeter/
collision-perimeter.rs

1use mittens_engine::{engine, utils};
2
3#[path = "example_util/mod.rs"]
4mod example_util;
5
6fn main() {
7    mittens_engine::example_support::ensure_model_assets();
8    utils::logger::init();
9
10    let world = engine::ecs::World::default();
11    let mut universe = engine::Universe::new(world);
12
13    let bg_color = universe
14        .world
15        .add_component(engine::ecs::component::BackgroundColorComponent::new());
16    let bg_color_c = universe
17        .world
18        .add_component(engine::ecs::component::ColorComponent::rgba(
19            0.1, 0.02, 0.05, 1.0,
20        ));
21    let _ = universe.world.add_child(bg_color, bg_color_c);
22    universe.add(bg_color);
23
24    // Gravity field for the pushable cubes.
25    // Any KineticResponseComponent nested under this subtree will have gravity applied.
26    let gravity_field = universe
27        .world
28        .add_component(engine::ecs::component::GravityComponent::new().with_coefficient(0.5));
29    universe.add(gravity_field);
30
31    // Input-driven camera rig.
32    // Topology: I { T { C3D }  CN{Rigged { Sphere }} }
33    let input = universe
34        .world
35        .add_component(engine::ecs::component::InputComponent::new().with_speed(2.0));
36
37    let rig_transform = universe.world.add_component(
38        engine::ecs::component::TransformComponent::new().with_position(0.0, 0.75, 0.0),
39    );
40    let camera3d = universe
41        .world
42        .add_component(engine::ecs::component::Camera3DComponent::new());
43    let input_mode = universe.world.add_component(
44        engine::ecs::component::InputTransformModeComponent::forward_z()
45            .with_roll_axis_y()
46            .with_fps_rotation(),
47    );
48
49    let rig_collision = universe
50        .world
51        .add_component(engine::ecs::component::CollisionComponent::RIGGED());
52
53    // Opt-in to default kinematic-vs-static collision response.
54    // Policy note: collisions still emit signals regardless; response only runs for entities
55    // that explicitly add a KineticResponseComponent.
56    let rig_response = universe
57        .world
58        .add_component(engine::ecs::component::KineticResponseComponent::slide());
59    let rig_shape =
60        universe
61            .world
62            .add_component(engine::ecs::component::CollisionShapeComponent::new(
63                engine::ecs::component::CollisionShape::sphere_radius(0.25),
64            ));
65
66    let _ = universe.attach(input, input_mode);
67    let _ = universe.attach(input, rig_transform);
68    let _ = universe.attach(rig_transform, camera3d);
69
70    // Click-to-pick: treat this camera rig as a pointer source.
71    let pointer = universe
72        .world
73        .add_component(engine::ecs::component::PointerComponent::new());
74    let _ = universe.attach(camera3d, pointer);
75
76    // Topology: I { T { C3D } } — add a small camera-attached controls hint.
77    example_util::spawn_desktop_camera_controls_hint(&mut universe, rig_transform);
78
79    let _ = universe.attach(rig_transform, rig_collision);
80    let _ = universe.attach(rig_collision, rig_response);
81    let _ = universe.attach(rig_collision, rig_shape);
82
83    universe.add(input);
84
85    // Lights so we can see non-emissive meshes and verify attenuation/direction.
86    fn spawn_light(
87        universe: &mut engine::Universe,
88        x: f32,
89        y: f32,
90        z: f32,
91        r: f32,
92        g: f32,
93        b: f32,
94        intensity: f32,
95        distance: f32,
96    ) {
97        let t = universe.world.add_component(
98            engine::ecs::component::TransformComponent::new()
99                .with_position(x, y, z)
100                .with_scale(0.12, 0.12, 0.12),
101        );
102
103        let l = universe.world.add_component(
104            engine::ecs::component::PointLightComponent::new()
105                .with_color(r, g, b)
106                .with_intensity(intensity)
107                .with_distance(distance),
108        );
109        // Visual marker: a small white cube at the light position.
110        // White ensures it mostly shows the light color.
111        let marker = universe
112            .world
113            .add_component(engine::ecs::component::RenderableComponent::cube());
114        let marker_color =
115            universe
116                .world
117                .add_component(engine::ecs::component::ColorComponent::rgba(
118                    1.0, 1.0, 1.0, 1.0,
119                ));
120
121        let _ = universe.attach(t, l);
122        let _ = universe.attach(t, marker);
123        let _ = universe.attach(marker, marker_color);
124        universe.add(t);
125    }
126
127    // Perimeter of cubes: 16 per side, 90deg turns.
128    const N: i32 = 16;
129    let spacing = 1.0;
130    let half = (N as f32 - 1.0) * spacing * 0.5;
131
132    // Place 4 very distinct colored lights near corners plus a white overhead fill.
133    // (High-ish saturation makes it easy to tell which light is contributing.)
134    spawn_light(&mut universe, -half, 2.8, -half, 1.0, 0.1, 0.1, 2.5, 14.0); // red
135    spawn_light(&mut universe, half, 2.8, -half, 0.1, 1.0, 0.1, 2.5, 14.0); // green
136    spawn_light(&mut universe, half, 2.8, half, 0.1, 0.2, 1.0, 2.5, 14.0); // blue
137    spawn_light(&mut universe, -half, 2.8, half, 1.0, 0.2, 1.0, 2.5, 14.0); // magenta-ish
138    spawn_light(&mut universe, 0.0, 5.5, 0.0, 1.0, 1.0, 1.0, 0.7, 40.0); // white fill
139
140    fn spawn_wall_cube(universe: &mut engine::Universe, x: f32, y: f32, z: f32) {
141        let t = universe.world.add_component(
142            engine::ecs::component::TransformComponent::new().with_position(x, y, z),
143        );
144        let r = universe
145            .world
146            .add_component(engine::ecs::component::RenderableComponent::cube());
147        let cn = universe
148            .world
149            .add_component(engine::ecs::component::CollisionComponent::STATIC());
150        let c = universe
151            .world
152            .add_component(engine::ecs::component::ColorComponent::rgba(
153                0.9, 0.2, 0.2, 1.0,
154            ));
155
156        let _ = universe.attach(t, r);
157        let _ = universe.attach(t, cn);
158        let _ = universe.attach(r, c);
159
160        universe.add(t);
161    }
162
163    fn spawn_cube(
164        universe: &mut engine::Universe,
165        x: f32,
166        y: f32,
167        z: f32,
168        sx: f32,
169        sy: f32,
170        sz: f32,
171        r: f32,
172        g: f32,
173        b: f32,
174    ) {
175        let t = universe.world.add_component(
176            engine::ecs::component::TransformComponent::new()
177                .with_position(x, y, z)
178                .with_scale(sx, sy, sz),
179        );
180        let renderable = universe
181            .world
182            .add_component(engine::ecs::component::RenderableComponent::cube());
183        let color = universe
184            .world
185            .add_component(engine::ecs::component::ColorComponent::rgba(r, g, b, 1.0));
186
187        let _ = universe.attach(t, renderable);
188        let _ = universe.attach(renderable, color);
189
190        universe.add(t);
191    }
192
193    fn spawn_pushable_cube(
194        universe: &mut engine::Universe,
195        parent: engine::ecs::ComponentId,
196        x: f32,
197        y: f32,
198        z: f32,
199        s: f32,
200        r: f32,
201        g: f32,
202        b: f32,
203    ) {
204        let t = universe.world.add_component(
205            engine::ecs::component::TransformComponent::new()
206                .with_position(x, y, z)
207                .with_scale(s, s, s),
208        );
209        let renderable = universe
210            .world
211            .add_component(engine::ecs::component::RenderableComponent::cube());
212        let color = universe
213            .world
214            .add_component(engine::ecs::component::ColorComponent::rgba(r, g, b, 1.0));
215
216        let cn = universe
217            .world
218            .add_component(engine::ecs::component::CollisionComponent::KINEMATIC());
219        let response = universe.world.add_component(
220            engine::ecs::component::KineticResponseComponent::push()
221                .with_push_strength(4.0)
222                .with_friction_y(10.0),
223        );
224        let shape =
225            universe
226                .world
227                .add_component(engine::ecs::component::CollisionShapeComponent::new(
228                    engine::ecs::component::CollisionShape::cube_half_extents([
229                        0.5 * s,
230                        0.5 * s,
231                        0.5 * s,
232                    ]),
233                ));
234
235        let _ = universe.attach(t, renderable);
236        let _ = universe.attach(renderable, color);
237
238        let _ = universe.attach(t, cn);
239        let _ = universe.attach(cn, response);
240        let _ = universe.attach(cn, shape);
241
242        let _ = universe.attach(parent, t);
243    }
244
245    fn spawn_invisible_static_wall(
246        universe: &mut engine::Universe,
247        x: f32,
248        y: f32,
249        z: f32,
250        half_extents: [f32; 3],
251    ) {
252        let t = universe.world.add_component(
253            engine::ecs::component::TransformComponent::new().with_position(x, y, z),
254        );
255
256        let cn = universe
257            .world
258            .add_component(engine::ecs::component::CollisionComponent::STATIC());
259        let shape =
260            universe
261                .world
262                .add_component(engine::ecs::component::CollisionShapeComponent::new(
263                    engine::ecs::component::CollisionShape::cube_half_extents(half_extents),
264                ));
265
266        // Requested: attach kinematic_response as well (even though STATIC colliders
267        // are not responders).
268        let response = universe
269            .world
270            .add_component(engine::ecs::component::KineticResponseComponent::slide());
271
272        let _ = universe.attach(t, cn);
273        let _ = universe.attach(cn, shape);
274        let _ = universe.attach(cn, response);
275        universe.add(t);
276    }
277
278    let y = 0.5;
279
280    // Static ground plane (thin cube). Top surface at y=0.
281    {
282        let ground_half = half + 2.0;
283        let thickness = 0.20;
284
285        // Split transforms so scaling the ground does not scale any potential children.
286        // Top surface at y=0.
287        let ground_root_t = universe
288            .world
289            .add_component(engine::ecs::component::TransformComponent::new());
290        let ground_geom_t = universe.world.add_component(
291            engine::ecs::component::TransformComponent::new()
292                .with_position(0.0, -thickness, 0.0)
293                .with_scale(ground_half * 2.0, thickness * 2.0, ground_half * 2.0),
294        );
295        let ground_r = universe
296            .world
297            .add_component(engine::ecs::component::RenderableComponent::cube());
298        let ground_c = universe
299            .world
300            .add_component(engine::ecs::component::ColorComponent::rgba(
301                0.08, 0.08, 0.09, 1.0,
302            ));
303        let ground_cn = universe
304            .world
305            .add_component(engine::ecs::component::CollisionComponent::STATIC());
306        let ground_shape =
307            universe
308                .world
309                .add_component(engine::ecs::component::CollisionShapeComponent::new(
310                    engine::ecs::component::CollisionShape::cube_half_extents([
311                        ground_half,
312                        thickness,
313                        ground_half,
314                    ]),
315                ));
316
317        let _ = universe.attach(ground_root_t, ground_geom_t);
318        let _ = universe.attach(ground_geom_t, ground_r);
319        let _ = universe.attach(ground_r, ground_c);
320        let _ = universe.attach(ground_geom_t, ground_cn);
321        let _ = universe.attach(ground_cn, ground_shape);
322        universe.add(ground_root_t);
323    }
324
325    // Bottom edge (left -> right)
326    for i in 0..N {
327        let x = -half + (i as f32) * spacing;
328        let z = -half;
329        spawn_wall_cube(&mut universe, x, y, z);
330    }
331
332    // Right edge (bottom -> top), skip first corner
333    for i in 1..N {
334        let x = half;
335        let z = -half + (i as f32) * spacing;
336        spawn_wall_cube(&mut universe, x, y, z);
337    }
338
339    // Top edge (right -> left), skip first corner
340    for i in 1..N {
341        let x = half - (i as f32) * spacing;
342        let z = half;
343        spawn_wall_cube(&mut universe, x, y, z);
344    }
345
346    // Left edge (top -> bottom), skip first and last corners
347    for i in 1..(N - 1) {
348        let x = -half;
349        let z = half - (i as f32) * spacing;
350        spawn_wall_cube(&mut universe, x, y, z);
351    }
352
353    // A few larger cubes outside the perimeter (visual landmarks + lighting test surfaces).
354    let outer = half + 3.0;
355    spawn_cube(
356        &mut universe,
357        0.0,
358        0.75,
359        -outer,
360        3.0,
361        1.5,
362        1.0,
363        0.15,
364        0.15,
365        0.18,
366    );
367    spawn_cube(
368        &mut universe,
369        0.0,
370        0.75,
371        outer,
372        3.0,
373        1.5,
374        1.0,
375        0.15,
376        0.15,
377        0.18,
378    );
379    spawn_cube(
380        &mut universe,
381        -outer,
382        0.75,
383        0.0,
384        1.0,
385        1.5,
386        3.0,
387        0.15,
388        0.15,
389        0.18,
390    );
391    spawn_cube(
392        &mut universe,
393        outer,
394        0.75,
395        0.0,
396        1.0,
397        1.5,
398        3.0,
399        0.15,
400        0.15,
401        0.18,
402    );
403
404    // Outer containment walls (invisible): keep runaway cubes near the scene.
405    // Collision shapes are in world units; transform scale is irrelevant for collision.
406    {
407        let wall_center_y = 6.0; // bottom at y=0, top at y=12
408        let wall_half_height = 6.0;
409        let wall_half_thickness = 0.25;
410        let wall_offset = half + 6.0;
411        let wall_half_len = wall_offset + 2.0;
412
413        // +Z / -Z walls
414        spawn_invisible_static_wall(
415            &mut universe,
416            0.0,
417            wall_center_y,
418            wall_offset,
419            [wall_half_len, wall_half_height, wall_half_thickness],
420        );
421        spawn_invisible_static_wall(
422            &mut universe,
423            0.0,
424            wall_center_y,
425            -wall_offset,
426            [wall_half_len, wall_half_height, wall_half_thickness],
427        );
428
429        // +X / -X walls
430        spawn_invisible_static_wall(
431            &mut universe,
432            wall_offset,
433            wall_center_y,
434            0.0,
435            [wall_half_thickness, wall_half_height, wall_half_len],
436        );
437        spawn_invisible_static_wall(
438            &mut universe,
439            -wall_offset,
440            wall_center_y,
441            0.0,
442            [wall_half_thickness, wall_half_height, wall_half_len],
443        );
444
445        // Roof
446        spawn_invisible_static_wall(
447            &mut universe,
448            0.0,
449            wall_center_y + wall_half_height + wall_half_thickness,
450            0.0,
451            [wall_half_len, wall_half_thickness, wall_half_len],
452        );
453    }
454
455    // Many small cubes inside the perimeter.
456    // Deterministic pseudo-random distribution so the scene is stable.
457    let mut seed: u32 = 0xC0111510;
458    let mut rng01 = || {
459        seed ^= seed << 13;
460        seed ^= seed >> 17;
461        seed ^= seed << 5;
462        (seed as f32) / (u32::MAX as f32)
463    };
464
465    // 3x more cubes, with three distinct size bands.
466    for band in 0..3 {
467        for _ in 0..60 {
468            let s = match band {
469                0 => 0.12 + rng01() * 0.20, // small
470                1 => 0.35 + rng01() * 0.35, // medium
471                _ => 0.75 + rng01() * 0.55, // large
472            };
473
474            let bound = (half - (1.0 + s)).max(0.5);
475            let x = (rng01() * 2.0 - 1.0) * bound;
476            let z = (rng01() * 2.0 - 1.0) * bound;
477            let y = 0.5 * s;
478
479            // Slightly varied colors to show multiple light contributions.
480            let cr = 0.25 + rng01() * 0.6;
481            let cg = 0.25 + rng01() * 0.6;
482            let cb = 0.25 + rng01() * 0.6;
483
484            spawn_pushable_cube(&mut universe, gravity_field, x, y, z, s, cr, cg, cb);
485        }
486    }
487
488    // Process init-time registrations.
489    universe.systems.process_commands(
490        &mut universe.world,
491        &mut universe.visuals,
492        &mut universe.render_assets,
493        &mut universe.command_queue,
494    );
495
496    engine::Windowing::run_app(universe).expect("Windowing failed");
497}