Skip to content

Commit

Permalink
Remove world access from modifiers and conditions
Browse files Browse the repository at this point in the history
  • Loading branch information
Shatur committed Oct 23, 2024
1 parent 20b36a4 commit ba11922
Show file tree
Hide file tree
Showing 30 changed files with 465 additions and 589 deletions.
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Changed

- Remove world access from conditions and modifiers. This means that you no longer can write game-specific conditions or modifiers. But it's much nicer (and faster) to just do it in observers instead.
- Replace `with_held_timer` with `relative_speed` that just accepts a boolean.
- Rename `HeldTimer` into `ConditionTimer`.
- Use Use `trace!` instead of `debug!` for triggered events.
Expand Down
4 changes: 1 addition & 3 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -14,9 +14,7 @@ include = ["/src", "/tests", "/examples", "/LICENSE*"]
[dependencies]
bevy_enhanced_input_macros = { path = "macros", version = "0.1" }
bevy = { version = "0.14", default-features = false, features = ["serialize"] }
bevy_egui = { version = "0.30", default-features = false, features = [
"immutable_ctx", # Required for get read-only access in our exclusive system.
], optional = true }
bevy_egui = { version = "0.30", default-features = false, optional = true }
serde = "1.0"
bitflags = { version = "2.6", features = ["serde"] }
interpolation = "0.3"
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -14,7 +14,7 @@ Dynamic and contextual input mappings for Bevy, inspired by [Unreal Engine Enhan
* 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", "tap", "chord", etc. You can also create custom conditions, such as "on the ground".
* Assign conditions for how and when an action is triggered, like "hold", "tap", "chord", etc. You can also create custom conditions by implementing a trait.
* React on actions with observers.

## Getting Started
Expand Down
2 changes: 1 addition & 1 deletion examples/context_switch.rs
Original file line number Diff line number Diff line change
Expand Up @@ -135,7 +135,7 @@ impl InputContext for InCar {
.with_wasd()
.with_modifier(Normalize)
.with_modifier(ScaleByDelta)
.with_modifier(Scalar::splat(DEFAULT_SPEED + 200.0)); // Make car faster. It's possible to get the value from a component by writing a custom modifier.
.with_modifier(Scalar::splat(DEFAULT_SPEED + 200.0)); // Make car faster.
ctx.bind::<ExitCar>().with(KeyCode::Enter);

ctx
Expand Down
44 changes: 27 additions & 17 deletions src/input/input_reader.rs
Original file line number Diff line number Diff line change
Expand Up @@ -28,8 +28,10 @@ pub(crate) struct InputReader<'w, 's> {
mouse_motion: Local<'s, Vec2>,
#[cfg(feature = "ui_priority")]
interactions: Query<'w, 's, &'static Interaction>,
// In egui mutable reference is required to get contexts,
// unless `immutable_ctx` feature is enabled.
#[cfg(feature = "egui_priority")]
egui: Query<'w, 's, &'static EguiContext>,
egui: Query<'w, 's, &'static mut EguiContext>,
}

impl InputReader<'_, '_> {
Expand All @@ -47,12 +49,20 @@ impl InputReader<'_, '_> {
}

#[cfg(feature = "egui_priority")]
if self.egui.iter().any(|ctx| ctx.get().wants_keyboard_input()) {
if self
.egui
.iter_mut()
.any(|mut ctx| ctx.get_mut().wants_keyboard_input())
{
self.consumed.ui_wants_keyboard = true;
}

#[cfg(feature = "egui_priority")]
if self.egui.iter().any(|ctx| ctx.get().wants_pointer_input()) {
if self
.egui
.iter_mut()
.any(|mut ctx| ctx.get_mut().wants_pointer_input())
{
self.consumed.ui_wants_mouse = true;
}

Expand Down Expand Up @@ -286,7 +296,7 @@ mod tests {
let key = KeyCode::Space;
world.resource_mut::<ButtonInput<KeyCode>>().press(key);

let mut reader = state.get(&world);
let mut reader = state.get_mut(&mut world);
assert_eq!(reader.value(key), ActionValue::Bool(true));
assert_eq!(reader.value(KeyCode::Escape), ActionValue::Bool(false));
assert_eq!(
Expand All @@ -311,7 +321,7 @@ mod tests {
.resource_mut::<ButtonInput<MouseButton>>()
.press(button);

let mut reader = state.get(&world);
let mut reader = state.get_mut(&mut world);
assert_eq!(reader.value(button), ActionValue::Bool(true));
assert_eq!(reader.value(MouseButton::Right), ActionValue::Bool(false));
assert_eq!(
Expand All @@ -335,7 +345,7 @@ mod tests {
world.send_event(MouseMotion { delta: value });

let input = Input::mouse_motion();
let mut reader = state.get(&world);
let mut reader = state.get_mut(&mut world);
reader.update_state();
assert_eq!(reader.value(input), ActionValue::Axis2D(value));
assert_eq!(
Expand All @@ -361,7 +371,7 @@ mod tests {
});

let input = Input::mouse_wheel();
let mut reader = state.get(&world);
let mut reader = state.get_mut(&mut world);
reader.update_state();
assert_eq!(reader.value(input), ActionValue::Axis2D(value));
assert_eq!(
Expand Down Expand Up @@ -396,7 +406,7 @@ mod tests {
button_type: other_button,
});

let mut reader = state.get(&world);
let mut reader = state.get_mut(&mut world);
reader.set_gamepad(gamepad);
assert_eq!(reader.value(button), ActionValue::Bool(true));
assert_eq!(
Expand Down Expand Up @@ -433,7 +443,7 @@ mod tests {
button_type: other_button,
});

let mut reader = state.get(&world);
let mut reader = state.get_mut(&mut world);
assert_eq!(reader.value(button), ActionValue::Bool(true));
assert_eq!(reader.value(other_button), ActionValue::Bool(true));
assert_eq!(
Expand Down Expand Up @@ -474,7 +484,7 @@ mod tests {
value,
);

let mut reader = state.get(&world);
let mut reader = state.get_mut(&mut world);
reader.set_gamepad(gamepad);
assert_eq!(reader.value(axis), ActionValue::Axis1D(1.0));
assert_eq!(
Expand Down Expand Up @@ -518,7 +528,7 @@ mod tests {
value,
);

let mut reader = state.get(&world);
let mut reader = state.get_mut(&mut world);
assert_eq!(reader.value(axis), ActionValue::Axis1D(1.0));
assert_eq!(reader.value(other_axis), ActionValue::Axis1D(1.0));
assert_eq!(
Expand Down Expand Up @@ -547,7 +557,7 @@ mod tests {
key,
modifiers: modifier.into(),
};
let mut reader = state.get(&world);
let mut reader = state.get_mut(&mut world);
assert_eq!(reader.value(input), ActionValue::Bool(true));
assert_eq!(reader.value(key), ActionValue::Bool(true));
assert_eq!(
Expand All @@ -572,7 +582,7 @@ mod tests {
key: other_key,
modifiers: modifier.into(),
};
let mut reader = state.get(&world);
let mut reader = state.get_mut(&mut world);
assert_eq!(reader.value(other_input), ActionValue::Bool(false));
assert_eq!(reader.value(other_key), ActionValue::Bool(true));
}
Expand All @@ -592,7 +602,7 @@ mod tests {
button,
modifiers: modifier.into(),
};
let mut reader = state.get(&world);
let mut reader = state.get_mut(&mut world);
assert_eq!(reader.value(input), ActionValue::Bool(true));
assert_eq!(reader.value(button), ActionValue::Bool(true));
assert_eq!(
Expand Down Expand Up @@ -621,7 +631,7 @@ mod tests {
let input = Input::MouseMotion {
modifiers: modifier.into(),
};
let mut reader = state.get(&world);
let mut reader = state.get_mut(&mut world);
reader.update_state();
assert_eq!(reader.value(input), ActionValue::Axis2D(value));
assert_eq!(
Expand Down Expand Up @@ -659,7 +669,7 @@ mod tests {
let input = Input::MouseWheel {
modifiers: modifier.into(),
};
let mut reader = state.get(&world);
let mut reader = state.get_mut(&mut world);
reader.update_state();
assert_eq!(reader.value(input), ActionValue::Axis2D(value));
assert_eq!(
Expand Down Expand Up @@ -698,7 +708,7 @@ mod tests {
window: Entity::PLACEHOLDER,
});

let mut reader = state.get(&world);
let mut reader = state.get_mut(&mut world);
reader.update_state();
reader.consumed.ui_wants_keyboard = true;
reader.consumed.ui_wants_mouse = true;
Expand Down
7 changes: 3 additions & 4 deletions src/input_context.rs
Original file line number Diff line number Diff line change
Expand Up @@ -174,20 +174,19 @@ impl ContextInstances {

pub(crate) fn update(
&mut self,
world: &World,
commands: &mut Commands,
reader: &mut InputReader,
delta: f32,
time: &Time<Virtual>,
) {
for group in &mut self.0 {
match group {
InstanceGroup::Exclusive { instances, .. } => {
for (entity, ctx) in instances {
ctx.update(world, commands, reader, &[*entity], delta);
ctx.update(commands, reader, time, &[*entity]);
}
}
InstanceGroup::Shared { entities, ctx, .. } => {
ctx.update(world, commands, reader, entities, delta);
ctx.update(commands, reader, time, entities);
}
}
}
Expand Down
40 changes: 8 additions & 32 deletions src/input_context/context_instance.rs
Original file line number Diff line number Diff line change
Expand Up @@ -76,15 +76,14 @@ impl ContextInstance {

pub(super) fn update(
&mut self,
world: &World,
commands: &mut Commands,
reader: &mut InputReader,
time: &Time<Virtual>,
entities: &[Entity],
delta: f32,
) {
reader.set_gamepad(self.gamepad);
for binding in &mut self.bindings {
binding.update(world, commands, reader, &mut self.actions, entities, delta);
binding.update(commands, reader, &mut self.actions, time, entities);
}
}

Expand Down Expand Up @@ -254,19 +253,13 @@ impl ActionBind {

fn update(
&mut self,
world: &World,
commands: &mut Commands,
reader: &mut InputReader,
actions: &mut ActionsData,
time: &Time<Virtual>,
entities: &[Entity],
delta: f32,
) {
trace!("updating action `{}`", self.action_name);
let ctx = ActionContext {
world,
entities,
actions,
};

reader.set_consume_input(self.consume_input);
let mut tracker = TriggerTracker::new(ActionValue::zero(self.dim));
Expand All @@ -282,40 +275,23 @@ impl ActionBind {
}

let mut current_tracker = TriggerTracker::new(value);
current_tracker.apply_modifiers(&ctx, delta, &mut binding.modifiers);
current_tracker.apply_conditions(&ctx, delta, &mut binding.conditions);
current_tracker.apply_modifiers(time, &mut binding.modifiers);
current_tracker.apply_conditions(actions, time, &mut binding.conditions);
tracker.merge(current_tracker, self.accumulation);
}

tracker.apply_modifiers(&ctx, delta, &mut self.modifiers);
tracker.apply_conditions(&ctx, delta, &mut self.conditions);
tracker.apply_modifiers(time, &mut self.modifiers);
tracker.apply_conditions(actions, time, &mut self.conditions);

let (state, value) = tracker.finish();
let action = actions
.get_mut(&self.type_id)
.expect("actions and bindings should have matching type IDs");

action.update(commands, entities, state, value, delta);
action.update(commands, time, entities, state, value);
}
}

/// Read-only data for [`InputCondition`]s and [`InputModifier`]s during action evaluation.
#[non_exhaustive]
pub struct ActionContext<'a> {
/// Current world.
pub world: &'a World,

/// The state of other actions within the currently evaluating context.
pub actions: &'a ActionsData,

/// The entities for which the action is being evaluated.
///
/// This can be either a single entity when [`InputContext::MODE`](super::InputContext::MODE) is
/// set to [`ContextMode::Exclusive`](super::ContextMode::Exclusive),
/// or multiple entities when using [`ContextMode::Shared`](super::ContextMode::Shared).
pub entities: &'a [Entity],
}

/// Associated input for [`ActionBind`].
#[derive(Debug)]
pub struct InputBind {
Expand Down
8 changes: 4 additions & 4 deletions src/input_context/input_action.rs
Original file line number Diff line number Diff line change
Expand Up @@ -47,21 +47,21 @@ impl ActionData {
pub fn update(
&mut self,
commands: &mut Commands,
time: &Time<Virtual>,
entities: &[Entity],
state: ActionState,
value: impl Into<ActionValue>,
delta: f32,
) {
// Add time from the previous frame if needed
// before triggering events.
match self.state {
ActionState::None => (),
ActionState::Ongoing => {
self.elapsed_secs += delta;
self.elapsed_secs += time.delta_seconds();
}
ActionState::Fired => {
self.elapsed_secs += delta;
self.fired_secs += delta;
self.elapsed_secs += time.delta_seconds();
self.fired_secs += time.delta_seconds();
}
}

Expand Down
41 changes: 12 additions & 29 deletions src/input_context/input_condition.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,49 +11,32 @@ pub mod tap;

use std::fmt::Debug;

use super::{context_instance::ActionContext, input_action::ActionState};
use bevy::prelude::*;

use super::input_action::{ActionState, ActionsData};
use crate::action_value::ActionValue;

pub const DEFAULT_ACTUATION: f32 = 0.5;

/// Defines how input activates.
///
/// Most conditions analyze the input itself, checking for minimum actuation values
/// Conditions analyze the input, checking for minimum actuation values
/// and validating patterns like short taps, prolonged holds, or the typical "press"
/// or "release" events.
///
/// Can be applied both to inputs and actions.
/// See [`ActionBind::with_condition`](super::context_instance::ActionBind::with_condition)
/// and [`InputBind::with_condition`](super::context_instance::InputBind::with_condition).
///
/// You can create game-specific conditions:
///
/// ```
/// # use bevy::prelude::*;
/// use bevy_enhanced_input::prelude::*;
///
/// #[derive(Debug, Clone, Copy)]
/// struct OnGround;
///
/// impl InputCondition for OnGround {
/// fn evaluate(&mut self, ctx: &ActionContext, _delta: f32, _value: ActionValue) -> ActionState {
/// let entity = *ctx.entities.first().unwrap();
/// let transform = ctx.world.get::<Transform>(entity).unwrap();
/// if transform.translation.z <= 0.1 {
/// ActionState::Fired
/// } else {
/// ActionState::None
/// }
/// }
///
/// fn kind(&self) -> ConditionKind {
/// ConditionKind::Required
/// }
/// }
/// ```
pub trait InputCondition: Sync + Send + Debug + 'static {
/// Returns calculates state.
fn evaluate(&mut self, ctx: &ActionContext, delta: f32, value: ActionValue) -> ActionState;
///
/// `actions` argument a state of other actions within the currently evaluating context.
fn evaluate(
&mut self,
actions: &ActionsData,
time: &Time<Virtual>,
value: ActionValue,
) -> ActionState;

/// Returns how the condition is combined with others.
fn kind(&self) -> ConditionKind {
Expand Down
Loading

0 comments on commit ba11922

Please sign in to comment.