Skip to main content

gizmo_physics_rigid/
system.rs

1use gizmo_physics_core::{Collider, Transform};
2use crate::components::{RigidBody, Velocity};use crate::world::PhysicsWorld;
3use gizmo_core::entity::Entity;
4use gizmo_core::query::{Mut, Query};
5use gizmo_core::world::World;
6
7/// Exclusive system that updates the entire physics simulation.
8/// It reads all rigid and soft bodies from the ECS, steps the physics world,
9/// and writes the transformed positions and velocities back to the ECS.
10#[tracing::instrument(skip_all, name = "physics_step_system")]
11pub fn physics_step_system(world: &World, dt: f32) {
12    // Record profiler scope (if FrameProfiler resource is available)
13    if let Ok(mut profiler) = world.try_get_resource_mut::<gizmo_core::profiler::FrameProfiler>() {
14        profiler.begin_scope("physics_total");
15    }
16
17    // 1. Acquire PhysicsWorld Resource
18    let mut physics_world = match world.try_get_resource_mut::<PhysicsWorld>() {
19        Ok(res) => res,
20        Err(e) => {
21            tracing::info!("[Physics] FAILED TO GET PhysicsWorld Resource: {:?}", e);
22            return;
23        }
24    };
25
26    // 2. Gather Compound Shapes (Read Locks Only)
27    let mut compound_shapes_map = std::collections::HashMap::new();
28    {
29        if let Some(query) = world.query::<(
30            &Collider,
31            &Transform,
32            &RigidBody,
33            gizmo_core::query::Without<gizmo_core::pool::Pooled>,
34            gizmo_core::query::Without<gizmo_core::component::IsDeleted>,
35        )>() {
36            let mut children_query = world.query::<&gizmo_core::component::Children>();
37            let trans_query = world.query::<&Transform>();
38            let col_query = world.query::<&Collider>();
39
40            for (id, (col, transform, _rb, _, _)) in query.iter() {
41                let mut compound_shapes = Vec::new();
42                compound_shapes.push((
43                    gizmo_physics_core::Transform::default(),
44                    Box::new(col.shape.clone()),
45                ));
46
47                // Check children recursively
48                let mut stack = vec![id];
49                while let Some(curr_id) = stack.pop() {
50                    if let Some(children_query_ref) = &mut children_query {
51                        if let Some(children) = children_query_ref.get(curr_id) {
52                            for &child_id in &children.0 {
53                                stack.push(child_id);
54                                if let (Some(tq), Some(cq)) = (&trans_query, &col_query) {
55                                    if let (Some(child_trans), Some(child_col)) = (tq.get(child_id), cq.get(child_id)) {
56                                        let inv_rot = transform.rotation.inverse();
57                                        let local_pos =
58                                            inv_rot.mul_vec3(child_trans.position - transform.position);
59                                        let local_rot = inv_rot * child_trans.rotation;
60
61                                        let local_t = gizmo_physics_core::Transform::new(local_pos)
62                                            .with_rotation(local_rot);
63                                        compound_shapes
64                                            .push((local_t, Box::new(child_col.shape.clone())));
65                                    }
66                                }
67                            }
68                        }
69                    }
70                }
71
72                // Create a single Collider for this RigidBody
73                let final_collider = if compound_shapes.is_empty() {
74                    Collider::default() // Should technically not be simulated
75                } else if compound_shapes.len() == 1 {
76                    // Single collider, avoid nesting in Compound
77                    let (_t, s) = compound_shapes.remove(0);
78                    Collider {
79                        shape: *s,
80                        ..Default::default()
81                    }
82                } else {
83                    Collider {
84                        shape: gizmo_physics_core::ColliderShape::Compound(compound_shapes),
85                        ..Default::default()
86                    }
87                };
88
89                compound_shapes_map.insert(id, final_collider);
90            }
91        }
92    }
93
94    // 3. Query Rigid Bodies (Write Locks)
95    let mut rigid_bodies = Vec::new();
96    if let Some(mut query) = world.query::<(
97        Mut<RigidBody>,
98        Mut<Transform>,
99        Mut<Velocity>,
100        gizmo_core::query::Without<gizmo_core::pool::Pooled>,
101        gizmo_core::query::Without<gizmo_core::component::IsDeleted>,
102    )>() {
103        for (id, (rb, transform, vel, _, _)) in query.iter_mut() {
104            if let Some(final_collider) = compound_shapes_map.remove(&id) {
105                rigid_bodies.push((Entity::new(id, 0), *rb, *transform, *vel, final_collider));
106            }
107        }
108    } else {
109        tracing::info!("[Physics] FAILED TO BORROW RigidBody/Transform/Velocity Mutably!");
110    }
111
112    // 4. Step Simulation
113    physics_world.sync_bodies(rigid_bodies.iter());
114
115    physics_world.step(dt).expect("Gizmo Physics Engine encountered a critical numerical error (NaN, Infinity, or Overflow) and halted!");
116
117    // Sync back to rigid_bodies so vehicles/ECS writeback works
118    for i in 0..physics_world.entities.len() {
119        let entity_id = physics_world.entities[i].id();
120        if let Some((_, rb, trans, vel, _)) =
121            rigid_bodies.iter_mut().find(|(e, ..)| e.id() == entity_id)
122        {
123            *rb = physics_world.rigid_bodies[i];
124            *trans = physics_world.transforms[i];
125            *vel = physics_world.velocities[i];
126        }
127    }
128
129    // 5. Write back to ECS (Rigid Bodies)
130    if !rigid_bodies.is_empty() {
131        if let Some(query) = world.query::<(
132            Mut<RigidBody>,
133            Mut<Transform>,
134            Mut<Velocity>,
135            gizmo_core::query::Without<gizmo_core::pool::Pooled>,
136        )>() {
137            for (entity, rb, transform, vel, _collider) in rigid_bodies {
138                if let Some((mut ecs_rb, mut ecs_trans, mut ecs_vel, _)) = query.get(entity.id()) {
139                    *ecs_rb = rb;
140                    *ecs_trans = transform;
141                    *ecs_vel = vel;
142                }
143            }
144        }
145    }
146
147    
148
149    // 7. Dispatch Events
150    if let Ok(mut trigger_queue) =
151        world.try_get_resource_mut::<gizmo_core::event::Events<gizmo_physics_core::TriggerEvent>>()
152    {
153        for event in &physics_world.trigger_events {
154            trigger_queue.send(event.clone());
155        }
156    }
157
158    if let Ok(mut collision_queue) =
159        world.try_get_resource_mut::<gizmo_core::event::Events<gizmo_physics_core::CollisionEvent>>()
160    {
161        for event in &physics_world.collision_events {
162            collision_queue.send(event.clone());
163        }
164    }
165
166    if physics_world.step_once {
167        physics_world.step_once = false;
168    }
169
170    // Close profiler scope
171    drop(physics_world); // PhysicsWorld lock'unu bırak
172    if let Ok(mut profiler) = world.try_get_resource_mut::<gizmo_core::profiler::FrameProfiler>() {
173        profiler.end_scope("physics_total");
174    }
175}
176
177/// System that processes collision events and breaks objects that exceed their threshold.
178pub fn physics_fracture_system(world: &World, dt: f32) {
179    use crate::components::Breakable;
180    use gizmo_core::commands::Commands;
181    use gizmo_core::system::SystemParam;
182
183    let physics_world = match world.try_get_resource::<PhysicsWorld>() {
184        Ok(res) => res,
185        Err(_) => return,
186    };
187
188    let mut commands = match Commands::fetch(world, dt) {
189        Ok(c) => c,
190        Err(_) => return,
191    };
192
193    let mut shattered = std::collections::HashSet::new();
194
195    let query_opt = Query::<(
196        gizmo_core::query::Mut<Breakable>,
197        &Transform,
198        &Collider,
199        &Velocity,
200        gizmo_core::query::Without<gizmo_core::pool::Pooled>,
201    )>::new(world);
202    let query = match query_opt {
203        Some(q) => q,
204        None => return,
205    };
206
207    for event in &physics_world.collision_events {
208        let mut max_impulse = 0.0;
209        let mut impact_normal = gizmo_math::Vec3::ZERO;
210        let mut impact_point = gizmo_math::Vec3::ZERO;
211
212        for contact in &event.contact_points {
213            if contact.normal_impulse > max_impulse {
214                max_impulse = contact.normal_impulse;
215                impact_normal = contact.normal;
216                impact_point = contact.point;
217            }
218        }
219
220        // Fallback: estimate impact from relative velocity when solver impulse is unavailable
221        if max_impulse <= 0.0 && !event.contact_points.is_empty() {
222            // Look up velocities of both entities to estimate impact force
223            let vel_a = physics_world
224                .entity_index_map
225                .get(&event.entity_a.id())
226                .map(|&idx| physics_world.velocities[idx].linear)
227                .unwrap_or(gizmo_math::Vec3::ZERO);
228            let vel_b = physics_world
229                .entity_index_map
230                .get(&event.entity_b.id())
231                .map(|&idx| physics_world.velocities[idx].linear)
232                .unwrap_or(gizmo_math::Vec3::ZERO);
233            let mass_a = physics_world
234                .entity_index_map
235                .get(&event.entity_a.id())
236                .map(|&idx| physics_world.rigid_bodies[idx].mass)
237                .unwrap_or(1.0);
238            let mass_b = physics_world
239                .entity_index_map
240                .get(&event.entity_b.id())
241                .map(|&idx| physics_world.rigid_bodies[idx].mass)
242                .unwrap_or(1.0);
243
244            let rel_speed = (vel_b - vel_a).length();
245            let reduced_mass = if mass_a > 0.0 && mass_b > 0.0 {
246                (mass_a * mass_b) / (mass_a + mass_b)
247            } else {
248                mass_a.max(mass_b)
249            };
250            max_impulse = rel_speed * reduced_mass;
251            if let Some(contact) = event.contact_points.first() {
252                impact_normal = contact.normal;
253                impact_point = contact.point;
254            }
255        }
256
257        if max_impulse <= 0.0 {
258            continue;
259        }
260
261        // Check Entity A
262        if !shattered.contains(&event.entity_a.id()) {
263            if let Some((mut breakable, transform, collider, vel, _)) =
264                query.get(event.entity_a.id())
265            {
266                if !breakable.is_broken && max_impulse > breakable.threshold {
267                    breakable.current_health -= max_impulse;
268                    if breakable.current_health <= 0.0 {
269                        breakable.is_broken = true;
270                        shattered.insert(event.entity_a.id());
271                        shatter_entity(
272                            &mut commands,
273                            event.entity_a,
274                            &breakable,
275                            transform,
276                            collider,
277                            vel,
278                            -impact_normal,
279                            impact_point,
280                        );
281                    }
282                }
283            }
284        }
285
286        // Check Entity B
287        if !shattered.contains(&event.entity_b.id()) {
288            if let Some((mut breakable, transform, collider, vel, _)) =
289                query.get(event.entity_b.id())
290            {
291                if !breakable.is_broken && max_impulse > breakable.threshold {
292                    breakable.current_health -= max_impulse;
293                    if breakable.current_health <= 0.0 {
294                        breakable.is_broken = true;
295                        shattered.insert(event.entity_b.id());
296                        shatter_entity(
297                            &mut commands,
298                            event.entity_b,
299                            &breakable,
300                            transform,
301                            collider,
302                            vel,
303                            impact_normal,
304                            impact_point,
305                        );
306                    }
307                }
308            }
309        }
310    }
311    drop(query);
312}
313
314fn shatter_entity(
315    commands: &mut gizmo_core::commands::Commands,
316    entity: Entity,
317    breakable: &crate::components::Breakable,
318    transform: &Transform,
319    collider: &Collider,
320    vel: &Velocity,
321    impact_direction: gizmo_math::Vec3,
322    _impact_point: gizmo_math::Vec3,
323) {
324    use crate::fracture::voronoi_shatter;
325
326    // We only support shattering boxes for now
327    let extents = match &collider.shape {
328        gizmo_physics_core::ColliderShape::Box(b) => b.half_extents,
329        _ => return, // Cannot shatter non-boxes easily with our voronoi yet
330    };
331
332    // Despawn the original entity
333    commands.entity(entity).despawn();
334
335    // Generate chunks
336    let chunks = voronoi_shatter(extents, breakable.max_pieces, 42);
337
338    for chunk in chunks {
339        // Create new convex hull colliders or approximated boxes for the chunks.
340        // For simplicity, we approximate each chunk with a small sphere or box based on its volume.
341        // A full implementation would use ConvexHull shapes.
342        let radius = (chunk.volume * 0.1).powf(1.0 / 3.0).max(0.1);
343
344        // Offset chunk center by parent's transform
345        let world_offset = transform.rotation * chunk.center_of_mass;
346        let mut new_transform = *transform;
347        new_transform.position += world_offset;
348
349        // Give chunks a slight explosive velocity outwards from the center of mass
350        let mut new_vel = *vel;
351        let outward = chunk.center_of_mass.normalize_or_zero();
352        new_vel.linear += outward * 2.0 + impact_direction * 5.0; // Explosion effect
353
354        let chunk_collider = Collider::sphere(radius).with_material(collider.material);
355        let mut rb = RigidBody::new(chunk.volume * collider.material.density, 0.0, 0.0, true);
356        rb.update_inertia_from_collider(&chunk_collider);
357
358        commands
359            .spawn()
360            .insert(rb)
361            .insert(chunk_collider)
362            .insert(new_transform)
363            .insert(new_vel);
364    }
365}
366
367/// System that checks for Explosion components and applies outward forces
368/// to all rigid bodies and soft body nodes within the radius.
369pub fn physics_explosion_system(world: &World, dt: f32) {
370    use crate::components::{Explosion, ExplosionFalloff};
371    use gizmo_core::commands::Commands;
372    use gizmo_core::system::SystemParam;
373
374    let mut commands = match Commands::fetch(world, dt) {
375        Ok(c) => c,
376        Err(_) => return,
377    };
378
379    let explosion_query_opt = Query::<(
380        &Explosion,
381        &Transform,
382        gizmo_core::query::Without<gizmo_core::pool::Pooled>,
383    )>::new(world);
384    let mut active_explosions = Vec::new();
385
386    if let Some(exp_query) = &explosion_query_opt {
387        for (ent_id, (explosion, transform, _)) in exp_query.iter() {
388            if explosion.is_active {
389                // Apply offset to transform position
390                active_explosions.push((
391                    Entity::new(ent_id, 0),
392                    *explosion,
393                    transform.position + explosion.offset,
394                ));
395            }
396        }
397    }
398
399    if active_explosions.is_empty() {
400        return; // Nothing to explode
401    }
402
403    let mut shattered = std::collections::HashSet::new();
404
405    // Helper closure to calculate falloff intensity
406    let calculate_intensity = |dist: f32, radius: f32, falloff: ExplosionFalloff| -> f32 {
407        if dist >= radius {
408            return 0.0;
409        }
410        match falloff {
411            ExplosionFalloff::None => 1.0,
412            ExplosionFalloff::Linear => 1.0 - (dist / radius),
413            ExplosionFalloff::Quadratic => {
414                let ratio = 1.0 - (dist / radius);
415                ratio * ratio
416            }
417        }
418    };
419
420    // Check for Breakables that should shatter
421    let breakable_query_opt = Query::<(
422        gizmo_core::query::Mut<crate::components::Breakable>,
423        &Transform,
424        &Collider,
425        &Velocity,
426        gizmo_core::query::Without<gizmo_core::pool::Pooled>,
427    )>::new(world);
428    if let Some(breakable_query) = &breakable_query_opt {
429        for (_exp_entity, explosion, exp_pos) in &active_explosions {
430            for (id, (mut breakable, transform, collider, vel, _)) in breakable_query.iter() {
431                if breakable.is_broken || shattered.contains(&id) {
432                    continue;
433                }
434
435                let diff = transform.position - *exp_pos;
436                let dist_sq = diff.length_squared();
437
438                if dist_sq < explosion.force_radius * explosion.force_radius && dist_sq > 0.001 {
439                    let dist = dist_sq.sqrt();
440                    let intensity =
441                        calculate_intensity(dist, explosion.force_radius, explosion.falloff);
442                    let impulse_mag = explosion.force * intensity;
443
444                    if impulse_mag > breakable.threshold {
445                        breakable.current_health -= explosion.damage * intensity;
446                        if breakable.current_health <= 0.0 {
447                            breakable.is_broken = true;
448                            shattered.insert(id);
449                            let dir = diff / dist;
450                            let mut exp_vel = *vel;
451                            exp_vel.linear += dir * impulse_mag * 0.1; // Estimate mass
452                            shatter_entity(
453                                &mut commands,
454                                Entity::new(id, 0),
455                                &breakable,
456                                transform,
457                                collider,
458                                &exp_vel,
459                                dir,
460                                transform.position,
461                            );
462                        }
463                    }
464                }
465            }
466        }
467    }
468
469    // Apply to Rigid Bodies
470    let rb_query_opt = Query::<(
471        Mut<RigidBody>,
472        &Transform,
473        Mut<Velocity>,
474        gizmo_core::query::Without<gizmo_core::pool::Pooled>,
475    )>::new(world);
476    if let Some(rb_query) = &rb_query_opt {
477        for (_exp_entity, explosion, exp_pos) in &active_explosions {
478            for (id, (rb, transform, mut vel, _)) in rb_query.iter() {
479                if !rb.is_dynamic() || shattered.contains(&id) {
480                    continue;
481                }
482
483                let diff = transform.position - *exp_pos;
484                let dist_sq = diff.length_squared();
485
486                if dist_sq < explosion.force_radius * explosion.force_radius && dist_sq > 0.001 {
487                    let dist = dist_sq.sqrt();
488                    let dir = diff / dist;
489
490                    let intensity =
491                        calculate_intensity(dist, explosion.force_radius, explosion.falloff);
492                    let impulse_mag = explosion.force * intensity;
493
494                    // Apply instantaneous velocity change
495                    vel.linear += dir * impulse_mag * rb.inv_mass();
496                }
497            }
498        }
499    }
500
501    // Despawn the explosions so they don't trigger again
502    // Note: If game logic needs to read explosion damage, it must run BEFORE the physics_explosion_system in the schedule!
503    for (exp_entity, _, _) in active_explosions {
504        commands.entity(exp_entity).despawn();
505    }
506}
507
508