From 494cdce42ee00a9d700ed4ecb2be1663dc2696cc Mon Sep 17 00:00:00 2001 From: Elijah Voigt Date: Thu, 16 Jul 2026 12:30:27 -0700 Subject: [PATCH] Default action_kind to instant; document multiple action sets - The #[action_kind(..)] attribute is now optional and defaults to instant (the most common kind), so bare enum variants are instant actions. - Document the multiple-plugin pattern: add one ActionsPlugin:: per action enum (gameplay/menu/audio/debug) for separation of concerns, each with its own independent ActionState/InputMap/systems, plus the run_if(in_state(..)) approach for mutually-exclusive sets. Co-Authored-By: Claude Opus 4.8 --- engine/macros/src/lib.rs | 29 ++++++++++++----------------- engine/src/actions.rs | 39 +++++++++++++++++++++++++++++++++------ 2 files changed, 45 insertions(+), 23 deletions(-) diff --git a/engine/macros/src/lib.rs b/engine/macros/src/lib.rs index 781fdbc..f22a32b 100644 --- a/engine/macros/src/lib.rs +++ b/engine/macros/src/lib.rs @@ -12,16 +12,17 @@ use syn::{spanned::Spanned, Data, DeriveInput, Fields, Ident, Path}; /// Derives `engine::GameAction` (and `engine::GameActionHandlers`) for a /// fieldless enum. /// -/// Each variant must be annotated with `#[action_kind(...)]` — one of `instant`, -/// `toggle`, `axis`, or `scalar` — and may carry any number of -/// `#[action_handler(path::to::system)]` attributes: +/// Each variant may carry `#[action_kind(...)]` — one of `instant`, `toggle`, +/// `axis`, or `scalar` — and any number of `#[action_handler(path::to::system)]` +/// attributes. `#[action_kind]` is optional and **defaults to `instant`**, so +/// bare variants are instant actions: /// /// ```ignore /// #[derive(Copy, Clone, Eq, PartialEq, Hash, GameAction)] /// enum Action { -/// #[action_kind(instant)] #[action_handler(jump)] -/// Jump, -/// #[action_kind(axis)] #[action_handler(move_player)] // fn(In, ..) +/// #[action_handler(jump)] +/// Jump, // instant (default) +/// #[action_kind(axis)] #[action_handler(move_player)] // fn(In, ..) /// Move, /// } /// ``` @@ -163,7 +164,8 @@ fn gen_handler( } } -/// Parses the single required `#[action_kind(...)]` attribute on a variant. +/// Parses the `#[action_kind(...)]` attribute on a variant, defaulting to +/// [`Kind::Instant`] when it is omitted (the common case). fn parse_kind(variant: &syn::Variant) -> syn::Result { let mut kind: Option = None; @@ -207,16 +209,9 @@ fn parse_kind(variant: &syn::Variant) -> syn::Result { }); } - kind.ok_or_else(|| { - syn::Error::new( - variant.ident.span(), - format!( - "action variant `{0}` is missing its kind; add one of \ - `#[action_kind(instant|toggle|axis|scalar)]`, e.g. `#[action_kind(instant)] {0}`", - variant.ident - ), - ) - }) + // A bare variant is an instant action — the most common kind — so the + // attribute is optional and only needed to pick a non-default kind. + Ok(kind.unwrap_or(Kind::Instant)) } /// Parses every `#[action_handler(path)]` attribute on a variant (zero or more). diff --git a/engine/src/actions.rs b/engine/src/actions.rs index c3abdc2..ac4d777 100644 --- a/engine/src/actions.rs +++ b/engine/src/actions.rs @@ -8,11 +8,11 @@ //! use engine::actions::*; //! use engine::*; // bevy prelude //! -//! // 1. Your game's actions, with each variant's kind declared inline. +//! // 1. Your game's actions. `#[action_kind]` defaults to `instant`, so a bare +//! // variant is an instant action; annotate only the others. //! #[derive(Copy, Clone, Eq, PartialEq, Hash, GameAction)] //! enum Action { -//! #[action_kind(instant)] // fires for one frame -//! Jump, +//! Jump, // instant (the default — no attribute needed) //! #[action_kind(toggle)] // stays on/off //! Grid, //! #[action_kind(axis)] // carries a Vec2 @@ -105,6 +105,32 @@ //! axis/scalar. [`ActionsPlugin`] registers them when you add it. //! * **[`AppActionExt::on_action`]** — `app.on_action(Update, Action::Jump, jump)`, //! the explicit, no-macro form that keeps registration visible in `main`. +//! +//! ## Multiple action sets +//! +//! The whole API is generic over the action type, so a game is **not** limited to +//! one enum. Add an [`ActionsPlugin`] per set to separate concerns — gameplay, +//! menu, audio, debug — each with its own bindings and handlers: +//! +//! ```rust,ignore +//! app.add_plugins(ActionsPlugin::::new().bind(/* .. */)) +//! .add_plugins(ActionsPlugin::::new().bind(/* .. */)) +//! .add_plugins(ActionsPlugin::::new().bind(/* .. */)); +//! ``` +//! +//! Each set gets its **own** independent [`ActionState`], [`InputMap`], +//! fan-in systems, and observers; [`on_action`], [`action_value`], and +//! [`ActionSource`] are all keyed by the set's type, so sets never collide. +//! (Deriving [`GameAction`](macro@GameAction) generates a distinct set of impls +//! per enum, and each `ActionsPlugin::` is a distinct plugin type, so adding +//! several is fine.) +//! +//! Bindings are read **independently** per set — there is no cross-set input +//! consumption or priority — which is exactly right for concerns that are +//! genuinely separate (audio vs debug vs gameplay). To make two sets *mutually +//! exclusive* (menu vs gameplay), gate their handlers with `run_if(in_state(..))`: +//! Bevy states are already exclusive, so only the active set's handlers run, even +//! when the same key is bound in both. use core::hash::Hash; use core::marker::PhantomData; @@ -201,13 +227,14 @@ impl ActionValue { } /// Derive macro for [`GameAction`]: annotate each variant with -/// `#[action_kind(...)]` — `instant`, `toggle`, `axis`, or `scalar`. +/// `#[action_kind(...)]` — `instant`, `toggle`, `axis`, or `scalar`. The +/// attribute is optional and defaults to `instant`, so bare variants are instant +/// actions. /// /// ```rust,ignore /// #[derive(Copy, Clone, Eq, PartialEq, Hash, GameAction)] /// enum Action { -/// #[action_kind(instant)] -/// Jump, +/// Jump, // instant (default) /// #[action_kind(toggle)] /// Grid, /// }