Something went wrong. Try again.
Homebrew 4D Game Engine In Rust, inspired by Engine4D
Something went wrong. Try again.
123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239//! Game simulation system//!//! Manages the game loop simulation including://! - Delta time calculation//! - Input → physics movement//! - Physics stepping//! - Camera synchronization
use rust4d_core::SceneManager;use rust4d_game::CharacterController4D;use rust4d_input::CameraController;use rust4d_math::Vec4;use rust4d_render::camera4d::Camera4D;use std::time::Instant;
/// Result of a simulation updatepub struct SimulationResult { /// Whether geometry needs to be rebuilt and re-uploaded pub geometry_dirty: bool,}
/// Manages the game simulation loop////// Handles:/// - Delta time calculation/// - Input → physics movement/// - Physics stepping/// - Camera synchronizationpub struct SimulationSystem { last_frame: Instant,}
impl SimulationSystem { /// Create a new simulation system pub fn new() -> Self { Self { last_frame: Instant::now(), } }
/// Run one simulation frame /// /// # Arguments /// * `scene_manager` - Scene manager containing world and physics /// * `camera` - 4D camera to sync position to /// * `controller` - Input controller for movement/rotation /// * `character` - Character controller for player movement (None if no player body) /// * `cursor_captured` - Whether cursor is captured (enables mouse look) /// /// # Returns /// SimulationResult with dirty flag and delta time pub fn update( &mut self, scene_manager: &mut SceneManager, camera: &mut Camera4D, controller: &mut CameraController, character: Option<&CharacterController4D>, cursor_captured: bool, ) -> SimulationResult { // Calculate delta time let now = Instant::now(); let raw_dt = (now - self.last_frame).as_secs_f32(); // Cap dt to prevent spiral of death on first frame or after window focus // The physics accumulator further subdivides into fixed timesteps let dt = raw_dt.min(0.25); self.last_frame = now;
self.update_with_dt( dt, scene_manager, camera, controller, character, cursor_captured, ) }
/// Run one simulation frame with an explicit delta time. /// /// This is the deterministic core of [`update`](Self::update); tests and /// headless harnesses call it directly with a fixed timestep. pub fn update_with_dt( &mut self, dt: f32, scene_manager: &mut SceneManager, camera: &mut Camera4D, controller: &mut CameraController, character: Option<&CharacterController4D>, cursor_captured: bool, ) -> SimulationResult { // 2. Get movement input from controller let (forward_input, right_input) = controller.get_movement_input(); let w_input = controller.get_w_input();
// Guard against NaN/infinity from broken input state let forward_input = if forward_input.is_finite() { forward_input } else { 0.0 }; let right_input = if right_input.is_finite() { right_input } else { 0.0 }; let w_input = if w_input.is_finite() { w_input } else { 0.0 };
// 3. Calculate movement direction in world space using camera orientation // // INVARIANT (see tests/slice_invariant.rs): WASD movement must stay // inside the player's current 3D slice — its world-space direction // must remain orthogonal to the camera's ana axis. Otherwise the // camera drifts across the slice plane and every cross-section on // screen morphs. Two things guarantee the invariant here: // // 1. forward/right are projected to the horizontal XZW hyperplane // (Y zeroed). They remain orthogonal to ana because ana never has // a Y component (SkipY construction) and rotation preserves // orthogonality. // 2. Slice movement (WASD) and ana movement (Q/E) are passed to the // character controller SEPARATELY, so each is speed-scaled // uniformly. Scaling world axes anisotropically would tilt the // direction across the slice plane. let camera_forward = camera.forward(); let camera_right = camera.right(); let camera_ana = camera.ana();
// Project to XZW hyperplane (zero out Y for horizontal movement) let forward_xzw = Vec4::new(camera_forward.x, 0.0, camera_forward.z, camera_forward.w).normalized(); let right_xzw = Vec4::new(camera_right.x, 0.0, camera_right.z, camera_right.w).normalized(); let ana_xzw = Vec4::new(camera_ana.x, 0.0, camera_ana.z, camera_ana.w).normalized();
// Combine WASD slice movement; clamp to unit length to prevent faster // diagonal movement (otherwise 2-axis movement is ~41% faster) let slice_dir = forward_xzw * forward_input + right_xzw * right_input; let slice_dir = if slice_dir.length_squared() > 1.0 { slice_dir.normalized() } else { slice_dir };
// Q/E deliberately moves along the camera's ana axis (this is the one // movement that is SUPPOSED to change the visible slice) let ana_dir = ana_xzw * w_input;
// 4. Apply movement to player via character controller // The controller owns the speeds and scales each component uniformly if let (Some(character), Some(physics)) = ( character, scene_manager .active_world_mut() .and_then(|w| w.physics_mut()), ) { character.apply_movement(physics, slice_dir, ana_dir); }
// 5. Handle jump via character controller if controller.consume_jump() { if let (Some(character), Some(physics)) = ( character, scene_manager .active_world_mut() .and_then(|w| w.physics_mut()), ) { character.jump(physics); } }
// 6. Step world physics scene_manager.update(dt);
// 7. Check for dirty entities let geometry_dirty = scene_manager .active_world() .map(|w| w.has_dirty_entities()) .unwrap_or(false);
// 8. Sync camera position to player body (pre-controller) // This sets the camera to the physics-authoritative position BEFORE the // controller runs, so controller.update() in step 9 computes rotation // deltas from the correct starting position. if let (Some(character), Some(physics)) = ( character, scene_manager.active_world().and_then(|w| w.physics()), ) { if let Some(pos) = character.position(physics) { camera.position = pos; } }
// 9. Apply mouse look for camera rotation controller.update(camera, dt, cursor_captured);
// 10. Re-sync position after controller (keep rotation, discard position drift) // controller.update() in step 9 applies both rotation AND movement. We want // the rotation (mouse look) but not the movement (physics owns position). // Re-syncing here overwrites any position drift the controller introduced. if let (Some(character), Some(physics)) = ( character, scene_manager.active_world().and_then(|w| w.physics()), ) { if let Some(pos) = character.position(physics) { camera.position = pos; } }
SimulationResult { geometry_dirty } }}
impl Default for SimulationSystem { fn default() -> Self { Self::new() }}
#[cfg(test)]mod tests { use super::*;
#[test] fn test_delta_time_capped() { let sim = SimulationSystem::new(); // Simulate a 100ms pause (first frame or window focus) std::thread::sleep(std::time::Duration::from_millis(100));
// Can't fully test without scene manager, but we can verify construction assert!(sim.last_frame.elapsed().as_millis() >= 100); }
#[test] fn test_default_construction() { let sim = SimulationSystem::default(); // Just verify it constructs without panic assert!(sim.last_frame.elapsed().as_millis() < 100); }}