Skip to main content

goud_engine/ecs/collision/
response.rs

1//! Collision response configuration and resolution functions.
2
3use crate::core::math::Vec2;
4use crate::ecs::collision::contact::Contact;
5
6/// Collision response configuration for impulse resolution.
7///
8/// These settings control how collisions are resolved using impulse-based physics.
9/// Different collision types (bounce, slide, etc.) require different configurations.
10///
11/// # Examples
12///
13/// ```
14/// use goud_engine::ecs::collision::CollisionResponse;
15///
16/// // Bouncy ball (high restitution)
17/// let bouncy = CollisionResponse::bouncy();
18///
19/// // Character controller (no bounce, high friction)
20/// let character = CollisionResponse::character();
21///
22/// // Realistic physics (default)
23/// let realistic = CollisionResponse::default();
24/// ```
25#[derive(Debug, Clone, Copy, PartialEq)]
26pub struct CollisionResponse {
27    /// How much velocity is retained after collision (0.0 = no bounce, 1.0 = perfect bounce).
28    ///
29    /// Typical values:
30    /// - 0.0-0.1: Non-bouncy (characters, boxes)
31    /// - 0.3-0.5: Moderately bouncy (rubber balls)
32    /// - 0.8-1.0: Very bouncy (super balls)
33    pub restitution: f32,
34
35    /// Resistance to sliding (0.0 = ice, 1.0 = rubber on concrete).
36    ///
37    /// Typical values:
38    /// - 0.0-0.1: Ice, very slippery
39    /// - 0.2-0.4: Wood on wood, metal on metal
40    /// - 0.6-0.8: Rubber on concrete
41    pub friction: f32,
42
43    /// Percentage of overlap to resolve (0.0-1.0).
44    ///
45    /// Helps prevent objects from sinking into each other due to numerical errors.
46    /// - 0.0: No positional correction
47    /// - 0.2-0.8: Recommended range (0.4 is common)
48    /// - 1.0: Full correction (may cause jitter)
49    pub position_correction: f32,
50
51    /// Threshold below which positional correction is not applied.
52    ///
53    /// Prevents over-correction for small penetrations.
54    /// Typical value: 0.01 units
55    pub slop: f32,
56}
57
58impl CollisionResponse {
59    /// Creates a new collision response configuration.
60    ///
61    /// # Arguments
62    ///
63    /// * `restitution` - Bounciness (0.0 = no bounce, 1.0 = perfect bounce)
64    /// * `friction` - Sliding resistance (0.0 = ice, 1.0 = rubber)
65    /// * `position_correction` - Overlap correction percentage (0.0-1.0)
66    /// * `slop` - Minimum penetration for correction
67    ///
68    /// # Example
69    ///
70    /// ```
71    /// use goud_engine::ecs::collision::CollisionResponse;
72    ///
73    /// let response = CollisionResponse::new(0.5, 0.4, 0.4, 0.01);
74    /// ```
75    pub fn new(restitution: f32, friction: f32, position_correction: f32, slop: f32) -> Self {
76        Self {
77            restitution: restitution.clamp(0.0, 1.0),
78            friction: friction.clamp(0.0, 1.0),
79            position_correction: position_correction.clamp(0.0, 1.0),
80            slop: slop.max(0.0),
81        }
82    }
83
84    /// Creates a bouncy collision response (high restitution, low friction).
85    ///
86    /// Good for: balls, projectiles, bouncing objects
87    ///
88    /// # Example
89    ///
90    /// ```
91    /// use goud_engine::ecs::collision::CollisionResponse;
92    ///
93    /// let bouncy = CollisionResponse::bouncy();
94    /// assert_eq!(bouncy.restitution, 0.8);
95    /// ```
96    pub fn bouncy() -> Self {
97        Self::new(0.8, 0.2, 0.4, 0.01)
98    }
99
100    /// Creates a character controller response (no bounce, high friction).
101    ///
102    /// Good for: player characters, NPCs, walking entities
103    ///
104    /// # Example
105    ///
106    /// ```
107    /// use goud_engine::ecs::collision::CollisionResponse;
108    ///
109    /// let character = CollisionResponse::character();
110    /// assert_eq!(character.restitution, 0.0);
111    /// assert_eq!(character.friction, 0.8);
112    /// ```
113    pub fn character() -> Self {
114        Self::new(0.0, 0.8, 0.6, 0.01)
115    }
116
117    /// Creates a slippery collision response (low friction, some bounce).
118    ///
119    /// Good for: ice, smooth surfaces, low-friction materials
120    ///
121    /// # Example
122    ///
123    /// ```
124    /// use goud_engine::ecs::collision::CollisionResponse;
125    ///
126    /// let slippery = CollisionResponse::slippery();
127    /// assert!(slippery.friction < 0.2);
128    /// ```
129    pub fn slippery() -> Self {
130        Self::new(0.3, 0.1, 0.4, 0.01)
131    }
132
133    /// Creates a perfectly elastic collision (no energy loss).
134    ///
135    /// Good for: billiard balls, perfect bounce scenarios
136    ///
137    /// # Example
138    ///
139    /// ```
140    /// use goud_engine::ecs::collision::CollisionResponse;
141    ///
142    /// let elastic = CollisionResponse::elastic();
143    /// assert_eq!(elastic.restitution, 1.0);
144    /// ```
145    pub fn elastic() -> Self {
146        Self::new(1.0, 0.4, 0.4, 0.01)
147    }
148}
149
150impl Default for CollisionResponse {
151    /// Default collision response: moderate bounce and friction.
152    fn default() -> Self {
153        Self::new(0.4, 0.4, 0.4, 0.01)
154    }
155}
156
157/// Resolves a collision between two bodies using impulse-based physics.
158///
159/// This function computes and applies the impulse needed to resolve a collision
160/// between two dynamic bodies. It handles both normal impulse (bounce/separation)
161/// and tangent impulse (friction).
162///
163/// # Algorithm
164///
165/// 1. Compute relative velocity at contact point
166/// 2. Apply restitution (bounce) along normal
167/// 3. Apply friction along tangent
168/// 4. Apply position correction to prevent sinking
169///
170/// # Arguments
171///
172/// * `contact` - Contact information from collision detection
173/// * `velocity_a` - Linear velocity of body A
174/// * `velocity_b` - Linear velocity of body B
175/// * `inv_mass_a` - Inverse mass of body A (0.0 for static)
176/// * `inv_mass_b` - Inverse mass of body B (0.0 for static)
177/// * `response` - Collision response configuration
178///
179/// # Returns
180///
181/// Tuple of velocity changes: `(delta_velocity_a, delta_velocity_b)`
182///
183/// # Examples
184///
185/// ```
186/// use goud_engine::ecs::collision::{Contact, CollisionResponse, resolve_collision};
187/// use goud_engine::core::math::Vec2;
188///
189/// let contact = Contact::new(
190///     Vec2::new(1.0, 0.0),  // contact point
191///     Vec2::new(1.0, 0.0),  // normal (A to B)
192///     0.1,                   // penetration
193/// );
194///
195/// let vel_a = Vec2::new(10.0, 0.0);  // A moving right
196/// let vel_b = Vec2::new(-5.0, 0.0);  // B moving left
197/// let inv_mass_a = 1.0;               // 1 kg
198/// let inv_mass_b = 1.0;               // 1 kg
199/// let response = CollisionResponse::default();
200///
201/// let (delta_vel_a, delta_vel_b) = resolve_collision(
202///     &contact,
203///     vel_a,
204///     vel_b,
205///     inv_mass_a,
206///     inv_mass_b,
207///     &response,
208/// );
209///
210/// // Apply velocity changes
211/// let new_vel_a = vel_a + delta_vel_a;
212/// let new_vel_b = vel_b + delta_vel_b;
213/// ```
214///
215/// # Physics Notes
216///
217/// - Static bodies (inv_mass = 0.0) do not move
218/// - Higher mass (lower inv_mass) means less velocity change
219/// - Restitution of 1.0 means perfect elastic collision
220/// - Friction is applied perpendicular to collision normal
221pub fn resolve_collision(
222    contact: &Contact,
223    velocity_a: Vec2,
224    velocity_b: Vec2,
225    inv_mass_a: f32,
226    inv_mass_b: f32,
227    response: &CollisionResponse,
228) -> (Vec2, Vec2) {
229    // Early exit if both bodies are static
230    let total_inv_mass = inv_mass_a + inv_mass_b;
231    if total_inv_mass < 1e-6 {
232        return (Vec2::zero(), Vec2::zero());
233    }
234
235    // Compute relative velocity
236    let relative_velocity = velocity_b - velocity_a;
237    let velocity_along_normal = relative_velocity.dot(contact.normal);
238
239    // Don't resolve if velocities are separating
240    if velocity_along_normal > 0.0 {
241        return (Vec2::zero(), Vec2::zero());
242    }
243
244    // =================================================================
245    // Normal Impulse (bounce/separation)
246    // =================================================================
247
248    // Compute impulse scalar with restitution
249    let restitution = response.restitution;
250    let impulse_scalar = -(1.0 + restitution) * velocity_along_normal / total_inv_mass;
251
252    // Apply impulse along normal
253    let impulse = contact.normal * impulse_scalar;
254    let delta_vel_a_normal = impulse * -inv_mass_a;
255    let delta_vel_b_normal = impulse * inv_mass_b;
256
257    // =================================================================
258    // Tangent Impulse (friction)
259    // =================================================================
260
261    // Compute tangent (perpendicular to normal)
262    let tangent = Vec2::new(-contact.normal.y, contact.normal.x);
263
264    // Relative velocity along tangent
265    let relative_velocity_tangent =
266        (relative_velocity + delta_vel_b_normal - delta_vel_a_normal).dot(tangent);
267
268    // Don't apply friction if already stationary in tangent direction
269    if relative_velocity_tangent.abs() < 1e-6 {
270        return (delta_vel_a_normal, delta_vel_b_normal);
271    }
272
273    // Compute friction impulse (Coulomb friction model)
274    let friction_impulse_scalar = -relative_velocity_tangent / total_inv_mass;
275    let mu = response.friction;
276
277    // Clamp friction to not exceed normal impulse (Coulomb's law)
278    let friction_impulse_scalar =
279        friction_impulse_scalar.clamp(-impulse_scalar * mu, impulse_scalar * mu);
280
281    let friction_impulse = tangent * friction_impulse_scalar;
282    let delta_vel_a_friction = friction_impulse * -inv_mass_a;
283    let delta_vel_b_friction = friction_impulse * inv_mass_b;
284
285    // =================================================================
286    // Combine normal and friction impulses
287    // =================================================================
288
289    let delta_vel_a = delta_vel_a_normal + delta_vel_a_friction;
290    let delta_vel_b = delta_vel_b_normal + delta_vel_b_friction;
291
292    (delta_vel_a, delta_vel_b)
293}
294
295/// Computes positional correction to prevent sinking due to numerical drift.
296///
297/// This function uses Baumgarte stabilization to gradually push objects apart
298/// when they penetrate each other. This prevents accumulation of small errors
299/// that would cause objects to sink over time.
300///
301/// # Arguments
302///
303/// * `contact` - Contact information with penetration depth
304/// * `inv_mass_a` - Inverse mass of body A (0.0 for static)
305/// * `inv_mass_b` - Inverse mass of body B (0.0 for static)
306/// * `response` - Collision response configuration (uses position_correction and slop)
307///
308/// # Returns
309///
310/// Tuple of position corrections: `(correction_a, correction_b)`
311///
312/// # Examples
313///
314/// ```
315/// use goud_engine::ecs::collision::{Contact, CollisionResponse, compute_position_correction};
316/// use goud_engine::core::math::Vec2;
317///
318/// let contact = Contact::new(
319///     Vec2::new(1.0, 0.0),
320///     Vec2::new(1.0, 0.0),
321///     0.1,  // 0.1 units penetration
322/// );
323///
324/// let inv_mass_a = 1.0;
325/// let inv_mass_b = 1.0;
326/// let response = CollisionResponse::default();
327///
328/// let (correction_a, correction_b) = compute_position_correction(
329///     &contact,
330///     inv_mass_a,
331///     inv_mass_b,
332///     &response,
333/// );
334///
335/// // Apply corrections to positions
336/// // position_a += correction_a;
337/// // position_b += correction_b;
338/// ```
339///
340/// # Physics Notes
341///
342/// - Only corrects penetration above `slop` threshold
343/// - Correction percentage controls trade-off between stability and jitter
344/// - Static bodies (inv_mass = 0.0) do not move
345/// - Heavier objects move less than lighter objects
346pub fn compute_position_correction(
347    contact: &Contact,
348    inv_mass_a: f32,
349    inv_mass_b: f32,
350    response: &CollisionResponse,
351) -> (Vec2, Vec2) {
352    // Early exit if both bodies are static
353    let total_inv_mass = inv_mass_a + inv_mass_b;
354    if total_inv_mass < 1e-6 {
355        return (Vec2::zero(), Vec2::zero());
356    }
357
358    // Only correct penetration above slop threshold
359    let penetration = contact.penetration - response.slop;
360    if penetration <= 0.0 {
361        return (Vec2::zero(), Vec2::zero());
362    }
363
364    // Compute correction magnitude (Baumgarte stabilization)
365    let correction_percent = response.position_correction;
366    let correction_magnitude = penetration * correction_percent / total_inv_mass;
367
368    // Apply correction along collision normal
369    let correction = contact.normal * correction_magnitude;
370    let correction_a = correction * -inv_mass_a;
371    let correction_b = correction * inv_mass_b;
372
373    (correction_a, correction_b)
374}