Skip to main content

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