Skip to content

Commit

Permalink
Make PhysicsLayer require a #[default] variant (#494)
Browse files Browse the repository at this point in the history
# Objective

#476 changed collision layers to reserve the first bit for a default layer. However, for enums implementing `PhysicsLayer`, the default layer is rather implicit and potentially footgunny.

The default layer should ideally be more explicit.

## Solution

Change `PhysicsLayer` to require `Default` to be implemented. The proc macro orders the variants such that the layer set as the default is always the first bit.

Note that `Default` *must* currently be derived instead of implemented manually, as the macro can only access the default variant if the `#[default]` attribute exists. An error is emitted if `#[default]` does not exist. If someone knows a way to make it work with the manual impl, let me know or consider opening a PR!

I also added documentation for the `PhysicsLayer` macro, and improved error handling.

---

## Migration Guide

Enums that derive `PhysicsLayer` must now also derive `Default` and specify a default layer using the `#[default]` attribute.

Before:

```rust
#[derive(PhysicsLayer)]
enum GameLayer {
    Player,
    Enemy,
    Ground,
}
```

After:

```rust
#[derive(PhysicsLayer, Default)]
enum GameLayer {
    #[default]
    Default, // The name doesn't matter, but Default is used here for clarity
    Player,
    Enemy,
    Ground,
}
```

The default layer always has the bit value `0b0001` regardless of its order relative to the other variants.

---------

Co-authored-by: Jan Hohenheim <[email protected]>
  • Loading branch information
Jondolf and janhohenheim committed Aug 23, 2024
1 parent e443720 commit 89b733e
Show file tree
Hide file tree
Showing 4 changed files with 96 additions and 14 deletions.
4 changes: 3 additions & 1 deletion crates/avian2d/examples/collision_layers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,8 +20,10 @@ fn main() {
}

// Define the collision layers
#[derive(PhysicsLayer)]
#[derive(PhysicsLayer, Default)]
enum Layer {
#[default]
Default,
Blue,
Red,
}
Expand Down
1 change: 1 addition & 0 deletions crates/avian_derive/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,5 +14,6 @@ bench = false

[dependencies]
proc-macro2 = "1.0.78"
proc-macro-error = "1.0"
quote = "1.0"
syn = "2.0"
82 changes: 74 additions & 8 deletions crates/avian_derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,19 +2,51 @@

use proc_macro::TokenStream;

use quote::{quote, quote_spanned};
use proc_macro_error::{abort, emit_error, proc_macro_error};
use quote::quote;
use syn::{parse_macro_input, spanned::Spanned, Data, DeriveInput};

// Modified macro From the discontinued Heron,
// see https://github.com/jcornaz/heron/blob/main/macros/src/lib.rs
// Modified macro from the discontinued Heron
// https://github.com/jcornaz/heron/blob/main/macros/src/lib.rs
/// A derive macro for defining physics layers using an enum.
///
/// Each variant of the enum represents a layer. Each layer has a unique bit determined by
/// the order of the variants. The bit value can be retrieved using the `to_bits` method.
///
/// # Requirements
///
/// - The enum must have at most 32 variants.
/// - The enum variants must not have any fields.
/// - The enum must have a default variant with the `#[default]` attribute.
/// - The first bit `1 << 0` will *always* be reserved for the default layer.
/// The bit values of the other layers are determined by their order in the enum, starting from `1 << 1`.
///
/// # Example
///
/// ```ignore
/// #[derive(PhysicsLayer, Clone, Copy, Debug, Default)]
/// enum GameLayer {
/// #[default]
/// Default, // Layer 0 - the default layer that objects are assigned to
/// Player, // Layer 1
/// Enemy, // Layer 2
/// Ground, // Layer 3
/// }
///
/// // The first bit is reserved for the default layer.
/// assert_eq!(GameLayer::default().to_bits(), 1 << 0);
///
/// // The `GameLayer::Ground` layer is the fourth layer, so its bit value is `1 << 3`.
/// assert_eq!(GameLayer::Ground.to_bits(), 1 << 3);
/// ```
#[proc_macro_error]
#[proc_macro_derive(PhysicsLayer)]
pub fn derive_physics_layer(input: TokenStream) -> TokenStream {
let input = parse_macro_input!(input as DeriveInput);
let enum_ident = input.ident;

fn non_enum_item_error(span: proc_macro2::Span) -> TokenStream {
quote_spanned! { span => compile_error!("only enums can automatically derive PhysicsLayer"); }
.into()
abort!(span, "only enums can automatically derive `PhysicsLayer`");
}
let variants = match &input.data {
Data::Enum(data) => &data.variants,
Expand All @@ -27,10 +59,39 @@ pub fn derive_physics_layer(input: TokenStream) -> TokenStream {
};

if variants.len() > 32 {
return quote! { compile_error!("PhysicsLayer only supports a maximum of 32 layers"); }
.into();
emit_error!(
enum_ident,
"`PhysicsLayer` only supports a maximum of 32 layers"
);
}

let mut default_variant_index = None;

for (i, variant) in variants.iter().enumerate() {
for attr in variant.attrs.iter() {
if attr.path().is_ident("default") {
if default_variant_index.is_some() {
emit_error!(enum_ident, "multiple defaults");
break;
}
default_variant_index = Some(i);
}
}
}

let Some(default_variant_index) = default_variant_index else {
abort!(
enum_ident,
"`PhysicsLayer` enums must derive `Default` and have a variant annotated with the `#[default]` attribute.";
note = "Manually implementing `Default` using `impl Default for FooLayer` is not supported."
);
};

// Move the default variant to the front (this probably isn't the best way to do this)
let mut variants = variants.iter().collect::<Vec<_>>();
let default_variant = variants.remove(default_variant_index);
variants.insert(0, default_variant);

let to_bits_result: Result<Vec<_>, _> = variants
.iter()
.enumerate()
Expand All @@ -47,7 +108,12 @@ pub fn derive_physics_layer(input: TokenStream) -> TokenStream {

let to_bits = match to_bits_result {
Ok(tokens) => tokens,
Err(span) => return quote_spanned! { span => compile_error!("can only derive PhysicsLayer for enums without fields"); }.into(),
Err(span) => {
abort!(
span,
"can only derive `PhysicsLayer` for enums without fields"
);
}
};

let all_bits: u32 = if variants.len() == 32 {
Expand Down
23 changes: 18 additions & 5 deletions src/collision/layers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,14 +6,17 @@ use bevy::prelude::*;
/// Physics layers are used heavily by [`CollisionLayers`].
///
/// This trait can be derived for enums with `#[derive(PhysicsLayer)]`.
pub trait PhysicsLayer: Sized {
pub trait PhysicsLayer: Sized + Default {
/// Converts the layer to a bitmask.
fn to_bits(&self) -> u32;
/// Creates a layer bitmask with all bits set to 1.
fn all_bits() -> u32;
}

impl<L: PhysicsLayer> PhysicsLayer for &L {
impl<'a, L: PhysicsLayer> PhysicsLayer for &'a L
where
&'a L: Default,
{
fn to_bits(&self) -> u32 {
L::to_bits(self)
}
Expand All @@ -26,19 +29,27 @@ impl<L: PhysicsLayer> PhysicsLayer for &L {
/// A bitmask for layers.
///
/// A [`LayerMask`] can be constructed from bits directly, or from types implementing [`PhysicsLayer`].
/// The first bit `0b0001` is reserved for the default layer, which all entities belong to by default.
///
/// ```
#[cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
#[cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
/// #
/// #[derive(PhysicsLayer, Clone, Copy, Debug)]
/// #[derive(PhysicsLayer, Clone, Copy, Debug, Default)]
/// enum GameLayer {
/// #[default]
/// Default, // Layer 0 - the default layer that objects are assigned to
/// Player, // Layer 1
/// Enemy, // Layer 2
/// Ground, // Layer 3
/// }
///
/// // The first bit is reserved for the default layer.
/// assert_eq!(GameLayer::default().to_bits(), 1 << 0);
///
/// // The `GameLayer::Ground` layer is the fourth layer, so its bit value is `1 << 3`.
/// assert_eq!(GameLayer::Ground.to_bits(), 1 << 3);
///
/// // Here, `GameLayer::Enemy` is automatically converted to a `LayerMask` for the comparison.
/// assert_eq!(LayerMask(0b00100), GameLayer::Enemy);
/// ```
Expand Down Expand Up @@ -269,8 +280,9 @@ impl Not for LayerMask {
#[cfg_attr(feature = "2d", doc = "# use avian2d::prelude::*;")]
#[cfg_attr(feature = "3d", doc = "# use avian3d::prelude::*;")]
/// #
/// #[derive(PhysicsLayer)]
/// #[derive(PhysicsLayer, Default)]
/// enum GameLayer {
/// #[default]
/// Default, // Layer 0 - the default layer that objects are assigned to
/// Player, // Layer 1
/// Enemy, // Layer 2
Expand Down Expand Up @@ -430,8 +442,9 @@ mod tests {

use crate::prelude::*;

#[derive(PhysicsLayer)]
#[derive(PhysicsLayer, Default)]
enum GameLayer {
#[default]
Default,
Player,
Enemy,
Expand Down

0 comments on commit 89b733e

Please sign in to comment.