Skip to main content

goud_engine/ecs/components/collider/
component.rs

1//! The [`Collider`] component definition and builder API.
2
3use crate::core::math::Rect;
4use crate::ecs::Component;
5
6use super::shape::ColliderShape;
7
8// =============================================================================
9// Collider Component
10// =============================================================================
11
12/// Collider component for physics collision detection.
13///
14/// Defines the collision shape, material properties (friction, restitution),
15/// and filtering (layers, masks) for an entity.
16///
17/// # Examples
18///
19/// ```
20/// use goud_engine::ecs::components::Collider;
21/// use goud_engine::core::math::Vec2;
22///
23/// // Circle collider
24/// let ball = Collider::circle(0.5);
25///
26/// // Box collider
27/// let wall = Collider::aabb(Vec2::new(5.0, 1.0));
28///
29/// // Capsule collider
30/// let player = Collider::capsule(0.3, 1.0);
31///
32/// // Sensor (trigger)
33/// let trigger = Collider::circle(2.0).with_is_sensor(true);
34/// ```
35#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
36pub struct Collider {
37    /// The geometric shape of the collider
38    shape: ColliderShape,
39
40    /// Coefficient of restitution (bounciness)
41    ///
42    /// 0.0 = no bounce, 1.0 = perfect bounce
43    restitution: f32,
44
45    /// Coefficient of friction
46    ///
47    /// 0.0 = frictionless, 1.0 = high friction
48    friction: f32,
49
50    /// Density for mass calculation (kg/m²)
51    ///
52    /// If set, the mass is automatically calculated from shape area × density.
53    /// If None, the [`RigidBody`](crate::ecs::components::RigidBody)'s mass is used directly.
54    density: Option<f32>,
55
56    /// Collision layer bitmask (which layer this collider is on)
57    ///
58    /// Use powers of 2 for layer values: 0b0001, 0b0010, 0b0100, etc.
59    layer: u32,
60
61    /// Collision mask bitmask (which layers this collider can collide with)
62    ///
63    /// Collision occurs if: (layer_a & mask_b) != 0 && (layer_b & mask_a) != 0
64    mask: u32,
65
66    /// If true, this collider is a sensor (trigger) that detects collisions
67    /// but doesn't produce physical response
68    is_sensor: bool,
69
70    /// If true, this collider is enabled and participates in collision detection
71    enabled: bool,
72}
73
74impl Collider {
75    // -------------------------------------------------------------------------
76    // Constructors
77    // -------------------------------------------------------------------------
78
79    /// Creates a new circle collider with the given radius.
80    ///
81    /// # Examples
82    ///
83    /// ```
84    /// use goud_engine::ecs::components::Collider;
85    ///
86    /// let ball = Collider::circle(0.5);
87    /// ```
88    pub fn circle(radius: f32) -> Self {
89        Self {
90            shape: ColliderShape::Circle { radius },
91            restitution: 0.3,
92            friction: 0.5,
93            density: None,
94            layer: 0xFFFFFFFF, // Default: all layers
95            mask: 0xFFFFFFFF,  // Default: collide with all layers
96            is_sensor: false,
97            enabled: true,
98        }
99    }
100
101    /// Creates a new axis-aligned box collider with the given half-extents.
102    ///
103    /// # Examples
104    ///
105    /// ```
106    /// use goud_engine::ecs::components::Collider;
107    /// use goud_engine::core::math::Vec2;
108    ///
109    /// // 10x2 box (half-extents are half the size)
110    /// let wall = Collider::aabb(Vec2::new(5.0, 1.0));
111    /// ```
112    pub fn aabb(half_extents: crate::core::math::Vec2) -> Self {
113        Self {
114            shape: ColliderShape::Aabb { half_extents },
115            restitution: 0.3,
116            friction: 0.5,
117            density: None,
118            layer: 0xFFFFFFFF,
119            mask: 0xFFFFFFFF,
120            is_sensor: false,
121            enabled: true,
122        }
123    }
124
125    /// Creates a new oriented box collider with the given half-extents.
126    ///
127    /// Similar to `aabb()` but supports rotation via the entity's Transform2D.
128    pub fn obb(half_extents: crate::core::math::Vec2) -> Self {
129        Self {
130            shape: ColliderShape::Obb { half_extents },
131            restitution: 0.3,
132            friction: 0.5,
133            density: None,
134            layer: 0xFFFFFFFF,
135            mask: 0xFFFFFFFF,
136            is_sensor: false,
137            enabled: true,
138        }
139    }
140
141    /// Creates a new capsule collider with the given half-height and radius.
142    ///
143    /// Good for character controllers with smooth movement.
144    ///
145    /// # Examples
146    ///
147    /// ```
148    /// use goud_engine::ecs::components::Collider;
149    ///
150    /// // Capsule with 0.6 radius and 2.0 total height
151    /// let player = Collider::capsule(0.3, 1.0);
152    /// ```
153    pub fn capsule(half_height: f32, radius: f32) -> Self {
154        Self {
155            shape: ColliderShape::Capsule {
156                half_height,
157                radius,
158            },
159            restitution: 0.3,
160            friction: 0.5,
161            density: None,
162            layer: 0xFFFFFFFF,
163            mask: 0xFFFFFFFF,
164            is_sensor: false,
165            enabled: true,
166        }
167    }
168
169    /// Creates a new convex polygon collider with the given vertices.
170    ///
171    /// Vertices must be in counter-clockwise order and form a convex hull.
172    ///
173    /// # Panics
174    ///
175    /// Panics if fewer than 3 vertices are provided.
176    pub fn polygon(vertices: Vec<crate::core::math::Vec2>) -> Self {
177        assert!(
178            vertices.len() >= 3,
179            "Polygon collider must have at least 3 vertices"
180        );
181        Self {
182            shape: ColliderShape::Polygon { vertices },
183            restitution: 0.3,
184            friction: 0.5,
185            density: None,
186            layer: 0xFFFFFFFF,
187            mask: 0xFFFFFFFF,
188            is_sensor: false,
189            enabled: true,
190        }
191    }
192
193    // -------------------------------------------------------------------------
194    // Builder Pattern
195    // -------------------------------------------------------------------------
196
197    /// Sets the restitution (bounciness) coefficient.
198    ///
199    /// 0.0 = no bounce, 1.0 = perfect bounce.
200    pub fn with_restitution(mut self, restitution: f32) -> Self {
201        self.restitution = restitution.clamp(0.0, 1.0);
202        self
203    }
204
205    /// Sets the friction coefficient.
206    ///
207    /// 0.0 = frictionless, 1.0 = high friction.
208    pub fn with_friction(mut self, friction: f32) -> Self {
209        self.friction = friction.max(0.0);
210        self
211    }
212
213    /// Sets the density for automatic mass calculation.
214    ///
215    /// Mass will be calculated as: area × density
216    pub fn with_density(mut self, density: f32) -> Self {
217        self.density = Some(density.max(0.0));
218        self
219    }
220
221    /// Sets the collision layer bitmask.
222    pub fn with_layer(mut self, layer: u32) -> Self {
223        self.layer = layer;
224        self
225    }
226
227    /// Sets the collision mask bitmask.
228    pub fn with_mask(mut self, mask: u32) -> Self {
229        self.mask = mask;
230        self
231    }
232
233    /// Sets whether this collider is a sensor (trigger).
234    pub fn with_is_sensor(mut self, is_sensor: bool) -> Self {
235        self.is_sensor = is_sensor;
236        self
237    }
238
239    /// Sets whether this collider is enabled.
240    pub fn with_enabled(mut self, enabled: bool) -> Self {
241        self.enabled = enabled;
242        self
243    }
244
245    // -------------------------------------------------------------------------
246    // Accessors
247    // -------------------------------------------------------------------------
248
249    /// Returns a reference to the collision shape.
250    pub fn shape(&self) -> &ColliderShape {
251        &self.shape
252    }
253
254    /// Returns the restitution coefficient.
255    pub fn restitution(&self) -> f32 {
256        self.restitution
257    }
258
259    /// Returns the friction coefficient.
260    pub fn friction(&self) -> f32 {
261        self.friction
262    }
263
264    /// Returns the density, if set.
265    pub fn density(&self) -> Option<f32> {
266        self.density
267    }
268
269    /// Returns the collision layer.
270    pub fn layer(&self) -> u32 {
271        self.layer
272    }
273
274    /// Returns the collision mask.
275    pub fn mask(&self) -> u32 {
276        self.mask
277    }
278
279    /// Returns true if this collider is a sensor (trigger).
280    pub fn is_sensor(&self) -> bool {
281        self.is_sensor
282    }
283
284    /// Returns true if this collider is enabled.
285    pub fn is_enabled(&self) -> bool {
286        self.enabled
287    }
288
289    /// Computes the axis-aligned bounding box (AABB) for this collider.
290    pub fn compute_aabb(&self) -> Rect {
291        self.shape.compute_aabb()
292    }
293
294    /// Checks if this collider can collide with another based on layer filtering.
295    ///
296    /// Returns true if: (self.layer & other.mask) != 0 && (other.layer & self.mask) != 0
297    pub fn can_collide_with(&self, other: &Collider) -> bool {
298        (self.layer & other.mask) != 0 && (other.layer & self.mask) != 0
299    }
300
301    // -------------------------------------------------------------------------
302    // Mutators
303    // -------------------------------------------------------------------------
304
305    /// Sets the restitution coefficient.
306    pub fn set_restitution(&mut self, restitution: f32) {
307        self.restitution = restitution.clamp(0.0, 1.0);
308    }
309
310    /// Sets the friction coefficient.
311    pub fn set_friction(&mut self, friction: f32) {
312        self.friction = friction.max(0.0);
313    }
314
315    /// Sets the density for automatic mass calculation.
316    pub fn set_density(&mut self, density: Option<f32>) {
317        self.density = density.map(|d| d.max(0.0));
318    }
319
320    /// Sets the collision layer.
321    pub fn set_layer(&mut self, layer: u32) {
322        self.layer = layer;
323    }
324
325    /// Sets the collision mask.
326    pub fn set_mask(&mut self, mask: u32) {
327        self.mask = mask;
328    }
329
330    /// Sets whether this collider is a sensor.
331    pub fn set_is_sensor(&mut self, is_sensor: bool) {
332        self.is_sensor = is_sensor;
333    }
334
335    /// Sets whether this collider is enabled.
336    pub fn set_enabled(&mut self, enabled: bool) {
337        self.enabled = enabled;
338    }
339
340    /// Replaces the collision shape.
341    pub fn set_shape(&mut self, shape: ColliderShape) {
342        self.shape = shape;
343    }
344}
345
346impl Default for Collider {
347    /// Returns a default circle collider with radius 0.5.
348    fn default() -> Self {
349        Self::circle(0.5)
350    }
351}
352
353impl std::fmt::Display for Collider {
354    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
355        write!(
356            f,
357            "Collider({}, restitution: {:.2}, friction: {:.2}{}{})",
358            self.shape.type_name(),
359            self.restitution,
360            self.friction,
361            if self.is_sensor { ", sensor" } else { "" },
362            if !self.enabled { ", disabled" } else { "" }
363        )
364    }
365}
366
367// Implement Component trait
368impl Component for Collider {}