Skip to main content

alice_physics/
filter.rs

1//! Collision Filtering (Layer/Mask System)
2//!
3//! Bitmask-based collision filtering for controlling which bodies can interact.
4//!
5//! # Usage
6//!
7//! ```
8//! use alice_physics::filter::CollisionFilter;
9//!
10//! // Layer 0 = player, Layer 1 = enemy, Layer 2 = projectile
11//! let player = CollisionFilter::new(1 << 0, (1 << 1) | (1 << 2)); // collides with enemy + projectile
12//! let enemy  = CollisionFilter::new(1 << 1, (1 << 0) | (1 << 2)); // collides with player + projectile
13//! let ghost  = CollisionFilter::new(1 << 3, 0);                    // collides with nothing
14//! ```
15
16/// Collision filter using layer/mask bitmasks.
17///
18/// Two bodies can collide iff:
19///   `(a.layer & b.mask) != 0 && (b.layer & a.mask) != 0`
20///
21/// This provides fine-grained bidirectional control.
22#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
23pub struct CollisionFilter {
24    /// Which layer(s) this body belongs to (bitmask)
25    pub layer: u32,
26    /// Which layers this body can collide with (bitmask)
27    pub mask: u32,
28    /// Collision group ID (bodies in the same non-zero group never collide)
29    pub group: u32,
30}
31
32impl CollisionFilter {
33    /// Default filter: layer 1, collides with everything
34    pub const DEFAULT: Self = Self {
35        layer: 1,
36        mask: u32::MAX,
37        group: 0,
38    };
39
40    /// Filter that collides with nothing
41    pub const NONE: Self = Self {
42        layer: 0,
43        mask: 0,
44        group: 0,
45    };
46
47    /// Filter that collides with everything
48    pub const ALL: Self = Self {
49        layer: u32::MAX,
50        mask: u32::MAX,
51        group: 0,
52    };
53
54    /// Create a new collision filter
55    #[inline]
56    #[must_use]
57    pub const fn new(layer: u32, mask: u32) -> Self {
58        Self {
59            layer,
60            mask,
61            group: 0,
62        }
63    }
64
65    /// Create filter with a collision group
66    #[inline]
67    #[must_use]
68    pub const fn with_group(mut self, group: u32) -> Self {
69        self.group = group;
70        self
71    }
72
73    /// Check if two filters allow collision
74    #[inline]
75    #[must_use]
76    pub const fn can_collide(a: &Self, b: &Self) -> bool {
77        // Same non-zero group => never collide
78        if a.group != 0 && a.group == b.group {
79            return false;
80        }
81        // Bidirectional layer/mask check
82        (a.layer & b.mask) != 0 && (b.layer & a.mask) != 0
83    }
84}
85
86impl Default for CollisionFilter {
87    #[inline]
88    fn default() -> Self {
89        Self::DEFAULT
90    }
91}
92
93/// Predefined collision layers for common game setups.
94///
95/// Each constant is a single-bit `u32` bitmask. Combine them with bitwise OR
96/// to build [`CollisionFilter`] layer and mask fields:
97///
98/// ```
99/// use alice_physics::filter::{CollisionFilter, layers};
100///
101/// let player_filter = CollisionFilter::new(
102///     layers::PLAYER,
103///     layers::ENEMY | layers::PROJECTILE | layers::STATIC,
104/// );
105/// ```
106pub mod layers {
107    /// Default collision layer
108    pub const DEFAULT: u32 = 1 << 0;
109    /// Static geometry layer
110    pub const STATIC: u32 = 1 << 1;
111    /// Kinematic body layer
112    pub const KINEMATIC: u32 = 1 << 2;
113    /// Player layer
114    pub const PLAYER: u32 = 1 << 3;
115    /// Enemy layer
116    pub const ENEMY: u32 = 1 << 4;
117    /// Projectile layer
118    pub const PROJECTILE: u32 = 1 << 5;
119    /// Trigger volume layer
120    pub const TRIGGER: u32 = 1 << 6;
121    /// Debris layer
122    pub const DEBRIS: u32 = 1 << 7;
123    /// Sensor layer
124    pub const SENSOR: u32 = 1 << 8;
125    /// Vehicle layer
126    pub const VEHICLE: u32 = 1 << 9;
127    /// All layers combined
128    pub const ALL: u32 = u32::MAX;
129}
130
131#[cfg(test)]
132mod tests {
133    use super::*;
134
135    #[test]
136    fn test_default_filter() {
137        let a = CollisionFilter::DEFAULT;
138        let b = CollisionFilter::DEFAULT;
139        assert!(CollisionFilter::can_collide(&a, &b));
140    }
141
142    #[test]
143    fn test_none_filter() {
144        let a = CollisionFilter::NONE;
145        let b = CollisionFilter::DEFAULT;
146        assert!(!CollisionFilter::can_collide(&a, &b));
147    }
148
149    #[test]
150    fn test_layer_mask() {
151        let player = CollisionFilter::new(
152            layers::PLAYER,
153            layers::ENEMY | layers::PROJECTILE | layers::STATIC,
154        );
155        let enemy = CollisionFilter::new(
156            layers::ENEMY,
157            layers::PLAYER | layers::PROJECTILE | layers::STATIC,
158        );
159        let wall = CollisionFilter::new(layers::STATIC, layers::ALL);
160
161        assert!(CollisionFilter::can_collide(&player, &enemy));
162        assert!(CollisionFilter::can_collide(&player, &wall));
163        assert!(CollisionFilter::can_collide(&enemy, &wall));
164    }
165
166    #[test]
167    fn test_one_way_mask() {
168        // A can see B, but B cannot see A
169        let a = CollisionFilter::new(1 << 0, 1 << 1);
170        let b = CollisionFilter::new(1 << 1, 0); // mask=0: B doesn't want to collide with anything
171        assert!(!CollisionFilter::can_collide(&a, &b));
172    }
173
174    #[test]
175    fn test_collision_group() {
176        let a = CollisionFilter::new(layers::ALL, layers::ALL).with_group(1);
177        let b = CollisionFilter::new(layers::ALL, layers::ALL).with_group(1);
178        let c = CollisionFilter::new(layers::ALL, layers::ALL).with_group(2);
179
180        // Same group => no collision
181        assert!(!CollisionFilter::can_collide(&a, &b));
182        // Different group => collision allowed
183        assert!(CollisionFilter::can_collide(&a, &c));
184    }
185
186    #[test]
187    fn test_group_zero_always_checks_mask() {
188        let a = CollisionFilter::new(layers::ALL, layers::ALL).with_group(0);
189        let b = CollisionFilter::new(layers::ALL, layers::ALL).with_group(0);
190        assert!(CollisionFilter::can_collide(&a, &b));
191    }
192}