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}