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 3bd399f
Show file tree
Hide file tree
Showing 30 changed files with 451 additions and 575 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
16 changes: 13 additions & 3 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(|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(|ctx| ctx.get_mut().wants_pointer_input())
{
self.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
44 changes: 18 additions & 26 deletions src/input_context/input_condition/blocked_by.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,10 +5,7 @@ use bevy::prelude::*;
use super::{ConditionKind, InputCondition};
use crate::{
action_value::ActionValue,
input_context::{
context_instance::ActionContext,
input_action::{ActionState, InputAction},
},
input_context::input_action::{ActionState, ActionsData, InputAction},
};

/// Requires another action to not be triggered within the same context.
Expand Down Expand Up @@ -37,8 +34,13 @@ impl<A: InputAction> Clone for BlockedBy<A> {
impl<A: InputAction> Copy for BlockedBy<A> {}

impl<A: InputAction> InputCondition for BlockedBy<A> {
fn evaluate(&mut self, ctx: &ActionContext, _delta: f32, _value: ActionValue) -> ActionState {
if let Some(action) = ctx.actions.action::<A>() {
fn evaluate(
&mut self,
actions: &ActionsData,
_time: &Time<Virtual>,
_value: ActionValue,
) -> ActionState {
if let Some(action) = actions.action::<A>() {
if action.state() == ActionState::Fired {
return ActionState::None;
}
Expand All @@ -64,42 +66,32 @@ mod tests {
use bevy_enhanced_input_macros::InputAction;

use super::*;
use crate::{
input_context::input_action::{ActionData, ActionsData},
ActionValueDim,
};
use crate::{input_context::input_action::ActionData, ActionValueDim};

#[test]
fn blocked() {
let mut world = World::new();
let mut condition = BlockedBy::<DummyAction>::default();
let mut action = ActionData::new::<DummyAction>();
action.update(&mut world.commands(), &[], ActionState::Fired, true, 0.0);
let mut world = World::new();
let time = Time::default();
action.update(&mut world.commands(), &time, &[], ActionState::Fired, true);
let mut actions = ActionsData::default();
actions.insert(TypeId::of::<DummyAction>(), action);
let ctx = ActionContext {
world: &world,
actions: &actions,
entities: &[],
};

let mut condition = BlockedBy::<DummyAction>::default();
assert_eq!(
condition.evaluate(&ctx, 0.0, true.into()),
condition.evaluate(&actions, &time, true.into()),
ActionState::None,
);
}

#[test]
fn missing_action() {
let ctx = ActionContext {
world: &World::new(),
actions: &ActionsData::default(),
entities: &[],
};

let mut condition = BlockedBy::<DummyAction>::default();
let actions = ActionsData::default();
let time = Time::default();

assert_eq!(
condition.evaluate(&ctx, 0.0, true.into()),
condition.evaluate(&actions, &time, true.into()),
ActionState::Fired,
);
}
Expand Down
Loading

0 comments on commit 3bd399f

Please sign in to comment.