diff --git a/engine/Cargo.lock b/engine/Cargo.lock index 0f87490..ece6462 100644 --- a/engine/Cargo.lock +++ b/engine/Cargo.lock @@ -2413,6 +2413,17 @@ name = "engine" version = "0.1.0" dependencies = [ "bevy", + "engine_macros", +] + +[[package]] +name = "engine_macros" +version = "0.1.0" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", ] [[package]] diff --git a/engine/Cargo.toml b/engine/Cargo.toml index 440ea7c..edc980b 100644 --- a/engine/Cargo.toml +++ b/engine/Cargo.toml @@ -5,6 +5,7 @@ edition = "2024" [dependencies] bevy = "0.19.0" +engine_macros = { path = "macros" } [features] dev = [ diff --git a/engine/macros/Cargo.toml b/engine/macros/Cargo.toml new file mode 100644 index 0000000..bb3cb59 --- /dev/null +++ b/engine/macros/Cargo.toml @@ -0,0 +1,13 @@ +[package] +name = "engine_macros" +version = "0.1.0" +edition = "2024" + +[lib] +proc-macro = true + +[dependencies] +syn = "2" +quote = "1" +proc-macro2 = "1" +proc-macro-crate = "3" diff --git a/engine/macros/src/lib.rs b/engine/macros/src/lib.rs new file mode 100644 index 0000000..8ac039e --- /dev/null +++ b/engine/macros/src/lib.rs @@ -0,0 +1,137 @@ +//! Proc-macros for the `engine` crate. +//! +//! Currently provides [`GameAction`](macro@GameAction), a derive that annotates +//! each enum variant with its [`ActionKind`] via `#[action_kind(...)]`. + +use proc_macro::TokenStream; +use proc_macro_crate::{crate_name, FoundCrate}; +use quote::{format_ident, quote}; +use syn::{spanned::Spanned, Data, DeriveInput, Fields, Ident}; + +/// Derives `engine::GameAction` for a fieldless enum. +/// +/// Each variant must be annotated with `#[action_kind(instant)]`, +/// `#[action_kind(toggle)]`, or `#[action_kind(axis)]`: +/// +/// ```ignore +/// #[derive(Copy, Clone, Eq, PartialEq, Hash, GameAction)] +/// enum Action { +/// #[action_kind(instant)] +/// Jump, +/// #[action_kind(toggle)] +/// Grid, +/// } +/// ``` +/// +/// This expands to an `impl GameAction for Action` whose `kind` matches each +/// variant to its declared [`ActionKind`]. +#[proc_macro_derive(GameAction, attributes(action_kind))] +pub fn derive_game_action(input: TokenStream) -> TokenStream { + let input = syn::parse_macro_input!(input as DeriveInput); + expand(input) + .unwrap_or_else(syn::Error::into_compile_error) + .into() +} + +fn expand(input: DeriveInput) -> syn::Result { + let Data::Enum(data) = &input.data else { + return Err(syn::Error::new( + input.ident.span(), + "`GameAction` can only be derived for enums", + )); + }; + + let engine = engine_path(); + let ident = &input.ident; + let (impl_generics, ty_generics, where_clause) = input.generics.split_for_impl(); + + let arms = data + .variants + .iter() + .map(|variant| { + if !matches!(variant.fields, Fields::Unit) { + return Err(syn::Error::new( + variant.fields.span(), + "`GameAction` variants must be fieldless (unit variants)", + )); + } + let variant_ident = &variant.ident; + let kind = parse_kind(variant, &engine)?; + Ok(quote!(Self::#variant_ident => #kind)) + }) + .collect::>>()?; + + Ok(quote! { + impl #impl_generics #engine::GameAction for #ident #ty_generics #where_clause { + fn kind(self) -> #engine::ActionKind { + match self { + #(#arms,)* + } + } + } + }) +} + +/// Parses the single required `#[action_kind(instant|toggle)]` attribute on a +/// variant into the corresponding `ActionKind` expression. +fn parse_kind( + variant: &syn::Variant, + engine: &proc_macro2::TokenStream, +) -> syn::Result { + let mut kind: Option = None; + + for attr in &variant.attrs { + if !attr.path().is_ident("action_kind") { + continue; + } + if kind.is_some() { + return Err(syn::Error::new( + attr.span(), + "duplicate `#[action_kind(...)]` attribute", + )); + } + let variant_name: Ident = attr.parse_args().map_err(|_| { + syn::Error::new( + attr.span(), + "expected `#[action_kind(instant)]` or `#[action_kind(toggle)]`", + ) + })?; + kind = Some(match variant_name.to_string().as_str() { + "instant" => quote!(#engine::ActionKind::Instant), + "toggle" => quote!(#engine::ActionKind::Toggle), + "axis" => quote!(#engine::ActionKind::Axis), + other => { + return Err(syn::Error::new( + variant_name.span(), + format!( + "unknown action kind `{other}`, expected `instant`, `toggle`, or `axis`" + ), + )) + } + }); + } + + kind.ok_or_else(|| { + syn::Error::new( + variant.ident.span(), + format!( + "variant `{}` needs `#[action_kind(instant)]` or `#[action_kind(toggle)]`", + variant.ident + ), + ) + }) +} + +/// Resolves the path to the `engine` crate so the derive works whether it is +/// invoked from a game, from `engine`'s own tests, or under a renamed dependency. +fn engine_path() -> proc_macro2::TokenStream { + match crate_name("engine") { + Ok(FoundCrate::Itself) => quote!(crate), + Ok(FoundCrate::Name(name)) => { + let ident = format_ident!("{name}"); + quote!(::#ident) + } + // Fallback: assume the conventional crate name. + Err(_) => quote!(::engine), + } +} diff --git a/engine/src/actions.rs b/engine/src/actions.rs new file mode 100644 index 0000000..5b3dbbe --- /dev/null +++ b/engine/src/actions.rs @@ -0,0 +1,563 @@ +//! Abstract, remappable game actions that unify keyboard, gamepad, and UI input. +//! +//! Games define their own action enum, then either gate a system on an action +//! ([`on_action`]) or pipe an action's payload into one ([`axis_value`]) — +//! regardless of whether the trigger was a keyboard, a gamepad, or a UI widget: +//! +//! ```rust,ignore +//! use engine::actions::*; +//! use engine::*; // bevy prelude +//! +//! // 1. Your game's actions, with each variant's kind declared inline. +//! #[derive(Copy, Clone, Eq, PartialEq, Hash, GameAction)] +//! enum Action { +//! #[action_kind(instant)] // fires for one frame +//! Jump, +//! #[action_kind(toggle)] // stays on/off +//! Grid, +//! #[action_kind(axis)] // carries a Vec2 +//! Move, +//! } +//! +//! App::new() +//! .add_plugins(DefaultPlugins) +//! .add_plugins( +//! ActionsPlugin::::new() +//! .bind(Action::Jump, [ +//! Binding::Key(KeyCode::Space), +//! Binding::Gamepad(GamepadButton::South), +//! ]) +//! .bind(Action::Grid, [Binding::Key(KeyCode::KeyG)]) +//! .bind(Action::Move, [Binding::Stick(Stick::Left)]), +//! ) +//! // The two entry points are symmetrical and take an action of ANY kind: +//! // `run_if(on_action(a))` gates a system; `action_value(a).pipe(sys)` feeds it. +//! .add_systems(Update, jump.run_if(on_action(Action::Jump))) +//! .add_systems(Update, draw_grid.run_if(on_action(Action::Grid))) +//! .add_systems(Update, action_value(Action::Move).pipe(move_player)); +//! +//! // Piped consumer: receives an `ActionValue` and never touches `ActionState`. +//! fn move_player(In(value): In, mut q: Query<&mut Transform, With>) { +//! let direction = value.as_axis(); // magnitude + direction +//! for mut t in &mut q { +//! t.translation += direction.extend(0.0); +//! } +//! } +//! +//! // 2. UI feeds the same actions: tag a widget with the action it drives. +//! // commands.spawn((button(..), ActionSource(Action::Jump))); // Activate -> Instant +//! // commands.spawn((checkbox(..), ActionSource(Action::Grid))); // ValueChange -> Toggle +//! ``` +//! +//! ## How it works +//! +//! All input sources fan in to a single [`ActionState`] resource. Instant +//! actions live in a momentary set cleared every frame; toggles live in a +//! persistent set; axes hold a per-frame [`Vec2`]. [`ActionState::value`] reads +//! whichever bucket the action's kind selects and returns a unified +//! [`ActionValue`], so every downstream read is kind-agnostic. +//! +//! ## Two symmetrical entry points +//! +//! Whatever action you can gate on, you can also receive the value of. The pair +//! differs only in *how the system gets involved*, not in which actions it accepts: +//! +//! * **`sys.run_if(on_action(a))`** — gate a system on the action. A run condition +//! can only return `bool`, so this reports [`ActionValue::is_active`]: `true` for +//! a fired instant, an on toggle, **or a deflected axis**. Reach for it when the +//! fact that the action is active is all the system needs — including "run every +//! frame the stick is being moved". +//! * **`action_value(a).pipe(sys)`** — deliver the action's [`ActionValue`] into a +//! system that takes `In`. Reach for it when the system needs the +//! **payload** (the axis [`Vec2`], or a toggle's bool to store). The consumer +//! pulls its type out with [`ActionValue::as_axis`] / [`ActionValue::as_bool`], +//! or use the typed [`axis_value`] / [`toggle_value`] helpers to pipe a bare +//! `Vec2` / `bool`. +//! +//! They compose — `action_value(a).pipe(sys).run_if(on_action(a))` delivers the +//! payload *and* skips the system when the action is inactive — and neither is +//! mandatory: any system may read [`ActionState`] directly. + +use core::hash::Hash; +use core::marker::PhantomData; + +use bevy::{ + platform::collections::{HashMap, HashSet}, + prelude::*, + ui::Checked, + ui_widgets::{Activate, Checkbox, SetChecked, ValueChange}, +}; + +/// How an action behaves: momentary, a persistent on/off toggle, or an analog +/// 2D axis (e.g. a gamepad stick). +#[derive(Copy, Clone, Debug, PartialEq, Eq)] +pub enum ActionKind { + /// Fires for a single frame each time it is triggered (jump, shoot, confirm). + Instant, + /// Holds an on/off state until toggled again (show grid, mute, pause). + Toggle, + /// Carries a [`Vec2`] direction + magnitude, refreshed every frame (movement, + /// aiming). Read the value with [`ActionState::axis`]. + Axis, +} + +/// The current value of an action, unified across kinds so one provider +/// ([`action_value`]) and one run condition ([`on_action`]) work for every kind. +/// +/// Digital kinds ([`Instant`](ActionKind::Instant)/[`Toggle`](ActionKind::Toggle)) +/// produce [`Bool`](Self::Bool); [`Axis`](ActionKind::Axis) produces +/// [`Axis`](Self::Axis). A consumer that knows the kind it wants pulls the payload +/// out with [`as_bool`](Self::as_bool) or [`as_axis`](Self::as_axis). +#[derive(Copy, Clone, Debug, PartialEq)] +pub enum ActionValue { + /// A digital value: an instant action fired this frame, or a toggle is on. + Bool(bool), + /// An analog 2D value. + Axis(Vec2), +} + +impl ActionValue { + /// Whether the action is "active": a `true` bool, or a non-zero axis. This is + /// what [`on_action`] gates on, so a system can run "while the stick is moved" + /// exactly as it runs "while the toggle is on". + pub fn is_active(self) -> bool { + match self { + ActionValue::Bool(b) => b, + ActionValue::Axis(v) => v != Vec2::ZERO, + } + } + + /// The digital payload. An axis collapses to whether it is deflected, so a + /// bool consumer can read an axis action too. + pub fn as_bool(self) -> bool { + self.is_active() + } + + /// The analog payload. A digital value maps to [`Vec2::ZERO`] when off; a + /// `true` bool has no direction and also yields [`Vec2::ZERO`]. + pub fn as_axis(self) -> Vec2 { + match self { + ActionValue::Axis(v) => v, + ActionValue::Bool(_) => Vec2::ZERO, + } + } +} + +/// Derive macro for [`GameAction`]: annotate each variant with +/// `#[action_kind(instant)]` or `#[action_kind(toggle)]`. +/// +/// ```rust,ignore +/// #[derive(Copy, Clone, Eq, PartialEq, Hash, GameAction)] +/// enum Action { +/// #[action_kind(instant)] +/// Jump, +/// #[action_kind(toggle)] +/// Grid, +/// } +/// ``` +pub use engine_macros::GameAction; + +/// Implemented by a game's action enum to describe each action's behavior. +/// +/// Usually derived — see the [`GameAction`](macro@GameAction) derive macro. +/// +/// The bounds mirror [`ButtonInput`]'s key type: actions must be cheap, hashable +/// identifiers. `kind` is the only thing the engine needs from the game. +pub trait GameAction: Copy + Eq + Hash + Send + Sync + 'static { + /// Returns whether this action is [`Instant`](ActionKind::Instant), + /// [`Toggle`](ActionKind::Toggle), or [`Axis`](ActionKind::Axis). + fn kind(self) -> ActionKind; +} + +/// A single physical input that can trigger an action. Extend as needed. +/// +/// [`Key`](Self::Key), [`Mouse`](Self::Mouse) and [`Gamepad`](Self::Gamepad) are +/// digital (for [`Instant`](ActionKind::Instant)/[`Toggle`](ActionKind::Toggle) +/// actions); [`Stick`](Self::Stick) is analog (for [`Axis`](ActionKind::Axis) +/// actions). +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Reflect)] +pub enum Binding { + /// A keyboard key. + Key(KeyCode), + /// A mouse button. + Mouse(MouseButton), + /// A gamepad button (matched across all connected gamepads). + Gamepad(GamepadButton), + /// A gamepad analog source producing a [`Vec2`] (matched across all connected + /// gamepads; deadzone already applied by Bevy). + Stick(Stick), +} + +/// An analog 2D source on a gamepad. +#[derive(Copy, Clone, Debug, PartialEq, Eq, Hash, Reflect)] +pub enum Stick { + /// The left analog stick. + Left, + /// The right analog stick. + Right, + /// The directional pad, reported as a [`Vec2`]. + DPad, +} + +/// Remappable map from actions to the physical inputs that trigger them. +/// +/// This is a [`Resource`]; mutate it at runtime to rebind controls (e.g. from a +/// settings menu). One action may have many bindings and they are additive. +#[derive(Resource)] +pub struct InputMap { + bindings: HashMap>, +} + +impl Default for InputMap { + fn default() -> Self { + Self { + bindings: HashMap::default(), + } + } +} + +impl Clone for InputMap { + fn clone(&self) -> Self { + Self { + bindings: self.bindings.clone(), + } + } +} + +impl InputMap { + /// Adds one or more bindings for `action`. Additive: existing bindings are + /// kept, so repeated calls accumulate. + pub fn bind(&mut self, action: T, bindings: impl IntoIterator) -> &mut Self { + self.bindings.entry(action).or_default().extend(bindings); + self + } + + /// Replaces all bindings for `action` with `bindings` (for rebinding UIs). + pub fn set_bindings(&mut self, action: T, bindings: impl IntoIterator) { + self.bindings.insert(action, bindings.into_iter().collect()); + } + + /// The bindings currently mapped to `action`, if any. + pub fn bindings(&self, action: T) -> Option<&[Binding]> { + self.bindings.get(&action).map(Vec::as_slice) + } +} + +/// The unified, source-agnostic state of every action for the current frame. +/// +/// Populated by the engine each frame from the [`InputMap`] and UI widgets. +/// Read it kind-agnostically via [`value`](Self::value) (behind [`on_action`] and +/// [`action_value`]), or reach for a specific bucket with [`axis`](Self::axis) / +/// [`enabled`](Self::enabled). Games may also drive actions programmatically with +/// [`press`](Self::press), [`set`](Self::set), and [`toggle`](Self::toggle). +#[derive(Resource)] +pub struct ActionState { + /// Instant actions active this frame; cleared at the start of every frame. + momentary: HashSet, + /// Toggle actions currently switched on; persists across frames. + enabled: HashSet, + /// Axis actions and their current value; refreshed every frame. + axes: HashMap, +} + +impl Default for ActionState { + fn default() -> Self { + Self { + momentary: HashSet::default(), + enabled: HashSet::default(), + axes: HashMap::default(), + } + } +} + +impl ActionState { + /// The current [`ActionValue`] of `action`, dispatched by its + /// [`ActionKind`]: [`Bool`](ActionValue::Bool) for instant/toggle, + /// [`Axis`](ActionValue::Axis) for axes. + /// + /// This is the single accessor every other read is built on, so one provider + /// ([`action_value`]) and one run condition ([`on_action`]) serve all kinds. + pub fn value(&self, action: T) -> ActionValue { + match action.kind() { + ActionKind::Instant => ActionValue::Bool(self.momentary.contains(&action)), + ActionKind::Toggle => ActionValue::Bool(self.enabled.contains(&action)), + ActionKind::Axis => ActionValue::Axis(self.axis(action)), + } + } + + /// Whether `action` is currently active: an [`Instant`](ActionKind::Instant) + /// action fired this frame, a [`Toggle`](ActionKind::Toggle) that is on, or an + /// [`Axis`](ActionKind::Axis) that is deflected (non-zero). + /// + /// Shorthand for `self.value(action).is_active()`; what [`on_action`] gates on. + pub fn active(&self, action: T) -> bool { + self.value(action).is_active() + } + + /// Whether a toggle `action` is currently switched on. + pub fn enabled(&self, action: T) -> bool { + self.enabled.contains(&action) + } + + /// The current value of an axis `action` (its direction and magnitude), or + /// [`Vec2::ZERO`] if it has no value this frame. + pub fn axis(&self, action: T) -> Vec2 { + self.axes.get(&action).copied().unwrap_or(Vec2::ZERO) + } + + /// Fires an instant `action` for this frame. + pub fn press(&mut self, action: T) { + self.momentary.insert(action); + } + + /// Sets a toggle `action` to `on`. + pub fn set(&mut self, action: T, on: bool) { + if on { + self.enabled.insert(action); + } else { + self.enabled.remove(&action); + } + } + + /// Flips a toggle `action`. + pub fn toggle(&mut self, action: T) { + if !self.enabled.remove(&action) { + self.enabled.insert(action); + } + } + + /// Sets an axis `action`'s value for this frame. + pub fn set_axis(&mut self, action: T, value: Vec2) { + self.axes.insert(action, value); + } +} + +/// Marker component linking a UI widget to the action it drives. +/// +/// Put it on a `Button` ([`Instant`](ActionKind::Instant)) or `Checkbox` +/// ([`Toggle`](ActionKind::Toggle)) — the engine wires the widget's events into +/// [`ActionState`] and keeps checkboxes visually in sync. +#[derive(Component, Copy, Clone, Debug)] +pub struct ActionSource(pub T); + +/// The two symmetrical entry points below let any system consume any action: +/// +/// * gate a system on it — [`on_action`] (a run condition, so bool-only), or +/// * receive its value — [`action_value`] (a pipe provider carrying the payload). +/// +/// Both accept an action of any [`ActionKind`]; the value type is unified by +/// [`ActionValue`]. + +/// Run condition that is active while `action` is active, for **any** kind: a +/// `true` bool (instant fired / toggle on) or a non-zero axis (stick deflected). +/// +/// So the same call gates "run while paused" and "run while the stick is moved". +/// +/// ```rust,ignore +/// app.add_systems(Update, jump.run_if(on_action(Action::Jump))); +/// app.add_systems(Update, camera_follow.run_if(on_action(Action::Move))); // stick moved +/// ``` +pub fn on_action(action: T) -> impl FnMut(Res>) -> bool + Clone { + move |state: Res>| state.active(action) +} + +/// Provider for [`pipe`](bevy::prelude::IntoSystem::pipe)ing an action's +/// [`ActionValue`] into a focused consumer, for **any** kind. The consumer takes +/// `In` and never touches [`ActionState`]. +/// +/// This is the pipe-side twin of [`on_action`]: whatever action you can gate on, +/// you can also deliver. +/// +/// ```rust,ignore +/// app.add_systems(Update, action_value(Action::Move).pipe(move_player)); +/// app.add_systems(Update, action_value(Action::Grid).pipe(set_grid)); +/// +/// fn move_player(In(v): In, mut q: Query<&mut Transform, With>) { +/// let dir = v.as_axis(); +/// for mut t in &mut q { t.translation += dir.extend(0.0); } +/// } +/// +/// fn set_grid(In(v): In, mut grid: ResMut) { +/// grid.visible = v.as_bool(); +/// } +/// ``` +/// +/// Compose with [`on_action`] to also skip the consumer when inactive: +/// `action_value(a).pipe(sys).run_if(on_action(a))`. +pub fn action_value( + action: T, +) -> impl FnMut(Res>) -> ActionValue + Clone { + move |state: Res>| state.value(action) +} + +/// Typed convenience over [`action_value`] for axis actions: pipes a bare +/// [`Vec2`] instead of an [`ActionValue`], so the consumer takes `In`. +pub fn axis_value(action: T) -> impl FnMut(Res>) -> Vec2 + Clone { + move |state: Res>| state.axis(action) +} + +/// Typed convenience over [`action_value`] for toggle actions: pipes a bare +/// `bool`, so the consumer takes `In`. +pub fn toggle_value(action: T) -> impl FnMut(Res>) -> bool + Clone { + move |state: Res>| state.enabled(action) +} + +// --- fan-in systems ------------------------------------------------------- + +/// Clears momentary state at the start of each frame so instant actions have +/// just-pressed semantics. Toggle state is left untouched. +fn clear_momentary(mut state: ResMut>) { + if !state.momentary.is_empty() { + state.momentary.clear(); + } +} + +/// Hardware fan-in: reads keyboard, mouse, and gamepads, consults the +/// [`InputMap`], and applies each triggered action according to its kind. +fn hardware_to_actions( + keys: Res>, + mouse: Res>, + pads: Query<&Gamepad>, + map: Res>, + mut state: ResMut>, +) { + for (&action, bindings) in &map.bindings { + match action.kind() { + // Analog: pick the most-deflected reading across sticks/gamepads and + // store it every frame (zero when nothing is connected or centered). + ActionKind::Axis => { + let mut value = Vec2::ZERO; + for binding in bindings { + let Binding::Stick(stick) = binding else { + continue; + }; + for pad in &pads { + let reading = match stick { + Stick::Left => pad.left_stick(), + Stick::Right => pad.right_stick(), + Stick::DPad => pad.dpad(), + }; + if reading.length_squared() > value.length_squared() { + value = reading; + } + } + } + state.set_axis(action, value); + } + // Digital: `break` on the first firing binding avoids double-toggling + // when two bound inputs are pressed on the same frame. + kind @ (ActionKind::Instant | ActionKind::Toggle) => { + for binding in bindings { + let just_pressed = match binding { + Binding::Key(k) => keys.just_pressed(*k), + Binding::Mouse(m) => mouse.just_pressed(*m), + Binding::Gamepad(g) => pads.iter().any(|pad| pad.just_pressed(*g)), + Binding::Stick(_) => false, // not meaningful for digital actions + }; + if just_pressed { + match kind { + ActionKind::Instant => state.press(action), + _ => state.toggle(action), + } + break; + } + } + } + } + } +} + +/// UI fan-in: a `Button`'s [`Activate`] drives an instant action. +fn button_to_action( + activate: On, + sources: Query<&ActionSource>, + mut state: ResMut>, +) { + if let Ok(ActionSource(action)) = sources.get(activate.entity) { + match action.kind() { + ActionKind::Instant => state.press(*action), + // A button bound to a toggle acts as a "flip" control (menu item). + ActionKind::Toggle => state.toggle(*action), + // A button can't produce an analog value; nothing to do. + ActionKind::Axis => {} + } + } +} + +/// UI fan-in: a `Checkbox`'s [`ValueChange`] sets a toggle action's on/off state. +fn checkbox_to_action( + change: On>, + sources: Query<&ActionSource>, + mut state: ResMut>, +) { + if let Ok(ActionSource(action)) = sources.get(change.source) { + if let ActionKind::Toggle = action.kind() { + state.set(*action, change.value); + } + } +} + +/// State -> UI sync: pushes toggle state back onto checkboxes so a keyboard or +/// gamepad toggle also moves the checkmark. Runs only when state changed. +fn sync_checkboxes( + state: Res>, + checkboxes: Query<(Entity, &ActionSource, Has), With>, + mut commands: Commands, +) { + if !state.is_changed() { + return; + } + for (entity, ActionSource(action), is_checked) in &checkboxes { + if let ActionKind::Toggle = action.kind() { + let want = state.enabled(*action); + if want != is_checked { + commands.trigger(SetChecked { entity, checked: want }); + } + } + } +} + +/// Registers everything needed for action `T`: the [`ActionState`] and +/// [`InputMap`] resources, the hardware/UI fan-in, and checkbox syncing. +/// +/// Build it with [`new`](Self::new) and [`bind`](Self::bind); one instance per +/// action type. +pub struct ActionsPlugin { + map: InputMap, + _marker: PhantomData, +} + +impl Default for ActionsPlugin { + fn default() -> Self { + Self { + map: InputMap::default(), + _marker: PhantomData, + } + } +} + +impl ActionsPlugin { + /// Creates a plugin with no bindings. + pub fn new() -> Self { + Self::default() + } + + /// Adds one or more default bindings for `action` (chainable). Games can + /// still rebind at runtime by mutating the [`InputMap`] resource. + pub fn bind(mut self, action: T, bindings: impl IntoIterator) -> Self { + self.map.bind(action, bindings); + self + } +} + +impl Plugin for ActionsPlugin { + fn build(&self, app: &mut App) { + app.init_resource::>() + .insert_resource(self.map.clone()) + .add_systems(First, clear_momentary::) + .add_systems(PreUpdate, hardware_to_actions::) + .add_systems(Update, sync_checkboxes::) + .add_observer(button_to_action::) + .add_observer(checkbox_to_action::); + } +} diff --git a/engine/src/lib.rs b/engine/src/lib.rs index be52f48..c0b68f7 100644 --- a/engine/src/lib.rs +++ b/engine/src/lib.rs @@ -1,3 +1,6 @@ +pub mod actions; +pub use actions::*; + pub mod color; pub use color::*; diff --git a/life/Cargo.lock b/life/Cargo.lock index 40618cc..a23cc6e 100644 --- a/life/Cargo.lock +++ b/life/Cargo.lock @@ -2383,6 +2383,17 @@ name = "engine" version = "0.1.0" dependencies = [ "bevy", + "engine_macros", +] + +[[package]] +name = "engine_macros" +version = "0.1.0" +dependencies = [ + "proc-macro-crate", + "proc-macro2", + "quote", + "syn", ] [[package]]