Skip to main content

arcane_engine/physics/
world.rs

1use std::collections::{HashMap, HashSet};
2
3use super::broadphase::SpatialHash;
4use super::constraints::solve_constraints;
5use super::integrate::integrate;
6use super::narrowphase::test_collision;
7use super::resolve::{initialize_contacts, resolve_contacts_position, resolve_contacts_velocity_iteration, warm_start_contacts};
8use super::sleep::update_sleep;
9use super::types::*;
10
11pub struct PhysicsWorld {
12    bodies: Vec<Option<RigidBody>>,
13    free_ids: Vec<BodyId>,
14    next_id: BodyId,
15    constraints: Vec<Constraint>,
16    next_constraint_id: ConstraintId,
17    gravity: (f32, f32),
18    fixed_dt: f32,
19    accumulator: f32,
20    contacts: Vec<Contact>,
21    /// Contacts accumulated across all sub-steps within a frame.
22    /// Game code reads this via get_contacts() to see every collision that
23    /// occurred during the frame, not just the last sub-step's contacts.
24    /// De-duplicated by body pair (first contact per pair wins).
25    frame_contacts: Vec<Contact>,
26    /// Tracks which body pairs already have a contact in frame_contacts.
27    frame_contact_pairs: HashSet<(BodyId, BodyId)>,
28    broadphase: SpatialHash,
29    solver_iterations: usize,
30    /// Warm-start cache: maps (body_a, body_b) → (normal_impulse, friction_impulse)
31    warm_cache: HashMap<(BodyId, BodyId), (f32, f32)>,
32}
33
34impl PhysicsWorld {
35    pub fn new(gravity_x: f32, gravity_y: f32) -> Self {
36        Self {
37            bodies: Vec::new(),
38            free_ids: Vec::new(),
39            next_id: 0,
40            constraints: Vec::new(),
41            next_constraint_id: 0,
42            gravity: (gravity_x, gravity_y),
43            fixed_dt: 1.0 / 60.0,
44            accumulator: 0.0,
45            contacts: Vec::new(),
46            frame_contacts: Vec::new(),
47            frame_contact_pairs: HashSet::new(),
48            broadphase: SpatialHash::new(64.0),
49            solver_iterations: 6,
50            warm_cache: HashMap::new(),
51        }
52    }
53
54    /// Fixed-timestep physics step. Accumulates dt and runs sub-steps as needed.
55    /// Uses 4 sub-steps per fixed step (Box2D v3 approach) for improved stack
56    /// stability: sub-stepping is more effective than extra solver iterations.
57    ///
58    /// Contacts are accumulated across all sub-steps (de-duplicated by body pair)
59    /// so that game code can see every collision via get_contacts().
60    pub fn step(&mut self, dt: f32) {
61        self.accumulator += dt;
62
63        // Clear frame-level contact accumulator at the start of each step call
64        self.frame_contacts.clear();
65        self.frame_contact_pairs.clear();
66
67        while self.accumulator >= self.fixed_dt {
68            let sub_dt = self.fixed_dt / 4.0;
69            for _ in 0..4 {
70                self.sub_step(sub_dt);
71
72                // Accumulate contacts from this sub-step (de-duplicate by pair)
73                for contact in &self.contacts {
74                    let key = (contact.body_a.min(contact.body_b), contact.body_a.max(contact.body_b));
75                    if self.frame_contact_pairs.insert(key) {
76                        self.frame_contacts.push(contact.clone());
77                    }
78                }
79            }
80            self.accumulator -= self.fixed_dt;
81        }
82    }
83
84    fn sub_step(&mut self, dt: f32) {
85        // 1. Integrate
86        for body in self.bodies.iter_mut().flatten() {
87            integrate(body, self.gravity.0, self.gravity.1, dt);
88        }
89
90        // 2. Broadphase
91        self.broadphase.clear();
92        for body in self.bodies.iter().flatten() {
93            let (min_x, min_y, max_x, max_y) = get_shape_aabb(body);
94            self.broadphase.insert(body.id, min_x, min_y, max_x, max_y);
95        }
96        let pairs = self.broadphase.get_pairs();
97
98        // 3. Narrowphase
99        self.contacts.clear();
100        for (id_a, id_b) in pairs {
101            let a_idx = id_a as usize;
102            let b_idx = id_b as usize;
103
104            // Layer/mask filtering
105            let (layer_a, mask_a, sleeping_a) = match &self.bodies[a_idx] {
106                Some(b) => (b.layer, b.mask, b.sleeping),
107                None => continue,
108            };
109            let (layer_b, mask_b, sleeping_b) = match &self.bodies[b_idx] {
110                Some(b) => (b.layer, b.mask, b.sleeping),
111                None => continue,
112            };
113
114            // Both layers must pass each other's masks
115            if (layer_a & mask_b) == 0 || (layer_b & mask_a) == 0 {
116                continue;
117            }
118
119            // Skip if both sleeping
120            if sleeping_a && sleeping_b {
121                continue;
122            }
123
124            // We need to borrow both bodies immutably. Use index-based access.
125            let body_a = self.bodies[a_idx].as_ref().unwrap();
126            let body_b = self.bodies[b_idx].as_ref().unwrap();
127
128            if let Some(contact) = test_collision(body_a, body_b) {
129                self.contacts.push(contact);
130            }
131        }
132
133        // 3b. Sort contacts bottom-up for stack stability.
134        self.contacts.sort_by(|a, b| {
135            let ay = self.bodies[a.body_a as usize]
136                .as_ref()
137                .map_or(0.0f32, |body| body.y)
138                .max(
139                    self.bodies[a.body_b as usize]
140                        .as_ref()
141                        .map_or(0.0f32, |body| body.y),
142                );
143            let by = self.bodies[b.body_a as usize]
144                .as_ref()
145                .map_or(0.0f32, |body| body.y)
146                .max(
147                    self.bodies[b.body_b as usize]
148                        .as_ref()
149                        .map_or(0.0f32, |body| body.y),
150                );
151            by.partial_cmp(&ay).unwrap_or(std::cmp::Ordering::Equal)
152        });
153
154        // 3c. Pre-compute velocity bias (restitution) and tangent direction for each contact.
155        // Must happen BEFORE warm start so bias reflects post-integration velocities.
156        let gravity_mag = (self.gravity.0 * self.gravity.0 + self.gravity.1 * self.gravity.1).sqrt();
157        let restitution_threshold = gravity_mag * dt * 1.5;
158        initialize_contacts(&self.bodies, &mut self.contacts, restitution_threshold);
159
160        // 3d. Warm start: initialize accumulated impulses from previous frame's cache
161        for contact in &mut self.contacts {
162            let key = (contact.body_a.min(contact.body_b), contact.body_a.max(contact.body_b));
163            if let Some(&(jn, jt)) = self.warm_cache.get(&key) {
164                // Scale slightly to avoid overshoot when contacts shift between frames.
165                // Island-based sleeping prevents the cascade that made 0.95 problematic
166                // with per-body sleeping (no body sleeps until the whole stack settles).
167                contact.accumulated_jn = jn * 0.95;
168                contact.accumulated_jt = jt * 0.95;
169            }
170        }
171        warm_start_contacts(&mut self.bodies, &self.contacts);
172
173        // 4. Velocity solve (contacts + constraints)
174        for i in 0..self.solver_iterations {
175            let reverse = i % 2 == 1;
176            resolve_contacts_velocity_iteration(&mut self.bodies, &mut self.contacts, reverse);
177            solve_constraints(&mut self.bodies, &self.constraints, dt);
178        }
179
180        // 4b. Save accumulated impulses to warm cache for next frame
181        self.warm_cache.clear();
182        for contact in &self.contacts {
183            let key = (contact.body_a.min(contact.body_b), contact.body_a.max(contact.body_b));
184            self.warm_cache.insert(key, (contact.accumulated_jn, contact.accumulated_jt));
185        }
186
187        // 5. Position correction (3 iterations per sub-step, matching Box2D v2).
188        // With 4 sub-steps per frame, this gives 12 effective position corrections per
189        // frame. Combined with a Baumgarte factor of 0.2 (Box2D standard) and max
190        // correction clamping, this converges stably for large body stacks.
191        for i in 0..3 {
192            for contact in &mut self.contacts {
193                let a = &self.bodies[contact.body_a as usize];
194                let b = &self.bodies[contact.body_b as usize];
195                if let (Some(a), Some(b)) = (a, b) {
196                    if let Some(fresh) = test_collision(a, b) {
197                        contact.penetration = fresh.penetration;
198                        contact.normal = fresh.normal;
199                        contact.contact_point = fresh.contact_point;
200                    } else {
201                        contact.penetration = 0.0;
202                    }
203                }
204            }
205            resolve_contacts_position(&mut self.bodies, &self.contacts, i % 2 == 1);
206        }
207
208        // 6. Post-correction velocity clamping: after position correction, if bodies
209        // are still penetrating, zero out any velocity component driving them deeper.
210        // This prevents re-introduction of penetration on the next integration step.
211        //
212        // Contact normal points from body_a toward body_b.
213        // Body A "into contact" = toward B = +normal direction.
214        // Body B "into contact" = toward A = -normal direction.
215        for contact in &self.contacts {
216            let a_idx = contact.body_a as usize;
217            let b_idx = contact.body_b as usize;
218
219            // Re-test to get fresh penetration
220            let still_penetrating = match (&self.bodies[a_idx], &self.bodies[b_idx]) {
221                (Some(a), Some(b)) => test_collision(a, b).map_or(false, |c| c.penetration > 0.01),
222                _ => false,
223            };
224
225            if !still_penetrating {
226                continue;
227            }
228
229            let (nx, ny) = contact.normal;
230
231            // Clamp velocity of body A: remove component toward B (along +normal)
232            if let Some(a) = &mut self.bodies[a_idx] {
233                if a.body_type == BodyType::Dynamic {
234                    let vn = a.vx * nx + a.vy * ny;
235                    if vn > 0.0 {
236                        a.vx -= vn * nx;
237                        a.vy -= vn * ny;
238                    }
239                }
240            }
241            // Clamp velocity of body B: remove component toward A (along -normal)
242            if let Some(b) = &mut self.bodies[b_idx] {
243                if b.body_type == BodyType::Dynamic {
244                    let vn = b.vx * nx + b.vy * ny;
245                    if vn < 0.0 {
246                        b.vx -= vn * nx;
247                        b.vy -= vn * ny;
248                    }
249                }
250            }
251        }
252
253        // 7. Update sleep
254        update_sleep(&mut self.bodies, &self.contacts, dt);
255    }
256
257    pub fn add_body(
258        &mut self,
259        body_type: BodyType,
260        shape: Shape,
261        x: f32,
262        y: f32,
263        mass: f32,
264        material: Material,
265        layer: u16,
266        mask: u16,
267    ) -> BodyId {
268        let id = if let Some(recycled) = self.free_ids.pop() {
269            recycled
270        } else {
271            let id = self.next_id;
272            self.next_id += 1;
273            id
274        };
275
276        let (inv_mass, inertia, inv_inertia) = compute_mass_and_inertia(&shape, mass, body_type);
277
278        let body = RigidBody {
279            id,
280            body_type,
281            shape,
282            material,
283            x,
284            y,
285            angle: 0.0,
286            vx: 0.0,
287            vy: 0.0,
288            angular_velocity: 0.0,
289            fx: 0.0,
290            fy: 0.0,
291            torque: 0.0,
292            mass,
293            inv_mass,
294            inertia,
295            inv_inertia,
296            layer,
297            mask,
298            sleeping: false,
299            sleep_timer: 0.0,
300        };
301
302        let idx = id as usize;
303        if idx >= self.bodies.len() {
304            self.bodies.resize_with(idx + 1, || None);
305        }
306        self.bodies[idx] = Some(body);
307        id
308    }
309
310    pub fn remove_body(&mut self, id: BodyId) {
311        let idx = id as usize;
312        if idx < self.bodies.len() {
313            self.bodies[idx] = None;
314            self.free_ids.push(id);
315        }
316    }
317
318    pub fn get_body(&self, id: BodyId) -> Option<&RigidBody> {
319        self.bodies.get(id as usize)?.as_ref()
320    }
321
322    pub fn get_body_mut(&mut self, id: BodyId) -> Option<&mut RigidBody> {
323        self.bodies.get_mut(id as usize)?.as_mut()
324    }
325
326    pub fn set_velocity(&mut self, id: BodyId, vx: f32, vy: f32) {
327        if let Some(body) = self.get_body_mut(id) {
328            body.vx = vx;
329            body.vy = vy;
330            body.sleeping = false;
331            body.sleep_timer = 0.0;
332        }
333    }
334
335    pub fn set_angular_velocity(&mut self, id: BodyId, av: f32) {
336        if let Some(body) = self.get_body_mut(id) {
337            body.angular_velocity = av;
338            body.sleeping = false;
339            body.sleep_timer = 0.0;
340        }
341    }
342
343    pub fn apply_force(&mut self, id: BodyId, fx: f32, fy: f32) {
344        if let Some(body) = self.get_body_mut(id) {
345            body.fx += fx;
346            body.fy += fy;
347            body.sleeping = false;
348            body.sleep_timer = 0.0;
349        }
350    }
351
352    pub fn apply_impulse(&mut self, id: BodyId, ix: f32, iy: f32) {
353        if let Some(body) = self.get_body_mut(id) {
354            body.vx += ix * body.inv_mass;
355            body.vy += iy * body.inv_mass;
356            body.sleeping = false;
357            body.sleep_timer = 0.0;
358        }
359    }
360
361    pub fn set_position(&mut self, id: BodyId, x: f32, y: f32) {
362        if let Some(body) = self.get_body_mut(id) {
363            body.x = x;
364            body.y = y;
365            body.sleeping = false;
366            body.sleep_timer = 0.0;
367        }
368    }
369
370    pub fn set_collision_layers(&mut self, id: BodyId, layer: u16, mask: u16) {
371        if let Some(body) = self.get_body_mut(id) {
372            body.layer = layer;
373            body.mask = mask;
374        }
375    }
376
377    pub fn add_constraint(&mut self, constraint: Constraint) -> ConstraintId {
378        let id = self.next_constraint_id;
379        self.next_constraint_id += 1;
380
381        let constraint = match constraint {
382            Constraint::Distance {
383                body_a,
384                body_b,
385                distance,
386                anchor_a,
387                anchor_b,
388                ..
389            } => Constraint::Distance {
390                id,
391                body_a,
392                body_b,
393                distance,
394                anchor_a,
395                anchor_b,
396            },
397            Constraint::Revolute {
398                body_a,
399                body_b,
400                pivot,
401                ..
402            } => Constraint::Revolute {
403                id,
404                body_a,
405                body_b,
406                pivot,
407            },
408        };
409        self.constraints.push(constraint);
410        id
411    }
412
413    pub fn remove_constraint(&mut self, id: ConstraintId) {
414        self.constraints.retain(|c| c.id() != id);
415    }
416
417    pub fn query_aabb(&self, min_x: f32, min_y: f32, max_x: f32, max_y: f32) -> Vec<BodyId> {
418        let mut result = Vec::new();
419        for body in self.bodies.iter().flatten() {
420            let (bmin_x, bmin_y, bmax_x, bmax_y) = get_shape_aabb(body);
421            if bmax_x >= min_x && bmin_x <= max_x && bmax_y >= min_y && bmin_y <= max_y {
422                result.push(body.id);
423            }
424        }
425        result
426    }
427
428    pub fn raycast(
429        &self,
430        ox: f32,
431        oy: f32,
432        dx: f32,
433        dy: f32,
434        max_dist: f32,
435    ) -> Option<(BodyId, f32, f32, f32)> {
436        let dir_len = (dx * dx + dy * dy).sqrt();
437        if dir_len < 1e-8 {
438            return None;
439        }
440        let ndx = dx / dir_len;
441        let ndy = dy / dir_len;
442
443        let mut closest: Option<(BodyId, f32, f32, f32)> = None;
444
445        for body in self.bodies.iter().flatten() {
446            let t = match &body.shape {
447                Shape::Circle { radius } => {
448                    ray_vs_circle(ox, oy, ndx, ndy, body.x, body.y, *radius)
449                }
450                Shape::AABB { half_w, half_h } => {
451                    ray_vs_aabb(ox, oy, ndx, ndy, body.x, body.y, *half_w, *half_h)
452                }
453                Shape::Polygon { vertices } => {
454                    ray_vs_polygon(ox, oy, ndx, ndy, body, vertices)
455                }
456            };
457
458            if let Some(t) = t {
459                if t >= 0.0 && t <= max_dist {
460                    let hit_x = ox + ndx * t;
461                    let hit_y = oy + ndy * t;
462                    if closest.is_none() || t < closest.unwrap().3 {
463                        closest = Some((body.id, hit_x, hit_y, t));
464                    }
465                }
466            }
467        }
468        closest
469    }
470
471    /// Return all contacts that occurred during the last step() call.
472    /// Accumulated across all sub-steps, de-duplicated by body pair.
473    /// This ensures game code sees every collision, even if the bodies
474    /// separated during a later sub-step.
475    pub fn get_contacts(&self) -> &[Contact] {
476        &self.frame_contacts
477    }
478
479    /// Return all active (non-None) bodies.
480    pub fn all_bodies(&self) -> Vec<&RigidBody> {
481        self.bodies.iter().filter_map(|b| b.as_ref()).collect()
482    }
483
484    /// Return the world gravity.
485    pub fn gravity(&self) -> (f32, f32) {
486        self.gravity
487    }
488
489    /// Return the number of active bodies.
490    pub fn body_count(&self) -> usize {
491        self.bodies.iter().filter(|b| b.is_some()).count()
492    }
493}
494
495fn ray_vs_circle(
496    ox: f32, oy: f32,
497    dx: f32, dy: f32,
498    cx: f32, cy: f32,
499    radius: f32,
500) -> Option<f32> {
501    let fx = ox - cx;
502    let fy = oy - cy;
503    let a = dx * dx + dy * dy;
504    let b = 2.0 * (fx * dx + fy * dy);
505    let c = fx * fx + fy * fy - radius * radius;
506    let discriminant = b * b - 4.0 * a * c;
507    if discriminant < 0.0 {
508        return None;
509    }
510    let sqrt_d = discriminant.sqrt();
511    let t1 = (-b - sqrt_d) / (2.0 * a);
512    let t2 = (-b + sqrt_d) / (2.0 * a);
513    if t1 >= 0.0 {
514        Some(t1)
515    } else if t2 >= 0.0 {
516        Some(t2)
517    } else {
518        None
519    }
520}
521
522fn ray_vs_aabb(
523    ox: f32, oy: f32,
524    dx: f32, dy: f32,
525    cx: f32, cy: f32,
526    hw: f32, hh: f32,
527) -> Option<f32> {
528    let min_x = cx - hw;
529    let max_x = cx + hw;
530    let min_y = cy - hh;
531    let max_y = cy + hh;
532
533    let (mut tmin, mut tmax) = if dx.abs() < 1e-8 {
534        if ox < min_x || ox > max_x {
535            return None;
536        }
537        (f32::MIN, f32::MAX)
538    } else {
539        let inv_dx = 1.0 / dx;
540        let t1 = (min_x - ox) * inv_dx;
541        let t2 = (max_x - ox) * inv_dx;
542        (t1.min(t2), t1.max(t2))
543    };
544
545    let (tymin, tymax) = if dy.abs() < 1e-8 {
546        if oy < min_y || oy > max_y {
547            return None;
548        }
549        (f32::MIN, f32::MAX)
550    } else {
551        let inv_dy = 1.0 / dy;
552        let t1 = (min_y - oy) * inv_dy;
553        let t2 = (max_y - oy) * inv_dy;
554        (t1.min(t2), t1.max(t2))
555    };
556
557    tmin = tmin.max(tymin);
558    tmax = tmax.min(tymax);
559
560    if tmin > tmax || tmax < 0.0 {
561        return None;
562    }
563
564    Some(if tmin >= 0.0 { tmin } else { tmax })
565}
566
567fn ray_vs_polygon(
568    ox: f32, oy: f32,
569    dx: f32, dy: f32,
570    body: &RigidBody,
571    vertices: &[(f32, f32)],
572) -> Option<f32> {
573    let cos = body.angle.cos();
574    let sin = body.angle.sin();
575    let n = vertices.len();
576    if n < 3 {
577        return None;
578    }
579
580    let mut closest_t: Option<f32> = None;
581
582    for i in 0..n {
583        let (vx0, vy0) = vertices[i];
584        let (vx1, vy1) = vertices[(i + 1) % n];
585
586        // Transform to world space
587        let ax = vx0 * cos - vy0 * sin + body.x;
588        let ay = vx0 * sin + vy0 * cos + body.y;
589        let bx = vx1 * cos - vy1 * sin + body.x;
590        let by = vx1 * sin + vy1 * cos + body.y;
591
592        if let Some(t) = ray_vs_segment(ox, oy, dx, dy, ax, ay, bx, by) {
593            if closest_t.is_none() || t < closest_t.unwrap() {
594                closest_t = Some(t);
595            }
596        }
597    }
598    closest_t
599}
600
601fn ray_vs_segment(
602    ox: f32, oy: f32,
603    dx: f32, dy: f32,
604    ax: f32, ay: f32,
605    bx: f32, by: f32,
606) -> Option<f32> {
607    let ex = bx - ax;
608    let ey = by - ay;
609    let denom = dx * ey - dy * ex;
610    if denom.abs() < 1e-8 {
611        return None;
612    }
613    let t = ((ax - ox) * ey - (ay - oy) * ex) / denom;
614    let u = ((ax - ox) * dy - (ay - oy) * dx) / denom;
615    if t >= 0.0 && u >= 0.0 && u <= 1.0 {
616        Some(t)
617    } else {
618        None
619    }
620}