//! 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, //! #[action_kind(scalar)] // carries an f32, from a slider //! Volume, //! } //! //! 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)) //! // Typed pipe: deliver the slider's f32, only on the frame it changes. //! .add_systems(Update, //! scalar_value(Action::Volume).pipe(set_volume).run_if(on_action(Action::Volume))); //! //! // 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); //! } //! } //! //! fn set_volume(In(v): In, mut audio: ResMut) { audio.volume = v; } //! //! // 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 //! // commands.spawn((slider(..), ActionSource(Action::Volume))); // ValueChange -> Scalar //! ``` //! //! ## 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`]; scalars (sliders) hold a //! persistent [`f32`]. [`ActionState::value`] reads whichever bucket the action's //! kind selects and returns a unified [`ActionValue`], so every downstream read is //! kind-agnostic. //! //! Kinds split by *trigger style*: [`Instant`](ActionKind::Instant) and //! [`Scalar`](ActionKind::Scalar) are **edge-triggered** (they fire on the frame //! they happen — a press, a slider change), while [`Toggle`](ActionKind::Toggle) //! and [`Axis`](ActionKind::Axis) are **level-triggered** (they report a state //! that is currently true). [`on_action`] reflects that per kind. //! //! ## 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 [`ActionState::active`]: an instant //! fired / a slider changed **this frame** (edge), or a toggle is on / an axis is //! deflected (level). Reach for it when the fact that the action is active is all //! the system needs — "run while the stick is moved", "run when the slider moves". //! * **`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`], the slider [`f32`], a toggle's bool). The //! consumer pulls its type out with [`ActionValue::as_axis`] / //! [`ActionValue::as_scalar`] / [`ActionValue::as_bool`], or use the typed //! [`axis_value`] / [`scalar_value`] / [`toggle_value`] helpers to pipe a bare //! `Vec2` / `f32` / `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. /// /// The kinds split two ways by *trigger style*: [`Instant`](Self::Instant) and /// [`Scalar`](Self::Scalar) are **edge-triggered** (they fire on the frame /// something happens), while [`Toggle`](Self::Toggle) and [`Axis`](Self::Axis) are /// **level-triggered** (they report a state that is currently true). This is what /// [`on_action`] reflects per kind. #[derive(Copy, Clone, Debug, PartialEq, Eq)] pub enum ActionKind { /// Fires for a single frame each time it is triggered (jump, shoot, confirm). /// Edge-triggered. Instant, /// Holds an on/off state until toggled again (show grid, mute, pause). /// Level-triggered. Toggle, /// Carries a [`Vec2`] direction + magnitude, refreshed every frame (movement, /// aiming). Level-triggered. Read the value with [`ActionState::axis`]. Axis, /// Carries an `f32`, emitted when a slider changes. Edge-triggered like /// [`Instant`](Self::Instant) but value-carrying like [`Axis`](Self::Axis). /// Read the value with [`ActionState::scalar`]. Scalar, } /// 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); [`Scalar`](ActionKind::Scalar) produces /// [`Scalar`](Self::Scalar). A consumer that knows the kind it wants pulls the /// payload out with [`as_bool`](Self::as_bool), [`as_axis`](Self::as_axis), or /// [`as_scalar`](Self::as_scalar). #[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), /// An analog 1D value (e.g. a slider position). Scalar(f32), } impl ActionValue { /// Whether the value is "non-zero": a `true` bool, a non-zero axis, or a /// non-zero scalar. Note this is a property of the *value* — for the /// edge-triggered kinds ([`Instant`](ActionKind::Instant)/[`Scalar`](ActionKind::Scalar)), /// [`on_action`] instead reports "happened this frame" via /// [`ActionState::active`]. pub fn is_active(self) -> bool { match self { ActionValue::Bool(b) => b, ActionValue::Axis(v) => v != Vec2::ZERO, ActionValue::Scalar(v) => v != 0.0, } } /// The digital payload. Analog values collapse to whether they are non-zero, /// so a bool consumer can read an axis or scalar action too. pub fn as_bool(self) -> bool { self.is_active() } /// The 2D analog payload. Non-axis values yield [`Vec2::ZERO`] (a `true` bool /// or a scalar has no direction). pub fn as_axis(self) -> Vec2 { match self { ActionValue::Axis(v) => v, ActionValue::Bool(_) | ActionValue::Scalar(_) => Vec2::ZERO, } } /// The 1D analog payload. A scalar yields its value; an axis yields its `x`; /// a bool yields `0.0`. pub fn as_scalar(self) -> f32 { match self { ActionValue::Scalar(v) => v, ActionValue::Axis(v) => v.x, ActionValue::Bool(_) => 0.0, } } } /// Derive macro for [`GameAction`]: annotate each variant with /// `#[action_kind(...)]` — `instant`, `toggle`, `axis`, or `scalar`. /// /// ```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), [`Axis`](ActionKind::Axis), or /// [`Scalar`](ActionKind::Scalar). 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) / /// [`scalar`](Self::scalar) / [`enabled`](Self::enabled). Games may also drive /// actions programmatically with [`press`](Self::press), [`set`](Self::set), /// [`toggle`](Self::toggle), and [`set_scalar`](Self::set_scalar). #[derive(Resource)] pub struct ActionState { /// Instant/Scalar actions that fired or changed this frame; cleared 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, /// Scalar (slider) actions and their latest value; persists across frames so /// the current value is readable between changes. scalars: HashMap, } impl Default for ActionState { fn default() -> Self { Self { momentary: HashSet::default(), enabled: HashSet::default(), axes: HashMap::default(), scalars: HashMap::default(), } } } impl ActionState { /// The current [`ActionValue`] of `action`, dispatched by its /// [`ActionKind`]: [`Bool`](ActionValue::Bool) for instant/toggle, /// [`Axis`](ActionValue::Axis) for axes, [`Scalar`](ActionValue::Scalar) for /// scalars. /// /// 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)), ActionKind::Scalar => ActionValue::Scalar(self.scalar(action)), } } /// Whether `action` is currently active — what [`on_action`] gates on. /// /// Dispatched by trigger style (see [`ActionKind`]): /// * **edge** — [`Instant`](ActionKind::Instant) fired / [`Scalar`](ActionKind::Scalar) /// changed **this frame**; /// * **level** — [`Toggle`](ActionKind::Toggle) is on / [`Axis`](ActionKind::Axis) /// is deflected (non-zero). /// /// For edge kinds this differs from [`value`](Self::value)`(a).is_active()` /// (which is a value property): a slider resting at `3.0` is *active only on the /// frame it moves*, not every frame its value is non-zero. pub fn active(&self, action: T) -> bool { match action.kind() { ActionKind::Instant | ActionKind::Scalar => self.momentary.contains(&action), ActionKind::Toggle => self.enabled.contains(&action), ActionKind::Axis => self.axis(action) != Vec2::ZERO, } } /// 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) } /// The latest value of a scalar (slider) `action`, or `0.0` if it has never /// been set. Persists between changes, so it is readable on any frame — pair /// with [`on_action`] to react only when it changes. pub fn scalar(&self, action: T) -> f32 { self.scalars.get(&action).copied().unwrap_or(0.0) } /// 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); } /// Sets a scalar (slider) `action`'s latest value, and marks it as changed /// this frame (so [`active`](Self::active)/[`on_action`] fire this frame). pub fn set_scalar(&mut self, action: T, value: f32) { self.scalars.insert(action, value); self.momentary.insert(action); } } /// Marker component linking a UI widget to the action it drives. /// /// Put it on a `Button` ([`Instant`](ActionKind::Instant)), `Checkbox` /// ([`Toggle`](ActionKind::Toggle)), or `Slider` ([`Scalar`](ActionKind::Scalar)) — /// the engine wires the widget's events into [`ActionState`] and keeps checkboxes /// visually in sync. #[derive(Component, Copy, Clone, Debug, Default)] 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 on `action`, for **any** kind — see [`ActionState::active`] for /// the per-kind meaning: an instant fired / a slider changed this frame (edge), or /// a toggle is on / an axis is deflected (level). /// /// So the same call gates "run while paused", "run while the stick is moved", and /// "run when the slider changes". /// /// ```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 /// app.add_systems(Update, save_volume.run_if(on_action(Action::Volume))); // slider changed /// ``` 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) } /// Typed convenience over [`action_value`] for scalar (slider) actions: pipes a /// bare `f32`, so the consumer takes `In`. /// /// Pair with [`on_action`] to run — and deliver the value — only when the slider /// changes: `scalar_value(a).pipe(sys).run_if(on_action(a))`. pub fn scalar_value(action: T) -> impl FnMut(Res>) -> f32 + Clone { move |state: Res>| state.scalar(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; } } } // Scalar actions come only from UI sliders, never from a binding. ActionKind::Scalar => {} } } } /// 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 | ActionKind::Scalar => {} } } } /// 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); } } } /// UI fan-in: a `Slider`'s [`ValueChange`](ValueChange) feeds a scalar action. /// /// The latest value is stored on every change so [`ActionState::scalar`] tracks /// the slider live (including mid-drag), while the edge trigger — what /// [`on_action`] fires on — only marks the interaction's *final* value, so /// gated consumers run once per settled change rather than every drag frame. fn slider_to_action( change: On>, sources: Query<&ActionSource>, mut state: ResMut>, ) { if let Ok(ActionSource(action)) = sources.get(change.source) { if let ActionKind::Scalar = action.kind() { if change.is_final { // Store value + mark changed this frame (edge trigger). state.set_scalar(*action, change.value); } else { // Mid-drag: keep the live value readable, but don't trigger. state.scalars.insert(*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::) .add_observer(slider_to_action::); } }