Skip to content

Commit

Permalink
Add documentation
Browse files Browse the repository at this point in the history
  • Loading branch information
Shatur committed Oct 12, 2024
1 parent f3832e9 commit aa5512b
Show file tree
Hide file tree
Showing 11 changed files with 520 additions and 63 deletions.
37 changes: 12 additions & 25 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -6,31 +6,18 @@

Dynamic and contextual input mappings inspired by [Unreal Engine Enhanced Input](https://dev.epicgames.com/documentation/en-us/unreal-engine/enhanced-input-in-unreal-engine) for Bevy.

## What makes Enhanced Input... enhanced?

Instead of directly reacting to inputs from various sources (like keyboard, gamepad, etc.), you assign them to your gameplay actions,
like "Move" or "Jump" which are just regular structs. Actions are assigned to contexts, which are components that represent the current
state of the player character, like "OnFoot" or "InCar".

For example, if you have a player character that can be on foot or drive a car, you can swap the context to have the same keys
perform different actions. On foot, pressing <kbd>Space</kbd> could make the character jump, while when driving, pressing <kbd>Space</kbd>
should act as a brake.

## Core concepts

Entities can have any number of contexts, with evaluation order controlled by a defined priority. Actions can also consume inputs,
allowing you to layer behaviors on top of each other.

Instead of reacting to raw input data like "Released" or "Pressed", the crate provides modifiers and conditions.

Modifiers let you change the input before passing it to the action. We provide common modifiers, like dead zones, inversion, and scaling,
but you can add your own by implementing a trait.

Conditions define how an action activates. We also provide built-in conditions, such as "Pressed", "Released", "Hold for n secs", etc.
However, you can also add gameplay-based conditions like "Can jump" by implementing a trait.

To respond to action changes, you can use observers. For example, with the condition "Hold for 1 sec", you can trigger a strong attack
on "Completed", or a regular attack on "Cancelled".
## Features

* Map inputs from various sources (keyboard, gamepad, etc.) to gameplay actions like `Jump`, `Move`, or `Attack`.
* Assign actions to different contexts like `OnFoot` or `InCar`, which are regular components.
* Activate or deactivate contexts by simply adding or removing components.
* Control how actions accumulate input from sources and consume it.
* Layer multiple contexts on a single entity, controlled by priority.
* Apply modifiers to inputs, such as dead zones, inversion, scaling, etc., or create custom modifiers by implementing a trait.
* Assign conditions for how and when an action is triggered, like "hold", "double-tap" or "chord", etc. You can also create custom conditions, such as "on the ground".
* React on actions with observers.

## Getting Started

Check out the [quick start guide](https://docs.rs/bevy_enhanced_input) for more details.

Expand Down
2 changes: 2 additions & 0 deletions examples/simple.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,8 @@ impl InputContext for Player {

// Mappings like WASD or sticks are very common,
// so we provide built-ins to assign all keys/axes at once.
// We don't assign any conditions. In this case the action will
// be triggered with any non-zero value.
ctx.bind::<Move>()
.with_wasd()
.with_stick(GamepadStick::Left);
Expand Down
26 changes: 26 additions & 0 deletions src/action_value.rs
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
use bevy::{input::ButtonState, prelude::*};
use serde::{Deserialize, Serialize};

/// Value for [`Input`](crate::input_reader::Input).
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Debug)]
pub enum ActionValue {
Bool(bool),
Expand All @@ -10,6 +11,7 @@ pub enum ActionValue {
}

impl ActionValue {
/// Creates a zero-initialized value for the specified dimention.
pub fn zero(dim: ActionValueDim) -> Self {
match dim {
ActionValueDim::Bool => ActionValue::Bool(false),
Expand All @@ -19,6 +21,7 @@ impl ActionValue {
}
}

/// Returns dimention.
pub fn dim(self) -> ActionValueDim {
match self {
Self::Bool(_) => ActionValueDim::Bool,
Expand All @@ -28,6 +31,10 @@ impl ActionValue {
}
}

/// Converts the value into the specified variant based on the dimention.
///
/// If the new dimension is larger, the additional axes will be set to zero.
/// If the new dimension is smaller, the extra axes will be discarded.
pub fn convert(self, dim: ActionValueDim) -> Self {
match dim {
ActionValueDim::Bool => self.as_bool().into(),
Expand All @@ -37,6 +44,10 @@ impl ActionValue {
}
}

/// Returns the value as a boolean.
///
/// If the value is not [`ActionValue::Bool`],
/// it returns `false` if the value is zero, and `true` otherwise.
pub fn as_bool(self) -> bool {
match self {
Self::Bool(value) => value,
Expand All @@ -46,6 +57,10 @@ impl ActionValue {
}
}

/// Returns the value as a 1-dimensional axis.
///
/// For [`ActionValue::Bool`], it returns `1.0` if `true`, otherwise `0.0`.
/// For multi-dimensional values, it returns the X axis.
pub fn as_axis1d(self) -> f32 {
match self {
Self::Bool(value) => {
Expand All @@ -61,6 +76,11 @@ impl ActionValue {
}
}

/// Returns the value as a 2-dimensional axis.
///
/// For [`ActionValue::Bool`], it returns [`Vec2::X`] if `true`, otherwise [`Vec2::ZERO`].
/// For [`ActionValue::Axis1D`], it maps the value to the X axis.
/// For [`ActionValue::Axis3D`], it returns the X and Y axes.
pub fn as_axis2d(self) -> Vec2 {
match self {
Self::Bool(value) => {
Expand All @@ -76,6 +96,11 @@ impl ActionValue {
}
}

/// Returns the value as a 3-dimensional axis .
///
/// For [`ActionValue::Bool`], it returns [`Vec3::X`] if `true`, otherwise [`Vec3::ZERO`].
/// For [`ActionValue::Axis1D`], it maps the value to the X axis.
/// For [`ActionValue::Axis2D`], it maps the value to the X and Y axes.
pub fn as_axis3d(self) -> Vec3 {
match self {
Self::Bool(value) => {
Expand All @@ -92,6 +117,7 @@ impl ActionValue {
}
}

/// A dimention discriminant for [`ActionValue`].
#[derive(Serialize, Deserialize, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Debug)]
pub enum ActionValueDim {
Bool,
Expand Down
80 changes: 79 additions & 1 deletion src/input_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,21 @@ use bevy::prelude::*;
use crate::input_reader::InputReader;
use context_instance::ContextInstance;

/// An extension trait for [`App`] to register contexts.
///
/// ```
/// # use bevy::prelude::*;
/// # use bevy_enhanced_input::prelude::*;
/// let mut app = App::new();
/// app.add_input_context::<Player>();
/// # #[derive(Component)]
/// # struct Player;
/// # impl InputContext for Player {
/// # fn context_instance(_world: &World, _entity: Entity) -> ContextInstance { Default::default() }
/// # }
/// ```
pub trait ContextAppExt {
/// Registers an input context.
fn add_input_context<C: InputContext>(&mut self) -> &mut Self;
}

Expand Down Expand Up @@ -186,7 +200,7 @@ impl ContextInstances {
}
}

/// Instances of [`InputContext`] for the same type.
/// Instances of [`InputContext`] for the same type based on [`InputContext::MODE`].
enum InstanceGroup {
Exclusive {
type_id: TypeId,
Expand All @@ -202,6 +216,7 @@ enum InstanceGroup {
}

impl InstanceGroup {
#[must_use]
fn new<C: InputContext>(world: &World, entity: Entity) -> Self {
let type_id = TypeId::of::<C>();
let ctx = C::context_instance(world, entity);
Expand Down Expand Up @@ -235,10 +250,73 @@ impl InstanceGroup {
}
}

/// Contexts are components that associate entities with [`InputAction`](input_action::InputAction)s.
///
/// Inserting this component associates [`ContextInstance`] for this
/// entity in a resource.
///
/// Removing deactivates [`ContextInstance`] for the entity end trigger
/// transitions for all actions to [`ActionState::None`](input_action::ActionState::None).
///
/// Each context should be registered using [`ContextAppExt::add_input_context`].
///
/// ```
/// # use bevy::prelude::*;
/// # use bevy_enhanced_input::prelude::*;
/// #[derive(Component)]
/// struct Player;
///
/// impl InputContext for Player {
/// fn context_instance(world: &World, entity: Entity) -> ContextInstance {
/// // You can use world to access the necessaary data.
/// let settings = world.resource::<AppSettings>();
///
/// // To can also access the context
/// // component itself from the entity.
/// let player = world.get::<Self>(entity).unwrap();
///
/// let mut ctx = ContextInstance::default();
///
/// ctx.bind::<Move>()
/// .with_wasd()
/// .with_stick(GamepadStick::Left);
///
/// ctx.bind::<Jump>()
/// .with(KeyCode::Space)
/// .with(GamepadButtonType::South);
///
/// ctx
/// }
/// }
/// # #[derive(Debug, InputAction)]
/// # #[input_action(dim = Axis2D)]
/// # struct Move;
/// # #[derive(Debug, InputAction)]
/// # #[input_action(dim = Bool)]
/// # struct Jump;
/// # #[derive(Resource)]
/// # struct AppSettings;
/// ```
pub trait InputContext: Component {
/// Configures how context will be instantiated.
const MODE: ContextMode = ContextMode::Exclusive;

/// Determines the evaluation order of [`ContextInstance`]s produced
/// by this component.
///
/// Ordering is global.
/// Contexts with a higher priority evaluated first.
const PRIORITY: usize = 0;

/// Creates a new instance for the given entity.
///
/// In the implementation you need call [`ContextInstance::bind`]
/// to associate it with [`InputAction`](input_action::InputAction)s.
///
/// The function is called on each context instantiation
/// which depends on [`Self::MODE`].
/// You can also rebuild all contexts by triggering [`RebuildInputContexts`].
#[must_use]
fn context_instance(world: &World, entity: Entity) -> ContextInstance;
}

Expand Down
Loading

0 comments on commit aa5512b

Please sign in to comment.