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::<T> 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 <noreply@anthropic.com>
main
Elijah Voigt 6 days ago
parent cf3a81f4bd
commit 494cdce42e

@ -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<Vec2>, ..)
/// #[action_handler(jump)]
/// Jump, // instant (default)
/// #[action_kind(axis)] #[action_handler(move_player)] // fn(In<Vec2>, ..)
/// 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<Kind> {
let mut kind: Option<Kind> = None;
@ -207,16 +209,9 @@ fn parse_kind(variant: &syn::Variant) -> syn::Result<Kind> {
});
}
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).

@ -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<T>`] per set to separate concerns — gameplay,
//! menu, audio, debug — each with its own bindings and handlers:
//!
//! ```rust,ignore
//! app.add_plugins(ActionsPlugin::<GameplayAction>::new().bind(/* .. */))
//! .add_plugins(ActionsPlugin::<MenuAction>::new().bind(/* .. */))
//! .add_plugins(ActionsPlugin::<AudioAction>::new().bind(/* .. */));
//! ```
//!
//! Each set gets its **own** independent [`ActionState<T>`], [`InputMap<T>`],
//! fan-in systems, and observers; [`on_action`], [`action_value`], and
//! [`ActionSource<T>`] 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::<T>` 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,
/// }

Loading…
Cancel
Save