khora_lanes/physics_lane/
mod.rs1mod native_lanes;
20mod physics_debug_lane;
21
22pub use native_lanes::*;
23pub use physics_debug_lane::*;
24
25use std::collections::{HashMap, HashSet};
26
27use khora_core::ecs::entity::EntityId;
28use khora_core::physics::{ColliderDesc, PhysicsProvider, RigidBodyDesc};
29use khora_data::ecs::{Collider, GlobalTransform, Parent, RigidBody, Transform, World};
30
31#[derive(Debug, Default)]
33pub struct StandardPhysicsLane;
34
35impl StandardPhysicsLane {
36 pub fn new() -> Self {
38 Self
39 }
40
41 fn sync_to_world(&self, world: &mut World, provider: &mut dyn PhysicsProvider) {
43 let mut active_bodies = HashSet::new();
44 let mut active_colliders = HashSet::new();
45
46 let rb_map = self.sync_rigid_bodies(world, provider, &mut active_bodies);
48
49 self.sync_colliders(world, provider, &mut active_colliders, &rb_map);
51
52 self.cleanup_orphans(provider, &active_bodies, &active_colliders);
54 }
55
56 fn sync_rigid_bodies(
57 &self,
58 world: &mut World,
59 provider: &mut dyn PhysicsProvider,
60 active_bodies: &mut HashSet<khora_core::physics::RigidBodyHandle>,
61 ) -> HashMap<EntityId, khora_core::physics::RigidBodyHandle> {
62 let mut rb_map = HashMap::new();
63 let query = world.query_mut::<(EntityId, &GlobalTransform, &mut RigidBody)>();
64
65 for (entity_id, transform, rb) in query {
66 let current_pos = transform.0.translation();
67 let current_rot = transform.0.rotation();
68
69 let desc = RigidBodyDesc {
70 position: current_pos,
71 rotation: current_rot,
72 body_type: rb.body_type,
73 linear_velocity: rb.linear_velocity,
74 angular_velocity: rb.angular_velocity,
75 mass: rb.mass,
76 ccd_enabled: rb.ccd_enabled,
77 };
78
79 let handle = if let Some(handle) = rb.handle {
80 let (phys_pos, phys_rot) = provider.get_body_transform(handle);
82 if (phys_pos - current_pos).length_squared() > 0.0001
83 || phys_rot.dot(current_rot).abs() < 0.9999
84 {
85 provider.set_body_transform(handle, current_pos, current_rot);
86 }
87 provider.update_body_properties(handle, desc);
88 handle
89 } else {
90 let h = provider.add_body(desc);
91 rb.handle = Some(h);
92 h
93 };
94
95 rb_map.insert(entity_id, handle);
96 active_bodies.insert(handle);
97 }
98 rb_map
99 }
100
101 fn sync_colliders(
102 &self,
103 world: &mut World,
104 provider: &mut dyn PhysicsProvider,
105 active_colliders: &mut HashSet<khora_core::physics::ColliderHandle>,
106 rb_map: &HashMap<EntityId, khora_core::physics::RigidBodyHandle>,
107 ) {
108 let mut parent_map = HashMap::new();
110 for (id, parent) in world.query::<(EntityId, &Parent)>() {
111 parent_map.insert(id, parent.0);
112 }
113
114 let mut parent_transforms = HashMap::new();
117 for (id, gt) in world.query::<(EntityId, &GlobalTransform)>() {
118 parent_transforms.insert(id, *gt);
122 }
123
124 let mut active_events = HashSet::new();
126 for (id, _) in world.query::<(EntityId, &khora_data::ecs::ActiveEvents)>() {
127 active_events.insert(id);
128 }
129
130 let mut materials = HashMap::new();
131 for (id, mat) in world.query::<(EntityId, &khora_data::ecs::PhysicsMaterial)>() {
132 materials.insert(id, *mat);
133 }
134
135 let query = world.query_mut::<(EntityId, &mut Collider, &GlobalTransform)>();
136 for (entity_id, collider, transform) in query {
137 let is_active = active_events.contains(&entity_id);
138 let material = materials.get(&entity_id).cloned().unwrap_or_default();
139
140 let desc = self.build_collider_desc(
141 entity_id,
142 transform,
143 collider,
144 &parent_map,
145 &parent_transforms,
146 is_active,
147 &material,
148 rb_map,
149 );
150
151 let handle = if let Some(handle) = collider.handle {
152 provider.update_collider_properties(handle, desc);
153 handle
154 } else {
155 let h = provider.add_collider(desc);
156 collider.handle = Some(h);
157 h
158 };
159
160 active_colliders.insert(handle);
161 }
162 }
163
164 #[allow(clippy::too_many_arguments)]
165 fn build_collider_desc(
166 &self,
167 entity_id: EntityId,
168 transform: &GlobalTransform,
169 collider: &Collider,
170 parent_map: &HashMap<EntityId, EntityId>,
171 parent_transforms: &HashMap<EntityId, GlobalTransform>,
172 active_events: bool,
173 material: &khora_data::ecs::PhysicsMaterial,
174 rb_map: &HashMap<EntityId, khora_core::physics::RigidBodyHandle>,
175 ) -> ColliderDesc {
176 let (parent_handle, parent_id) = self.find_parent_body(entity_id, parent_map, rb_map);
177 let mut pos = transform.0.translation();
178 let mut rot = transform.0.rotation();
179
180 if let Some(p_id) = parent_id {
182 if p_id != entity_id {
183 if let Some(p_global) = parent_transforms.get(&p_id) {
184 if let Some(inv_p) = p_global.0.inverse() {
185 let local = inv_p.0 * transform.0 .0;
186 let local_t = khora_core::math::AffineTransform(local);
187 pos = local_t.translation();
188 rot = local_t.rotation();
189 }
190 }
191 }
192 }
193
194 ColliderDesc {
195 parent_body: parent_handle,
196 position: pos,
197 rotation: rot,
198 shape: collider.shape.clone(),
199 active_events,
200 friction: material.friction,
201 restitution: material.restitution,
202 }
203 }
204
205 fn find_parent_body(
206 &self,
207 entity_id: EntityId,
208 parent_map: &HashMap<EntityId, EntityId>,
209 rb_map: &HashMap<EntityId, khora_core::physics::RigidBodyHandle>,
210 ) -> (
211 Option<khora_core::physics::RigidBodyHandle>,
212 Option<EntityId>,
213 ) {
214 let mut curr = entity_id;
215 loop {
216 if let Some(h) = rb_map.get(&curr) {
217 return (Some(*h), Some(curr));
218 }
219 if let Some(p) = parent_map.get(&curr) {
220 curr = *p;
221 } else {
222 break;
223 }
224 }
225 (None, None)
226 }
227
228 fn cleanup_orphans(
229 &self,
230 provider: &mut dyn PhysicsProvider,
231 active_bodies: &HashSet<khora_core::physics::RigidBodyHandle>,
232 active_colliders: &HashSet<khora_core::physics::ColliderHandle>,
233 ) {
234 for h in provider.get_all_bodies() {
235 if !active_bodies.contains(&h) {
236 provider.remove_body(h);
237 }
238 }
239 for h in provider.get_all_colliders() {
240 if !active_colliders.contains(&h) {
241 provider.remove_collider(h);
242 }
243 }
244 }
245
246 fn sync_from_world(&self, world: &mut World, provider: &dyn PhysicsProvider) {
248 let query = world.query_mut::<(&mut Transform, &mut RigidBody)>();
249 for (transform, rb) in query {
250 if let Some(handle) = rb.handle {
251 let (pos, rot) = provider.get_body_transform(handle);
252 transform.translation = pos;
254 transform.rotation = rot;
255 }
256 }
257 }
258
259 fn resolve_characters(&self, world: &mut World, provider: &dyn PhysicsProvider) {
260 let mut results = Vec::new();
261 {
262 let query = world.query_mut::<(
263 EntityId,
264 &mut khora_data::ecs::KinematicCharacterController,
265 &Collider,
266 )>();
267 for (id, kcc, collider) in query {
268 if let Some(h) = collider.handle {
269 let options = khora_core::physics::CharacterControllerOptions {
270 autostep_height: kcc.autostep_height,
271 autostep_min_width: kcc.autostep_min_width,
272 autostep_enabled: kcc.autostep_enabled,
273 max_slope_climb_angle: kcc.max_slope_climb_angle,
274 min_slope_slide_angle: kcc.min_slope_slide_angle,
275 offset: kcc.offset,
276 };
277 let (m, g) = provider.move_character(h, kcc.desired_translation, &options);
278 results.push((id, m, g));
279 }
280 }
281 }
282
283 for (id, m, g) in results {
284 if let Some(kcc) = world.get_mut::<khora_data::ecs::KinematicCharacterController>(id) {
285 kcc.is_grounded = g;
286 kcc.desired_translation = khora_core::math::Vec3::ZERO;
287 }
288 if let Some(transform) = world.get_mut::<Transform>(id) {
289 transform.translation = transform.translation + m;
290 }
291 }
292 }
293
294 fn dispatch_events(&self, world: &mut World, provider: &dyn PhysicsProvider) {
295 let events = provider.get_collision_events();
296 let query = world.query_mut::<(EntityId, &mut khora_data::ecs::CollisionEvents)>();
297 for (_, buffer) in query {
298 if events.is_empty() {
299 buffer.events.clear();
300 } else {
301 buffer.events = events.clone();
302 }
303 }
304 }
305}
306
307impl khora_core::lane::Lane for StandardPhysicsLane {
308 fn strategy_name(&self) -> &'static str {
309 "StandardPhysics"
310 }
311
312 fn lane_kind(&self) -> khora_core::lane::LaneKind {
313 khora_core::lane::LaneKind::Physics
314 }
315
316 fn execute(
317 &self,
318 ctx: &mut khora_core::lane::LaneContext,
319 ) -> Result<(), khora_core::lane::LaneError> {
320 use khora_core::lane::{LaneError, Slot};
321
322 let dt = ctx
323 .get::<khora_core::lane::PhysicsDeltaTime>()
324 .ok_or(LaneError::missing("PhysicsDeltaTime"))?
325 .0;
326 let world = ctx
327 .get::<Slot<World>>()
328 .ok_or(LaneError::missing("Slot<World>"))?
329 .get();
330 let provider = ctx
331 .get::<Slot<dyn PhysicsProvider>>()
332 .ok_or(LaneError::missing("Slot<dyn PhysicsProvider>"))?
333 .get();
334
335 self.step(world, provider, dt);
336 Ok(())
337 }
338
339 fn as_any(&self) -> &dyn std::any::Any {
340 self
341 }
342
343 fn as_any_mut(&mut self) -> &mut dyn std::any::Any {
344 self
345 }
346}
347
348impl StandardPhysicsLane {
349 pub fn step(&self, world: &mut World, provider: &mut dyn PhysicsProvider, dt: f32) {
351 self.sync_to_world(world, provider);
353
354 provider.step(dt);
356
357 self.sync_from_world(world, provider);
359
360 self.resolve_characters(world, provider);
362
363 self.dispatch_events(world, provider);
365 }
366}