From 304c210350c1b7f9cbdd6cd495a4a67cf15c35f4 Mon Sep 17 00:00:00 2001 From: Joona Aalto Date: Sat, 17 Feb 2024 22:36:20 +0200 Subject: [PATCH] Rework layers (#313) # Objective Fixes #322 (among other things). Improve and simplify the API used for `CollisionLayers` and `SpatialQueryFilter`. ## Solution Add a `LayerMask` struct that stores a bitmask used for collision layers. The `groups` and `masks` properties have also been renamed to `memberships` and `filters`, because "groups" and "layers" sound like the same thing, and "masks" might be confusing since it's a single bitmask (just like groups/memberships). The new naming is also what Rapier uses. A `LayerMask` can be created from a `u32`, a type implementing `PhysicsLayer`, or an array of layers. `CollisionLayers::new` now takes `impl Into`. All of the following work: ```rust let layers = CollisionLayers::new(0b00010, 0b0111); let layers = CollisionLayers::new(GameLayer::Player, [GameLayer::Enemy, GameLayer::Ground]); let layers = CollisionLayers::new(LayerMask(0b0001), LayerMask::ALL); ``` `LayerMask` also allows us to reduce the number of `CollisionLayers` methods. `contains_group`/`contains_mask`, `add_group`/`add_mask` and so on have been removed in favor of simply calling methods like `add` or `remove` on the actual properties. ```rust layers.memberships.remove(GameLayer::Environment); layers.filters.add([GameLayer::Environment, GameLayer::Tree]); // Bitwise ops also work since we're accessing the bitmasks/layermasks directly layers.memberships |= GameLayer::Player; // or bitmask directly, e.g. 0b0010 ``` The methods mutate in place instead of returning a copy, which fixes #322. `SpatialQueryFilter` also now uses `LayerMask` for its masks, and its constructors have been improved. `new` has been removed for redundancy, `with_mask_from_bits` has been removed, and instead it has the methods `from_mask`, `from_excluded_entities`, `with_mask`, and `with_excluded_entities`. --- ## Changelog ### Added - `LayerMask` struct - `SpatialQueryFilter::from_mask` - `SpatialQueryFilter::from_excluded_entities` ### Changed #### `CollisionLayers` - `groups` and `masks` of `CollisionLayers` are now called `memberships` and `filters` - `CollisionLayers` stores `LayerMask`s for memberships and filters - Methods that took `impl IntoIterator` now take `impl Into` - Change `CollisionLayers::all()` to constant `CollisionLayers::ALL` - Change `CollisionLayers::none()` to constant `CollisionLayers::NONE` - Change `CollisionLayers::all_groups()` to constant `CollisionLayers::ALL_MEMBERSHIPS` - Change `CollisionLayers::all_masks()` to constant `CollisionLayers::ALL_FILTERS` #### `SpatialQueryFilter` - `SpatialQueryFilter` stores a `LayerMask` for collision masks - `SpatialQueryFilter::with_mask` now takes `impl Into` - Rename `SpatialQueryFilter::without_entities` to `with_excluded_entities` for consistency ### Removed - `CollisionLayers::add_group` and `CollisionLayers::add_mask` - `CollisionLayers::remove_group` and `CollisionLayers::remove_mask` - `CollisionLayers::contains_group` and `CollisionLayers::contains_mask` - `CollisionLayers::groups_bits` and `CollisionLayers::masks_bits` (you could get e.g. `layers.groups.0` instead) - `SpatialQueryFilter::new` - `SpatialQueryFilter::with_masks_from_bits` ## Migration Guide - Replace e.g. every `layers.add_group(...)` with `layers.memberships.add(...)` and so on, and similarly for masks/filters - Change `CollisionLayers::all()` to `CollisionLayers::ALL` - Change `CollisionLayers::none()` to `CollisionLayers::NONE` - Change `CollisionLayers::all_groups()` to `CollisionLayers::ALL_MEMBERSHIPS` - Change `CollisionLayers::all_masks()` to `CollisionLayers::ALL_FILTERS` - Change `SpatialQueryFilter::new()` to `SpatialQueryFilter::default()` or use the `SpatialQueryFilter::from_mask` or `SpatialQueryFilter::from_excluded_entities` constructors - Change `SpatialQueryFilter::without_entities` to `with_excluded_entities` - Change `CollisionLayers::groups_bits()` and `CollisionLayers::masks_bits()` to `layers.memberships.0` and `layers.filters.0` --- .../examples/cast_ray_predicate.rs | 2 +- src/components/layers.rs | 485 +++++++++++++----- src/plugins/spatial_query/query_filter.rs | 55 +- 3 files changed, 384 insertions(+), 158 deletions(-) diff --git a/crates/bevy_xpbd_3d/examples/cast_ray_predicate.rs b/crates/bevy_xpbd_3d/examples/cast_ray_predicate.rs index 95e61466..bf2806a8 100644 --- a/crates/bevy_xpbd_3d/examples/cast_ray_predicate.rs +++ b/crates/bevy_xpbd_3d/examples/cast_ray_predicate.rs @@ -175,7 +175,7 @@ fn raycast( direction, Scalar::MAX, true, - SpatialQueryFilter::new(), + SpatialQueryFilter::default(), &|entity| { if let Ok((_, out_of_glass)) = cubes.get(entity) { return !out_of_glass.0; // only look at cubes not out of glass diff --git a/src/components/layers.rs b/src/components/layers.rs index 39a2a290..e2fc87ee 100644 --- a/src/components/layers.rs +++ b/src/components/layers.rs @@ -1,3 +1,5 @@ +use std::ops::{BitAnd, BitAndAssign, BitOr, BitOrAssign, BitXor, BitXorAssign, Not}; + use bevy::prelude::*; /// A layer used for determining which entities should interact with each other. @@ -21,192 +23,419 @@ impl PhysicsLayer for &L { } } -/// Defines the collision layers of a collider using *groups* and *masks*. -/// -/// **Groups** indicate what layers the collider is a part of.\ -/// **Masks** indicate what layers the collider can interact with. +/// A bitmask for layers. /// -/// Two colliders `A` and `B` can interact if and only if: +/// A [`LayerMask`] can be constructed from bits directly, or from types implementing [`PhysicsLayer`]. /// -/// - The groups of `A` contain a layer that is also in the masks of `B` -/// - The groups of `B` contain a layer that is also in the masks of `A` -/// -/// Colliders without this component can be considered as having all groups and masks, and they can -/// interact with everything that belongs on any layer. +/// ``` +#[cfg_attr(feature = "2d", doc = "# use bevy_xpbd_2d::prelude::*;")] +#[cfg_attr(feature = "3d", doc = "# use bevy_xpbd_3d::prelude::*;")] +/// # +/// #[derive(PhysicsLayer, Clone, Copy, Debug)] +/// enum GameLayer { +/// Player, // Layer 0 +/// Enemy, // Layer 1 +/// Ground, // Layer 2 +/// } /// -/// ## Creation +/// // Here, `GameLayer::Enemy` is automatically converted to a `LayerMask` for the comparison. +/// assert_eq!(LayerMask(0b0010), GameLayer::Enemy); +/// ``` /// -/// The easiest way to build a [`CollisionLayers`] configuration is to use the [`CollisionLayers::new()`] method -/// that takes in a list of groups and masks. Additional groups and masks can be added and removed by calling methods like -/// [`add_groups`](Self::add_groups), [`add_masks`](Self::add_masks), [`remove_groups`](Self::remove_groups) and -/// [`remove_masks`](Self::remove_masks). +/// Bitwise operations can be used to modify and combine masks: /// -/// These methods require the layers to implement [`PhysicsLayer`]. The easiest way to define the physics layers is to -/// create an enum with `#[derive(PhysicsLayer)]`. +/// ``` +#[cfg_attr(feature = "2d", doc = "# use bevy_xpbd_2d::prelude::*;")] +#[cfg_attr(feature = "3d", doc = "# use bevy_xpbd_3d::prelude::*;")] +/// let mask1 = LayerMask(0b0001); +/// let mask2 = LayerMask(0b0010); +/// assert_eq!(mask1 | mask2, LayerMask(0b0011)); /// -/// Internally, the groups and masks are represented as bitmasks, so you can also use [`CollisionLayers::from_bits()`] -/// to create collision layers. +/// // You can also add layers from `u32` bitmasks and compare against them directly. +/// assert_eq!(mask1 | 0b0010, 0b0011); +/// ``` /// -/// ## Example +/// Another way to use [`LayerMask`] is to define layers as constants: /// /// ``` -/// use bevy::prelude::*; -#[cfg_attr(feature = "2d", doc = "use bevy_xpbd_2d::prelude::*;")] -#[cfg_attr(feature = "3d", doc = "use bevy_xpbd_3d::prelude::*;")] -/// -/// #[derive(PhysicsLayer)] -/// enum Layer { -/// Player, -/// Enemy, -/// Ground, -/// } +#[cfg_attr(feature = "2d", doc = "# use bevy_xpbd_2d::prelude::*;")] +#[cfg_attr(feature = "3d", doc = "# use bevy_xpbd_3d::prelude::*;")] +/// // `1 << n` is bitshifting: the first layer shifted by `n` layers. +/// pub const FIRST_LAYER: LayerMask = LayerMask(1 << 0); +/// pub const LAST_LAYER: LayerMask = LayerMask(1 << 31); /// -/// fn spawn(mut commands: Commands) { -/// commands.spawn(( -/// Collider::ball(0.5), -/// // Player collides with enemies and the ground, but not with other players -/// CollisionLayers::new([Layer::Player], [Layer::Enemy, Layer::Ground]) -/// )); -/// } +/// // Bitwise operations for `LayerMask` unfortunately can't be const, so we need to access the `u32` values. +/// pub const COMBINED: LayerMask = LayerMask(FIRST_LAYER.0 | LAST_LAYER.0); /// ``` -#[derive(Reflect, Clone, Copy, Component, Debug, PartialEq)] +#[derive(Reflect, Clone, Copy, Debug, Deref, DerefMut, Eq, PartialOrd, Ord)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] -#[reflect(Component)] -pub struct CollisionLayers { - groups: u32, - masks: u32, -} +pub struct LayerMask(pub u32); -impl CollisionLayers { - /// Creates a new [`CollisionLayers`] configuration with the given collision groups and masks. - pub fn new( - groups: impl IntoIterator, - masks: impl IntoIterator, - ) -> Self { - Self::none().add_groups(groups).add_masks(masks) +impl From for LayerMask { + fn from(layer: u32) -> Self { + Self(layer) } +} - /// Contains all groups and masks. - pub fn all() -> Self { - Self::from_bits(L::all_bits(), L::all_bits()) +impl From for LayerMask { + fn from(layer: L) -> Self { + LayerMask(layer.to_bits()) } +} + +impl, const N: usize> From<[L; N]> for LayerMask { + fn from(value: [L; N]) -> Self { + let mut bits = 0; - /// Contains all groups but no masks. - pub fn all_groups() -> Self { - Self::from_bits(L::all_bits(), 0) + for layer in value.into_iter().map(|l| { + let layers: LayerMask = l.into(); + layers + }) { + bits |= layer.0; + } + + LayerMask(bits) } +} + +impl LayerMask { + /// Contains all layers. + pub const ALL: Self = Self(0xffff_ffff); + /// Contains no layers. + pub const NONE: Self = Self(0); - /// Contains all masks but no groups. - pub fn all_masks() -> Self { - Self::from_bits(0, L::all_bits()) + /// Adds the given `layers` to `self`. + /// + /// # Example + /// + /// ``` + #[cfg_attr(feature = "2d", doc = "# use bevy_xpbd_2d::prelude::*;")] + #[cfg_attr(feature = "3d", doc = "# use bevy_xpbd_3d::prelude::*;")] + /// let mut layers = LayerMask(0b1010); + /// + /// // These are equivalent + /// layers.add(0b0001); + /// layers |= 0b0001; + /// + /// assert_eq!(layers, 0b1011); + /// ``` + pub fn add(&mut self, layers: impl Into) { + let layers: LayerMask = layers.into(); + *self |= layers; } - /// Contains no masks or groups. - pub const fn none() -> Self { - Self::from_bits(0, 0) + /// Removes the given `layers` from `self`. + /// + /// # Example + /// + /// ``` + #[cfg_attr(feature = "2d", doc = "# use bevy_xpbd_2d::prelude::*;")] + #[cfg_attr(feature = "3d", doc = "# use bevy_xpbd_3d::prelude::*;")] + /// let mut layers = LayerMask(0b1010); + /// + /// // These are equivalent + /// layers.remove(0b0010); + /// layers &= !0b0010; + /// + /// assert_eq!(layers, 0b1000); + /// ``` + pub fn remove(&mut self, layers: impl Into) { + let layers: LayerMask = layers.into(); + *self &= !layers; } - /// Creates a new [`CollisionLayers`] using bits. + /// Returns `true` if `self` contains all of the given `layers`. /// - /// There is one bit per group and mask, so there are a total of 32 layers. - /// For example, if an entity is a part of the layers `[0, 1, 3]` and can interact with the layers `[1, 2]`, - /// the groups in bits would be `0b01011` while the masks would be `0b00110`. - pub const fn from_bits(groups: u32, masks: u32) -> Self { - Self { groups, masks } + /// # Example + /// + /// ``` + #[cfg_attr(feature = "2d", doc = "# use bevy_xpbd_2d::prelude::*;")] + #[cfg_attr(feature = "3d", doc = "# use bevy_xpbd_3d::prelude::*;")] + /// let mut layers = LayerMask(0b1010); + /// + /// // These are equivalent + /// assert!(layers.has_all(0b1010)); + /// assert!((layers & 0b1010) != 0); + /// + /// assert!(!layers.has_all(0b0100)); + /// assert!((layers & 0b0100) == 0); + /// ``` + #[doc(alias = "contains_all")] + pub fn has_all(self, layers: impl Into) -> bool { + let layers: LayerMask = layers.into(); + (self & layers) != 0 } +} - /// Returns true if an entity with this [`CollisionLayers`] configuration - /// can interact with an entity with the `other` [`CollisionLayers`] configuration. - pub fn interacts_with(self, other: Self) -> bool { - (self.groups & other.masks) != 0 && (other.groups & self.masks) != 0 +impl + Copy> PartialEq for LayerMask { + fn eq(&self, other: &L) -> bool { + let other: Self = (*other).into(); + self.0 == other.0 } +} + +impl> BitAnd for LayerMask { + type Output = Self; - /// Returns true if the given layer is contained in `groups`. - pub fn contains_group(self, layer: impl PhysicsLayer) -> bool { - (self.groups & layer.to_bits()) != 0 + fn bitand(self, rhs: L) -> Self::Output { + Self(self.0 & rhs.into().0) } +} - /// Adds the given layer into `groups`. - pub fn add_group(mut self, layer: impl PhysicsLayer) -> Self { - self.groups |= layer.to_bits(); - self +impl> BitAndAssign for LayerMask { + fn bitand_assign(&mut self, rhs: L) { + self.0 = self.0 & rhs.into().0; } +} - /// Adds the given layers into `groups`. - pub fn add_groups(mut self, layers: impl IntoIterator) -> Self { - for layer in layers.into_iter().map(|l| l.to_bits()) { - self.groups |= layer; - } +impl> BitOr for LayerMask { + type Output = Self; - self + fn bitor(self, rhs: L) -> Self::Output { + Self(self.0 | rhs.into().0) } +} - /// Removes the given layer from `groups`. - pub fn remove_group(mut self, layer: impl PhysicsLayer) -> Self { - self.groups &= !layer.to_bits(); - self +impl> BitOrAssign for LayerMask { + fn bitor_assign(&mut self, rhs: L) { + self.0 = self.0 | rhs.into().0; } +} - /// Removes the given layers from `groups`. - pub fn remove_groups(mut self, layers: impl IntoIterator) -> Self { - for layer in layers.into_iter().map(|l| l.to_bits()) { - self.groups &= !layer; - } +impl> BitXor for LayerMask { + type Output = Self; - self + fn bitxor(self, rhs: L) -> Self::Output { + Self(self.0 ^ rhs.into().0) } +} - /// Returns true if the given layer is contained in `masks`. - pub fn contains_mask(self, layer: impl PhysicsLayer) -> bool { - (self.masks & layer.to_bits()) != 0 +impl> BitXorAssign for LayerMask { + fn bitxor_assign(&mut self, rhs: L) { + self.0 = self.0 ^ rhs.into().0; } +} - /// Adds the given layer into `masks`. - pub fn add_mask(mut self, layer: impl PhysicsLayer) -> Self { - self.masks |= layer.to_bits(); - self +impl Not for LayerMask { + type Output = Self; + + fn not(self) -> Self::Output { + Self(!self.0) } +} - /// Adds the given layers in `masks`. - pub fn add_masks(mut self, layers: impl IntoIterator) -> Self { - for layer in layers.into_iter().map(|l| l.to_bits()) { - self.masks |= layer; - } +/// Defines the collision layers of a collider using *memberships* and *filters*. +/// +/// **Memberships** indicate what layers the collider is a part of.\ +/// **Filters** indicate what layers the collider can interact with. +/// +/// Two colliders `A` and `B` can interact if and only if: +/// +/// - The memberships of `A` contain a layer that is also in the filters of `B` +/// - The memberships of `B` contain a layer that is also in the filters of `A` +/// +/// Colliders without this component can be considered as having all memberships and filters, and they can +/// interact with everything that belongs on any layer. +/// +/// ## Creation +/// +/// Collision layers store memberships and filters using [`LayerMask`]s. A [`LayerMask`] can be created using +/// bitmasks, or by creating an enum that implements [`PhysicsLayer`]. +/// +/// Many [`CollisionLayers`] methods can take any type that implements `Into`. +/// For example, you can use bitmasks with [`CollisionLayers::new`]: +/// +/// ``` +#[cfg_attr(feature = "2d", doc = "# use bevy_xpbd_2d::prelude::*;")] +#[cfg_attr(feature = "3d", doc = "# use bevy_xpbd_3d::prelude::*;")] +/// # +/// // Belongs to the second layer and interacts with colliders +/// // on the first, second, and third layer. +/// let layers = CollisionLayers::new(0b00010, 0b0111); +/// ``` +/// +/// You can also use an enum that implements [`PhysicsLayer`]: +/// +/// ``` +#[cfg_attr(feature = "2d", doc = "# use bevy_xpbd_2d::prelude::*;")] +#[cfg_attr(feature = "3d", doc = "# use bevy_xpbd_3d::prelude::*;")] +/// # +/// #[derive(PhysicsLayer)] +/// enum GameLayer { +/// Player, // Layer 0 +/// Enemy, // Layer 1 +/// Ground, // Layer 2 +/// } +/// +/// // Player collides with enemies and the ground, but not with other players +/// let layers = CollisionLayers::new(GameLayer::Player, [GameLayer::Enemy, GameLayer::Ground]); +/// ``` +/// +/// You can also use [`LayerMask`] directly: +/// +/// ``` +#[cfg_attr(feature = "2d", doc = "# use bevy_xpbd_2d::prelude::*;")] +#[cfg_attr(feature = "3d", doc = "# use bevy_xpbd_3d::prelude::*;")] +/// # +/// // Belongs to the first layer and interacts with all layers. +/// let layers = CollisionLayers::new(LayerMask(0b0001), LayerMask::ALL); +/// ``` +/// +/// Layers can also be defined using constants and bitwise operations: +/// +/// ``` +/// # use bevy::prelude::Commands; +#[cfg_attr(feature = "2d", doc = "# use bevy_xpbd_2d::prelude::*;")] +#[cfg_attr(feature = "3d", doc = "# use bevy_xpbd_3d::prelude::*;")] +/// // `1 << n` is bitshifting: the first layer shifted by `n` layers. +/// pub const FIRST_LAYER: u32 = 1 << 0; +/// pub const SECOND_LAYER: u32 = 1 << 1; +/// pub const LAST_LAYER: u32 = 1 << 31; +/// +/// fn spawn(mut commands: Commands) { +/// // This collider belongs to the first two layers and can interact with the last layer. +/// commands.spawn(( +/// Collider::ball(0.5), +/// CollisionLayers::from_bits(FIRST_LAYER | SECOND_LAYER, LAST_LAYER), +/// // ...other components +/// )); +/// } +/// ``` +/// +/// ## Modifying layers +/// +/// Existing [`CollisionLayers`] can be modified by simply accessing the `memberships` and `filters` +/// and changing their [`LayerMask`]s. +/// +/// ``` +#[cfg_attr(feature = "2d", doc = "# use bevy_xpbd_2d::prelude::*;")] +#[cfg_attr(feature = "3d", doc = "# use bevy_xpbd_3d::prelude::*;")] +/// let mut layers = CollisionLayers::new(0b0010, 0b1011); +/// +/// // Add memberships (these are equivalent) +/// layers.memberships.add(0b0001); +/// layers.memberships |= 0b0001; +/// +/// assert_eq!(layers.memberships, 0b0011); +/// +/// // Remove filters +/// layers.filters.remove(0b0001); +/// layers.filters &= !0b0001; +/// +/// assert_eq!(layers.filters, 0b1010); +/// +/// // Check if layers are contained +/// assert!(layers.memberships.has_all(0b0011)); +/// assert!((layers.memberships & 0b0011) != 0); +/// ``` +#[derive(Reflect, Clone, Copy, Component, Debug, PartialEq, Eq)] +#[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] +#[reflect(Component)] +pub struct CollisionLayers { + /// The layers that an entity belongs to. + #[doc(alias = "groups", alias = "layers")] + pub memberships: LayerMask, + /// The layers that an entity can interact with. + #[doc(alias = "masks", alias = "layer_mask")] + pub filters: LayerMask, +} - self - } +impl CollisionLayers { + /// Contains all memberships and filters. + pub const ALL: Self = Self { + memberships: LayerMask::ALL, + filters: LayerMask::ALL, + }; - /// Removes the given layer from `masks`. - pub fn remove_mask(mut self, layer: impl PhysicsLayer) -> Self { - self.masks &= !layer.to_bits(); - self - } + /// Contains no memberships and no filters. + pub const NONE: Self = Self { + memberships: LayerMask::NONE, + filters: LayerMask::NONE, + }; - /// Removes the given layers from `masks`. - pub fn remove_masks(mut self, layers: impl IntoIterator) -> Self { - for layer in layers.into_iter().map(|l| l.to_bits()) { - self.masks &= !layer; - } + /// Contains all memberships but no filters. + pub const ALL_MEMBERSHIPS: Self = Self { + memberships: LayerMask::ALL, + filters: LayerMask::NONE, + }; + + /// Contains all filters but no memberships. + pub const ALL_FILTERS: Self = Self { + memberships: LayerMask::NONE, + filters: LayerMask::ALL, + }; - self + /// Creates a new [`CollisionLayers`] configuration with the given collision memberships and filters. + pub fn new(memberships: impl Into, filters: impl Into) -> Self { + Self { + memberships: memberships.into(), + filters: filters.into(), + } } - /// Returns the `groups` bitmask. - pub fn groups_bits(self) -> u32 { - self.groups + /// Creates a new [`CollisionLayers`] configuration using bits. + /// + /// There is one bit per group and mask, so there are a total of 32 layers. + /// For example, if an entity is a part of the layers `[0, 1, 3]` and can interact with the layers `[1, 2]`, + /// the memberships in bits would be `0b01011` while the filters would be `0b00110`. + pub const fn from_bits(memberships: u32, filters: u32) -> Self { + Self { + memberships: LayerMask(memberships), + filters: LayerMask(filters), + } } - /// Returns the `masks` bitmask. - pub fn masks_bits(self) -> u32 { - self.masks + /// Returns true if an entity with this [`CollisionLayers`] configuration + /// can interact with an entity with the `other` [`CollisionLayers`] configuration. + pub fn interacts_with(self, other: Self) -> bool { + (self.memberships & other.filters) != LayerMask::NONE + && (other.memberships & self.filters) != LayerMask::NONE } } impl Default for CollisionLayers { fn default() -> Self { Self { - groups: 0xffff_ffff, - masks: 0xffff_ffff, + memberships: LayerMask::ALL, + filters: LayerMask::ALL, } } } + +#[cfg(test)] +mod tests { + // Needed for PhysicsLayer derive macro + #[cfg(feature = "2d")] + use crate as bevy_xpbd_2d; + #[cfg(feature = "3d")] + use crate as bevy_xpbd_3d; + + use crate::prelude::*; + + #[derive(PhysicsLayer)] + enum GameLayer { + Player, + Enemy, + Ground, + } + + #[test] + fn creation() { + let with_bitmask = CollisionLayers::new(0b0010, 0b0101); + let with_enum = + CollisionLayers::new(GameLayer::Enemy, [GameLayer::Player, GameLayer::Ground]); + let with_layers = + CollisionLayers::new(LayerMask::from(GameLayer::Enemy), LayerMask(0b0101)); + + assert_eq!(with_bitmask, with_enum); + assert_eq!(with_bitmask, with_layers); + + assert!(with_bitmask.memberships.has_all(GameLayer::Enemy)); + assert!(!with_bitmask.memberships.has_all(GameLayer::Player)); + + assert!(with_bitmask + .filters + .has_all([GameLayer::Player, GameLayer::Ground])); + assert!(!with_bitmask.filters.has_all(GameLayer::Enemy)); + } +} diff --git a/src/plugins/spatial_query/query_filter.rs b/src/plugins/spatial_query/query_filter.rs index ae5ae0d7..af230d0d 100644 --- a/src/plugins/spatial_query/query_filter.rs +++ b/src/plugins/spatial_query/query_filter.rs @@ -14,10 +14,8 @@ use crate::prelude::*; /// fn setup(mut commands: Commands) { /// let object = commands.spawn(Collider::ball(0.5)).id(); /// -/// // A query filter that has three collision masks and excludes the `object` entity -/// let query_filter = SpatialQueryFilter::new() -/// .with_masks_from_bits(0b1011) -/// .without_entities([object]); +/// // A query filter that has three collision layers and excludes the `object` entity +/// let query_filter = SpatialQueryFilter::from_mask(0b1011).with_excluded_entities([object]); /// /// // Spawn a ray caster with the query filter /// commands.spawn(RayCaster::default().with_query_filter(query_filter)); @@ -26,8 +24,8 @@ use crate::prelude::*; #[derive(Clone)] #[cfg_attr(feature = "serialize", derive(serde::Serialize, serde::Deserialize))] pub struct SpatialQueryFilter { - /// Specifies which [collision groups](CollisionLayers) will be included in a [spatial query](crate::spatial_query). - pub masks: u32, + /// Specifies which [collision layers](CollisionLayers) will be included in the [spatial query](crate::spatial_query). + pub mask: LayerMask, /// Entities that will not be included in [spatial queries](crate::spatial_query). pub excluded_entities: HashSet, } @@ -35,39 +33,39 @@ pub struct SpatialQueryFilter { impl Default for SpatialQueryFilter { fn default() -> Self { Self { - masks: 0xffff_ffff, + mask: LayerMask::ALL, excluded_entities: default(), } } } impl SpatialQueryFilter { - /// Creates a new [`SpatialQueryFilter`] that doesn't exclude any colliders. - pub fn new() -> Self { - Self::default() + /// Creates a new [`SpatialQueryFilter`] with the given [`LayerMask`] determining + /// which [collision layers](CollisionLayers) will be included in the [spatial query](crate::spatial_query). + pub fn from_mask(mask: impl Into) -> Self { + Self { + mask: mask.into(), + ..default() + } } - /// Sets the masks of the filter configuration using a bitmask. Colliders with the corresponding - /// [collision group](CollisionLayers) will be included in the [spatial query](crate::spatial_query). - pub fn with_masks_from_bits(mut self, masks: u32) -> Self { - self.masks = masks; - self + /// Creates a new [`SpatialQueryFilter`] with the given entities excluded from the [spatial query](crate::spatial_query). + pub fn from_excluded_entities(entities: impl IntoIterator) -> Self { + Self { + excluded_entities: HashSet::from_iter(entities), + ..default() + } } - /// Sets the masks of the filter configuration using a list of [layers](PhysicsLayer). - /// Colliders with the corresponding [collision groups](CollisionLayers) will be included - /// in the [spatial query](crate::spatial_query). - pub fn with_masks(mut self, masks: impl IntoIterator) -> Self { - self.masks = 0; - for mask in masks.into_iter().map(|l| l.to_bits()) { - self.masks |= mask; - } + /// Sets the [`LayerMask`] of the filter configuration. Only colliders with the corresponding + /// [collision layer memberships](CollisionLayers) will be included in the [spatial query](crate::spatial_query). + pub fn with_mask(mut self, masks: impl Into) -> Self { + self.mask = masks.into(); self } - /// Excludes the given entities from [spatial queries](crate::spatial_query). - #[doc(alias = "exclude_entities")] - pub fn without_entities(mut self, entities: impl IntoIterator) -> Self { + /// Excludes the given entities from the [spatial query](crate::spatial_query). + pub fn with_excluded_entities(mut self, entities: impl IntoIterator) -> Self { self.excluded_entities = HashSet::from_iter(entities); self } @@ -76,8 +74,7 @@ impl SpatialQueryFilter { /// filter configuration. pub fn test(&self, entity: Entity, layers: CollisionLayers) -> bool { !self.excluded_entities.contains(&entity) - && CollisionLayers::from_bits(0xffff_ffff, self.masks).interacts_with( - CollisionLayers::from_bits(layers.groups_bits(), 0xffff_ffff), - ) + && CollisionLayers::new(LayerMask::ALL, self.mask) + .interacts_with(CollisionLayers::new(layers.memberships, LayerMask::ALL)) } }