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 {}