Skip to main content

goud_engine/ecs/systems/
physics_sync_2d.rs

1//! 2D physics synchronization system.
2//!
3//! Provides [`PhysicsStepSystem2D`] which steps a [`PhysicsProvider`] each frame
4//! and synchronizes body positions back to ECS [`Transform2D`] components.
5//!
6//! # Usage
7//!
8//! ```rust,ignore
9//! use goud_engine::ecs::app::App;
10//! use goud_engine::ecs::schedule::CoreStage;
11//! use goud_engine::ecs::systems::physics_sync_2d::{PhysicsStepSystem2D, PhysicsHandleMap2D};
12//! use goud_engine::core::providers::impls::NullPhysicsProvider;
13//!
14//! let mut app = App::new();
15//! app.insert_resource(PhysicsHandleMap2D::default());
16//! app.add_system(
17//!     CoreStage::Update,
18//!     PhysicsStepSystem2D::new(Box::new(NullPhysicsProvider::new())),
19//! );
20//! ```
21
22use std::collections::HashMap;
23
24use crate::core::providers::physics::PhysicsProvider;
25use crate::core::providers::types::BodyHandle;
26use crate::ecs::entity::Entity;
27use crate::ecs::query::Access;
28use crate::ecs::system::System;
29use crate::ecs::World;
30
31/// Resource that maps ECS entities to physics body handles.
32///
33/// This resource tracks which entities have been registered with the physics
34/// provider. Users register entities by inserting them into this map before
35/// the physics step runs.
36///
37/// # Example
38///
39/// ```rust,ignore
40/// let mut map = PhysicsHandleMap2D::default();
41/// map.entity_to_body.insert(entity, body_handle);
42/// world.insert_resource(map);
43/// ```
44#[derive(Debug, Default)]
45pub struct PhysicsHandleMap2D {
46    /// Maps entities to their physics body handles.
47    pub entity_to_body: HashMap<Entity, BodyHandle>,
48}
49
50/// System that steps the 2D physics simulation and writes positions back.
51///
52/// Each frame this system:
53/// 1. Steps the physics provider by a fixed timestep (1/60 s).
54/// 2. For each entity tracked in [`PhysicsHandleMap2D`], reads the body
55///    position from the provider and updates the entity's `Transform2D`.
56///
57/// The system owns the physics provider. Users add bodies to the provider
58/// directly and register the entity-to-handle mapping in `PhysicsHandleMap2D`.
59pub struct PhysicsStepSystem2D {
60    provider: Box<dyn PhysicsProvider>,
61}
62
63impl PhysicsStepSystem2D {
64    /// Creates a new 2D physics step system with the given provider.
65    pub fn new(provider: Box<dyn PhysicsProvider>) -> Self {
66        Self { provider }
67    }
68
69    /// Returns a reference to the underlying physics provider.
70    pub fn provider(&self) -> &dyn PhysicsProvider {
71        &*self.provider
72    }
73
74    /// Returns a mutable reference to the underlying physics provider.
75    pub fn provider_mut(&mut self) -> &mut dyn PhysicsProvider {
76        &mut *self.provider
77    }
78}
79
80impl System for PhysicsStepSystem2D {
81    fn name(&self) -> &'static str {
82        "PhysicsStepSystem2D"
83    }
84
85    fn component_access(&self) -> Access {
86        // The system reads/writes Transform2D and reads the handle map resource,
87        // but since we access these through World methods rather than queries,
88        // we return empty access. The scheduler treats this as opaque.
89        Access::new()
90    }
91
92    fn run(&mut self, world: &mut World) {
93        // Step the physics simulation at a fixed 60 Hz timestep.
94        const FIXED_DT: f32 = 1.0 / 60.0;
95        if let Err(e) = self.provider.step(FIXED_DT) {
96            log::error!("PhysicsStepSystem2D: step failed: {e}");
97            return;
98        }
99
100        // Read back positions from the physics provider into Transform2D.
101        // We need to take the handle map out of the world temporarily to
102        // avoid holding an immutable borrow while mutating transforms.
103        let handle_map = match world.get_resource_mut::<PhysicsHandleMap2D>() {
104            Some(map) => {
105                // Collect entries to avoid borrowing world during iteration.
106                map.entity_to_body
107                    .iter()
108                    .map(|(&entity, &handle)| (entity, handle))
109                    .collect::<Vec<_>>()
110            }
111            None => return,
112        };
113
114        for (entity, body_handle) in handle_map {
115            if let Ok(pos) = self.provider.body_position(body_handle) {
116                use crate::core::math::Vec2;
117                use crate::ecs::components::transform2d::Transform2D;
118                if let Some(transform) = world.get_mut::<Transform2D>(entity) {
119                    transform.set_position(Vec2::new(pos[0], pos[1]));
120                }
121            }
122        }
123    }
124}
125
126// SAFETY: PhysicsProvider: Provider requires Send + Sync + 'static.
127// Box<dyn PhysicsProvider> is Send because Provider: Send.
128// This makes PhysicsStepSystem2D: Send, satisfying the System trait bound.
129
130#[cfg(test)]
131mod tests {
132    use super::*;
133    use crate::core::providers::impls::NullPhysicsProvider;
134
135    #[test]
136    fn test_handle_map_default() {
137        let map = PhysicsHandleMap2D::default();
138        assert!(map.entity_to_body.is_empty());
139    }
140
141    #[test]
142    fn test_system_construction() {
143        let provider = NullPhysicsProvider::new();
144        let system = PhysicsStepSystem2D::new(Box::new(provider));
145        assert_eq!(system.name(), "PhysicsStepSystem2D");
146    }
147
148    #[test]
149    fn test_system_run_empty_world() {
150        let provider = NullPhysicsProvider::new();
151        let mut system = PhysicsStepSystem2D::new(Box::new(provider));
152        let mut world = World::new();
153        // Should not panic even without the resource.
154        system.run(&mut world);
155    }
156
157    #[test]
158    fn test_system_run_with_empty_handle_map() {
159        let provider = NullPhysicsProvider::new();
160        let mut system = PhysicsStepSystem2D::new(Box::new(provider));
161        let mut world = World::new();
162        world.insert_resource(PhysicsHandleMap2D::default());
163        // Should not panic with an empty map.
164        system.run(&mut world);
165    }
166
167    #[test]
168    fn test_provider_accessors() {
169        let provider = NullPhysicsProvider::new();
170        let mut system = PhysicsStepSystem2D::new(Box::new(provider));
171        assert_eq!(system.provider().name(), "null");
172        assert_eq!(system.provider_mut().gravity(), [0.0, 0.0]);
173    }
174
175    #[test]
176    fn test_system_should_run() {
177        let provider = NullPhysicsProvider::new();
178        let system = PhysicsStepSystem2D::new(Box::new(provider));
179        let world = World::new();
180        assert!(system.should_run(&world));
181    }
182
183    #[test]
184    fn test_system_component_access_is_empty() {
185        let provider = NullPhysicsProvider::new();
186        let system = PhysicsStepSystem2D::new(Box::new(provider));
187        assert!(system.component_access().is_empty());
188    }
189}