mittens_engine/engine/ecs/system/
kinetic_response_system.rs1use crate::engine::ecs::component::{
2 CollisionComponent, CollisionMode, CollisionShape, CollisionShapeComponent,
3 KineticResponseComponent, KineticResponseMode, RenderableComponent, TransformComponent,
4};
5use crate::engine::ecs::system::{CollisionSystem, TransformSystem};
6use crate::engine::ecs::{ComponentId, IntentValue, SignalEmitter, World};
7use crate::engine::graphics::VisualWorld;
8use crate::engine::user_input::InputState;
9use crate::utils::math;
10use std::collections::HashMap;
11
12#[derive(Debug, Default)]
13pub struct KineticResponseSystem {
14 responders: Vec<ComponentId>,
15}
16
17impl KineticResponseSystem {
18 pub fn new() -> Self {
19 Self::default()
20 }
21
22 pub fn register_kinetic_response(&mut self, world: &mut World, component: ComponentId) {
23 if !self.responders.iter().any(|c| *c == component) {
24 self.responders.push(component);
25 }
26
27 let mut cur = component;
29 let mut gravity_coef = 0.0f32;
30 while let Some(parent) = world.parent_of(cur) {
31 if let Some(g) = world
32 .get_component_by_id_as::<crate::engine::ecs::component::GravityComponent>(parent)
33 {
34 if g.enabled {
35 gravity_coef = g.coefficient;
36 break;
37 }
38 }
39 cur = parent;
40 }
41
42 if let Some(r) = world.get_component_by_id_as_mut::<KineticResponseComponent>(component) {
43 r.gravity_coefficient = gravity_coef;
44 }
45 }
46
47 pub fn remove_kinetic_response(&mut self, component: ComponentId) {
48 self.responders.retain(|c| *c != component);
49 }
50
51 pub fn tick_with_queue(
52 &mut self,
53 world: &mut World,
54 _visuals: &mut VisualWorld,
55 _input: &InputState,
56 dt_sec: f32,
57 emit: &mut dyn SignalEmitter,
58 collision: &CollisionSystem,
59 ) {
60 if self.responders.is_empty() {
61 return;
62 }
63
64 let pairs = collision.active_pairs_with_delta_snapshot();
65
66 let mut overlaps_by_collider: HashMap<ComponentId, Vec<(ComponentId, [f32; 3])>> =
70 HashMap::new();
71 if !pairs.is_empty() {
72 for (a, b, delta_ab) in pairs.iter().copied() {
73 overlaps_by_collider
75 .entry(a)
76 .or_default()
77 .push((b, [-delta_ab[0], -delta_ab[1], -delta_ab[2]]));
78 overlaps_by_collider
79 .entry(b)
80 .or_default()
81 .push((a, delta_ab));
82 }
83 }
84
85 let responder_ids: Vec<ComponentId> = self.responders.iter().copied().collect();
86 let mut pending_updates: Vec<(ComponentId, ComponentId, [f32; 3], [f32; 3])> = Vec::new();
87
88 for response_cid in responder_ids {
89 let Some(response) =
90 world.get_component_by_id_as::<KineticResponseComponent>(response_cid)
91 else {
92 continue;
93 };
94 if !response.enabled {
95 continue;
96 }
97
98 let Some(collider_cid) = world.parent_of(response_cid) else {
101 continue;
102 };
103 if world
104 .get_component_by_id_as::<CollisionComponent>(collider_cid)
105 .is_none()
106 {
107 continue;
108 }
109
110 let Some(transform_cid) = world.parent_of(collider_cid) else {
111 continue;
112 };
113 if world
114 .get_component_by_id_as::<TransformComponent>(transform_cid)
115 .is_none()
116 {
117 continue;
118 }
119
120 let Some(collider) = world.get_component_by_id_as::<CollisionComponent>(collider_cid)
122 else {
123 continue;
124 };
125 if collider.mode == CollisionMode::Static {
126 continue;
127 }
128
129 let gravity_coef = response.gravity_coefficient;
130
131 let overlaps: Vec<(ComponentId, [f32; 3])> = overlaps_by_collider
132 .get(&collider_cid)
133 .cloned()
134 .unwrap_or_default();
135
136 let mut statics: Vec<ComponentId> = Vec::new();
138 let mut non_statics: Vec<(ComponentId, [f32; 3])> = Vec::new();
139 for (other, delta_other_to_self) in overlaps {
140 let Some(c) = world.get_component_by_id_as::<CollisionComponent>(other) else {
141 continue;
142 };
143 if c.mode == CollisionMode::Static {
144 statics.push(other);
145 } else {
146 non_statics.push((other, delta_other_to_self));
147 }
148 }
149
150 if response.mode == KineticResponseMode::Slide && statics.is_empty() {
152 continue;
153 }
154
155 let mut moved = false;
156 let mut desired_world_pos = match TransformSystem::world_position(world, transform_cid)
157 {
158 Some(p) => p,
159 None => continue,
160 };
161 let base_world_pos = desired_world_pos;
162
163 let mut velocity = response.velocity;
165
166 if dt_sec > 0.0 && gravity_coef != 0.0 {
168 const GRAVITY_MPS2: f32 = -9.81;
169 velocity[1] += GRAVITY_MPS2 * gravity_coef * dt_sec;
170 }
171
172 if response.mode == KineticResponseMode::Push {
173 if dt_sec > 0.0 && response.friction > 0.0 {
175 let k = (1.0 - response.friction * dt_sec).clamp(0.0, 1.0);
176 velocity[0] *= k;
177 velocity[1] *= k;
178 velocity[2] *= k;
179 }
180
181 if !non_statics.is_empty() && dt_sec > 0.0 && response.push_strength != 0.0 {
183 let mut sum = [0.0f32; 3];
184 let mut n = 0.0f32;
185 let self_motion = [
186 desired_world_pos[0] - base_world_pos[0],
187 desired_world_pos[1] - base_world_pos[1],
188 desired_world_pos[2] - base_world_pos[2],
189 ];
190
191 for &(_other_cid, delta_other_to_self_at_base) in non_statics.iter() {
192 let adjusted = [
194 delta_other_to_self_at_base[0] + self_motion[0],
195 delta_other_to_self_at_base[1] + self_motion[1],
196 delta_other_to_self_at_base[2] + self_motion[2],
197 ];
198
199 sum[0] += adjusted[0];
201 sum[1] += adjusted[1];
202 sum[2] += adjusted[2];
203 n += 1.0;
204 }
205
206 if n > 0.0 {
207 let avg = [sum[0] / n, sum[1] / n, sum[2] / n];
208 velocity[0] += avg[0] * response.push_strength * dt_sec;
209 velocity[1] += avg[1] * response.push_strength * dt_sec;
210 velocity[2] += avg[2] * response.push_strength * dt_sec;
211 }
212 }
213
214 if response.max_speed > 0.0 {
216 let speed = (velocity[0] * velocity[0]
217 + velocity[1] * velocity[1]
218 + velocity[2] * velocity[2])
219 .sqrt();
220 if speed > response.max_speed {
221 let s = response.max_speed / speed;
222 velocity[0] *= s;
223 velocity[1] *= s;
224 velocity[2] *= s;
225 }
226 }
227
228 desired_world_pos[0] += velocity[0] * dt_sec;
230 desired_world_pos[1] += velocity[1] * dt_sec;
231 desired_world_pos[2] += velocity[2] * dt_sec;
232 moved = moved || velocity != [0.0, 0.0, 0.0];
233 }
234
235 if response.mode == KineticResponseMode::Slide && dt_sec > 0.0 {
238 if velocity[1] != 0.0 {
239 desired_world_pos[1] += velocity[1] * dt_sec;
240 moved = true;
241 }
242 }
243
244 let a_shape = resolve_shape(world, collider_cid)
245 .unwrap_or_else(|| crate::engine::ecs::component::CollisionShape::CUBE());
246
247 for _ in 0..response.max_iterations {
249 let mut any_overlap = false;
250
251 for &static_cid in statics.iter() {
252 let Some(static_parent) = world.parent_of(static_cid) else {
253 continue;
254 };
255 if world
256 .get_component_by_id_as::<TransformComponent>(static_parent)
257 .is_none()
258 {
259 continue;
260 }
261
262 let b_world_pos = match TransformSystem::world_position(world, static_parent) {
263 Some(p) => p,
264 None => continue,
265 };
266 let b_shape = resolve_shape(world, static_cid)
267 .unwrap_or_else(|| crate::engine::ecs::component::CollisionShape::CUBE());
268
269 let Some(push) = compute_push_out_aabb(
270 desired_world_pos,
271 a_shape,
272 b_world_pos,
273 b_shape,
274 response.push_out_epsilon,
275 ) else {
276 continue;
277 };
278
279 desired_world_pos[0] += push[0];
280 desired_world_pos[1] += push[1];
281 desired_world_pos[2] += push[2];
282
283 let axis = if push[0] != 0.0 {
284 0
285 } else if push[1] != 0.0 {
286 1
287 } else {
288 2
289 };
290
291 if axis == 1 {
292 if dt_sec > 0.0 && response.friction_y > 0.0 {
294 let k = (1.0 - response.friction_y * dt_sec).clamp(0.0, 1.0);
295 velocity[1] *= k;
296 }
297 } else if response.mode == KineticResponseMode::Push {
298 if velocity[axis] * push[axis] < 0.0 {
302 const RESTITUTION: f32 = 0.85;
303 velocity[axis] = -velocity[axis] * RESTITUTION;
304 }
305 }
306 any_overlap = true;
307 }
308
309 if !any_overlap {
310 break;
311 }
312 moved = true;
313 }
314
315 if !moved {
316 continue;
317 }
318
319 let new_local_translation =
320 world_to_local_translation(world, transform_cid, desired_world_pos);
321
322 pending_updates.push((response_cid, transform_cid, new_local_translation, velocity));
323 }
324
325 for (response_cid, transform_cid, new_local_translation, new_velocity) in pending_updates {
326 if let Some(r) =
327 world.get_component_by_id_as_mut::<KineticResponseComponent>(response_cid)
328 {
329 r.velocity = new_velocity;
330 }
331 if let Some(t) = world.get_component_by_id_as_mut::<TransformComponent>(transform_cid) {
332 t.transform.translation = new_local_translation;
333 t.transform.recompute_model();
334 let transform = t.transform;
335 emit.push_intent_now(
336 transform_cid,
337 IntentValue::UpdateTransform {
338 component_ids: vec![transform_cid],
339 translation: transform.translation,
340 rotation_quat_xyzw: transform.rotation,
341 scale: transform.scale,
342 },
343 );
344 }
345 }
346 }
347}
348
349fn resolve_shape(world: &World, collision_cid: ComponentId) -> Option<CollisionShape> {
350 for child in world.children_of(collision_cid) {
351 if let Some(s) = world.get_component_by_id_as::<CollisionShapeComponent>(*child) {
352 return Some(s.shape);
353 }
354 }
355
356 let parent = world.parent_of(collision_cid)?;
357 for sib in world.children_of(parent) {
358 if *sib == collision_cid {
359 continue;
360 }
361 let Some(r) = world.get_component_by_id_as::<RenderableComponent>(*sib) else {
362 continue;
363 };
364
365 if r.renderable.base_mesh == crate::engine::graphics::primitives::CpuMeshHandle::CUBE {
366 return Some(CollisionShape::CUBE());
367 }
368 if r.renderable.base_mesh == crate::engine::graphics::primitives::CpuMeshHandle::SPHERE {
369 return Some(CollisionShape::SPHERE());
370 }
371 }
372
373 None
374}
375
376fn compute_push_out_aabb(
377 a_center: [f32; 3],
378 a_shape: CollisionShape,
379 b_center: [f32; 3],
380 b_shape: CollisionShape,
381 eps: f32,
382) -> Option<[f32; 3]> {
383 let (a_min, a_max) = aabb_world(a_center, a_shape);
384 let (b_min, b_max) = aabb_world(b_center, b_shape);
385
386 let overlap_x = f32::min(a_max[0], b_max[0]) - f32::max(a_min[0], b_min[0]);
387 let overlap_y = f32::min(a_max[1], b_max[1]) - f32::max(a_min[1], b_min[1]);
388 let overlap_z = f32::min(a_max[2], b_max[2]) - f32::max(a_min[2], b_min[2]);
389
390 if overlap_x <= 0.0 || overlap_y <= 0.0 || overlap_z <= 0.0 {
391 return None;
392 }
393
394 let mut axis = 0;
395 let mut min_overlap = overlap_x;
396 if overlap_y < min_overlap {
397 min_overlap = overlap_y;
398 axis = 1;
399 }
400 if overlap_z < min_overlap {
401 min_overlap = overlap_z;
402 axis = 2;
403 }
404
405 let mut out = [0.0f32; 3];
406 let dir = if a_center[axis] < b_center[axis] {
407 -1.0
408 } else {
409 1.0
410 };
411 out[axis] = dir * (min_overlap + eps);
412 Some(out)
413}
414
415fn aabb_world(center: [f32; 3], shape: CollisionShape) -> ([f32; 3], [f32; 3]) {
416 match shape {
417 CollisionShape::Cube { half_extents } => (
418 [
419 center[0] - half_extents[0],
420 center[1] - half_extents[1],
421 center[2] - half_extents[2],
422 ],
423 [
424 center[0] + half_extents[0],
425 center[1] + half_extents[1],
426 center[2] + half_extents[2],
427 ],
428 ),
429 CollisionShape::Sphere { radius } => (
430 [center[0] - radius, center[1] - radius, center[2] - radius],
431 [center[0] + radius, center[1] + radius, center[2] + radius],
432 ),
433 }
434}
435
436fn mat4_mul_vec4(m: [[f32; 4]; 4], v: [f32; 4]) -> [f32; 4] {
437 [
438 m[0][0] * v[0] + m[1][0] * v[1] + m[2][0] * v[2] + m[3][0] * v[3],
439 m[0][1] * v[0] + m[1][1] * v[1] + m[2][1] * v[2] + m[3][1] * v[3],
440 m[0][2] * v[0] + m[1][2] * v[1] + m[2][2] * v[2] + m[3][2] * v[3],
441 m[0][3] * v[0] + m[1][3] * v[1] + m[2][3] * v[2] + m[3][3] * v[3],
442 ]
443}
444
445fn world_to_local_translation(
446 world: &World,
447 transform_cid: ComponentId,
448 desired_world: [f32; 3],
449) -> [f32; 3] {
450 let mut cur = transform_cid;
451 while let Some(parent) = world.parent_of(cur) {
452 if let Some(t) = world.get_component_by_id_as::<TransformComponent>(parent) {
453 if let Some(inv) = math::mat4_inverse(t.transform.matrix_world) {
454 let p_local = mat4_mul_vec4(
455 inv,
456 [desired_world[0], desired_world[1], desired_world[2], 1.0],
457 );
458 return [p_local[0], p_local[1], p_local[2]];
459 }
460 break;
461 }
462 cur = parent;
463 }
464
465 desired_world
466}