fixed_update/fixed_update.rs
1//! # Fixed Update — Bouncing Ball
2//!
3//! Demonstrates [`Window::on_fixed_update`]: a sphere simulates simple
4//! vertical physics (gravity + elastic ground bounce) using a **constant**
5//! timestep so the simulation is framerate-independent.
6//!
7//! A second, purely visual cube spins in `on_update` (variable dt) for
8//! comparison. Both share the same scene, showing how `on_update` and
9//! `on_fixed_update` coexist.
10//!
11//! **Run:**
12//! ```sh
13//! cargo run --example fixed_update
14//! ```
15//!
16//! **How it works:**
17//!
18//! * `on_fixed_update` is called at a fixed 60 Hz cadence regardless of the
19//! rendering frame rate. Use it for physics, AI ticks, or any logic that
20//! must not drift with framerate.
21//! * `on_update` is called once per rendered frame with a variable `dt`.
22//! Use it for visuals and input handling.
23//!
24//! > **Note:** both callbacks are **suppressed** while editor mode is active.
25//! > This example intentionally does *not* enable editor mode so you can watch
26//! > the simulation run.
27
28use vertra::camera::Camera;
29use vertra::geometry::Geometry;
30use vertra::objects::Object;
31use vertra::transform::Transform;
32use vertra::window::Window;
33
34/// Simulation and render state.
35struct AppState {
36 /// Numeric ID of the bouncing sphere.
37 ball_id: Option<usize>,
38 /// Numeric ID of the spinning cube.
39 cube_id: Option<usize>,
40 /// Current vertical velocity of the ball (m/s, world-space).
41 ball_vy: f32,
42}
43
44fn main() {
45 Window::new(AppState {
46 ball_id: None,
47 cube_id: None,
48 ball_vy: 8.0, // initial upward kick
49 })
50 .with_title("Fixed Update — Bouncing Ball")
51 .with_camera(
52 Camera::new()
53 .with_position([0.0, 3.0, -8.0])
54 .with_rotation(90.0, -15.0),
55 )
56 .on_startup(|state, scene, _| {
57 // Bouncing sphere
58 let ball_id = scene.spawn(
59 Object {
60 name: "Ball".to_string(),
61 str_id: "ball".to_string(),
62 geometry: Some(Geometry::Sphere {
63 radius: 0.5,
64 subdivisions: 20,
65 }),
66 color: [0.3, 0.7, 1.0, 1.0],
67 transform: Transform::from_position(0.0, 4.0, 0.0),
68 ..Default::default()
69 },
70 None,
71 );
72
73 // Ground plane
74 scene.spawn(
75 Object {
76 name: "Ground".to_string(),
77 str_id: "ground".to_string(),
78 geometry: Some(Geometry::Plane { size: 12.0 }),
79 color: [0.3, 0.6, 0.3, 1.0],
80 transform: Transform::from_position(0.0, 0.0, 0.0),
81 ..Default::default()
82 },
83 None,
84 );
85
86 // Spinning cube for on_update comparison
87 let cube_id = scene.spawn(
88 Object {
89 name: "Spinner".to_string(),
90 str_id: "spinner".to_string(),
91 geometry: Some(Geometry::Cube { size: 1.0 }),
92 color: [1.0, 0.5, 0.2, 1.0],
93 transform: Transform::from_position(3.5, 1.0, 0.0),
94 ..Default::default()
95 },
96 None,
97 );
98
99 state.ball_id = Some(ball_id);
100 state.cube_id = Some(cube_id);
101 })
102 // ── Physics tick (fixed 60 Hz) ────────────────────────────────────────────
103 .on_fixed_update(|state, scene, ctx| {
104 const GRAVITY: f32 = -12.0; // m/s²
105 const RESTITUTION: f32 = 0.78; // energy retained on bounce (0–1)
106 const GROUND_Y: f32 = 0.5; // ball radius — lowest allowed centre
107
108 if let Some(id) = state.ball_id {
109 // Integrate gravity.
110 state.ball_vy += GRAVITY * ctx.dt;
111
112 if let Some(ball) = scene.world.get_mut(id) {
113 ball.transform.position[1] += state.ball_vy * ctx.dt;
114
115 // Ground collision: reflect and damp.
116 if ball.transform.position[1] < GROUND_Y {
117 ball.transform.position[1] = GROUND_Y;
118 state.ball_vy = state.ball_vy.abs() * RESTITUTION;
119
120 // Stop micro-bounces that would never settle.
121 if state.ball_vy < 0.2 {
122 state.ball_vy = 0.0;
123 }
124 }
125 }
126 }
127 })
128 // ── Visual update (variable dt) ───────────────────────────────────────────
129 .on_update(|state, scene, ctx| {
130 // Spin the reference cube at 90 °/s — purely cosmetic.
131 if let Some(spinner) = state.cube_id.and_then(|id| scene.world.get_mut(id)) {
132 spinner.transform.rotation[1] += 90.0 * ctx.dt;
133 spinner.transform.rotation[0] += 45.0 * ctx.dt;
134 }
135 })
136 .create();
137}
138