-
Notifications
You must be signed in to change notification settings - Fork 1
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
- Loading branch information
Showing
5 changed files
with
538 additions
and
0 deletions.
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,82 @@ | ||
mod action_recorder; | ||
|
||
use action_recorder::{ActionRecorderPlugin, AppTriggeredExt, RecordedActions}; | ||
use bevy::{input::InputPlugin, prelude::*}; | ||
use bevy_enhanced_input::prelude::*; | ||
|
||
#[test] | ||
fn max_abs() { | ||
let mut app = App::new(); | ||
app.add_plugins(( | ||
MinimalPlugins, | ||
InputPlugin, | ||
EnhancedInputPlugin, | ||
ActionRecorderPlugin, | ||
)) | ||
.add_input_context::<Moving>() | ||
.record_action::<MaxAbsMove>(); | ||
|
||
let entity = app.world_mut().spawn(Moving).id(); | ||
|
||
app.update(); | ||
|
||
let mut keys = app.world_mut().resource_mut::<ButtonInput<KeyCode>>(); | ||
keys.press(KeyCode::KeyW); | ||
keys.press(KeyCode::KeyS); | ||
|
||
app.update(); | ||
|
||
let recorded = app.world().resource::<RecordedActions>(); | ||
assert_eq!(recorded.last::<MaxAbsMove>(entity).value, Vec2::Y.into()); | ||
} | ||
|
||
#[test] | ||
fn cumulative() { | ||
let mut app = App::new(); | ||
app.add_plugins(( | ||
MinimalPlugins, | ||
InputPlugin, | ||
EnhancedInputPlugin, | ||
ActionRecorderPlugin, | ||
)) | ||
.add_input_context::<Moving>() | ||
.record_action::<CumulativeMove>(); | ||
|
||
let entity = app.world_mut().spawn(Moving).id(); | ||
|
||
app.update(); | ||
|
||
let mut keys = app.world_mut().resource_mut::<ButtonInput<KeyCode>>(); | ||
keys.press(KeyCode::ArrowUp); | ||
keys.press(KeyCode::ArrowDown); | ||
|
||
app.update(); | ||
|
||
let recorded = app.world().resource::<RecordedActions>(); | ||
assert!( | ||
recorded.is_empty::<CumulativeMove>(entity), | ||
"up and down should cancel each other" | ||
); | ||
} | ||
|
||
#[derive(Debug, Component)] | ||
struct Moving; | ||
|
||
impl InputContext for Moving { | ||
fn context_instance(_world: &World, _entity: Entity) -> ContextInstance { | ||
let mut ctx = ContextInstance::default(); | ||
|
||
ctx.bind::<MaxAbsMove>().with_wasd(); | ||
ctx.bind::<CumulativeMove>().with_arrows(); | ||
|
||
ctx | ||
} | ||
} | ||
|
||
#[derive(Debug, InputAction)] | ||
#[input_action(dim = Axis2D, accumulation = MaxAbs)] | ||
struct MaxAbsMove; | ||
|
||
#[derive(Debug, InputAction)] | ||
#[input_action(dim = Axis2D, accumulation = Cumulative)] | ||
struct CumulativeMove; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,124 @@ | ||
//! Assert action events in tests. | ||
|
||
use std::any::{self, TypeId}; | ||
|
||
use bevy::{ecs::entity::EntityHashMap, prelude::*, utils::HashMap}; | ||
use bevy_enhanced_input::{prelude::*, EnhancedInputSystem}; | ||
|
||
pub trait AppTriggeredExt { | ||
/// Observes for [`ActionEvent`] and stores them inside [`RecordedActions`]. | ||
fn record_action<A: InputAction>(&mut self) -> &mut Self; | ||
} | ||
|
||
impl AppTriggeredExt for App { | ||
fn record_action<A: InputAction>(&mut self) -> &mut Self { | ||
self.world_mut() | ||
.resource_mut::<RecordedActions>() | ||
.register::<A>(); | ||
self.observe(read::<A>) | ||
} | ||
} | ||
|
||
fn read<A: InputAction>(trigger: Trigger<ActionEvent<A>>, mut triggered: ResMut<RecordedActions>) { | ||
triggered.insert::<A>(trigger.entity(), *trigger.event()); | ||
} | ||
|
||
pub struct ActionRecorderPlugin; | ||
|
||
impl Plugin for ActionRecorderPlugin { | ||
fn build(&self, app: &mut App) { | ||
app.init_resource::<RecordedActions>() | ||
.add_systems(PreUpdate, Self::clear.before(EnhancedInputSystem)); | ||
} | ||
} | ||
|
||
impl ActionRecorderPlugin { | ||
fn clear(mut triggered: ResMut<RecordedActions>) { | ||
triggered.clear(); | ||
} | ||
} | ||
|
||
#[derive(Default, Resource)] | ||
pub struct RecordedActions(HashMap<TypeId, EntityHashMap<Vec<UntypedActionEvent>>>); | ||
|
||
impl RecordedActions { | ||
fn insert<A: InputAction>(&mut self, entity: Entity, event: ActionEvent<A>) { | ||
let event_group = self.0.entry(TypeId::of::<A>()).or_default(); | ||
let events = event_group.entry(entity).or_default(); | ||
events.push(event.into()); | ||
} | ||
|
||
#[allow(dead_code)] | ||
pub fn assert_array<A: InputAction, const SIZE: usize>( | ||
&self, | ||
entity: Entity, | ||
) -> [UntypedActionEvent; SIZE] { | ||
let events = self.get::<A>(entity); | ||
events.try_into().unwrap_or_else(|_| { | ||
panic!( | ||
"expected {SIZE} events of type `{}`, but got {}", | ||
events.len(), | ||
any::type_name::<A>() | ||
); | ||
}) | ||
} | ||
|
||
#[allow(dead_code)] | ||
pub fn is_empty<A: InputAction>(&self, entity: Entity) -> bool { | ||
self.get::<A>(entity).is_empty() | ||
} | ||
|
||
#[allow(dead_code)] | ||
pub fn last<A: InputAction>(&self, entity: Entity) -> &UntypedActionEvent { | ||
self.get::<A>(entity).last().unwrap_or_else(|| { | ||
panic!( | ||
"expected at least one action event of type `{}`", | ||
any::type_name::<A>() | ||
) | ||
}) | ||
} | ||
|
||
#[allow(dead_code)] | ||
fn get<A: InputAction>(&self, entity: Entity) -> &[UntypedActionEvent] { | ||
let event_group = self.0.get(&TypeId::of::<A>()).unwrap_or_else(|| { | ||
panic!( | ||
"action event of type `{}` is not registered", | ||
any::type_name::<A>() | ||
) | ||
}); | ||
|
||
event_group | ||
.get(&entity) | ||
.map(|events| &events[..]) | ||
.unwrap_or(&[]) | ||
} | ||
|
||
fn register<A: InputAction>(&mut self) { | ||
self.0.insert(TypeId::of::<A>(), Default::default()); | ||
} | ||
|
||
fn clear(&mut self) { | ||
for event_group in self.0.values_mut() { | ||
event_group.clear(); | ||
} | ||
} | ||
} | ||
|
||
/// Untyped version of [`ActionEvent`]. | ||
#[derive(Clone, Copy)] | ||
#[allow(dead_code)] | ||
pub struct UntypedActionEvent { | ||
pub transition: ActionTransition, | ||
pub value: ActionValue, | ||
pub state: ActionState, | ||
} | ||
|
||
impl<A: InputAction> From<ActionEvent<A>> for UntypedActionEvent { | ||
fn from(value: ActionEvent<A>) -> Self { | ||
Self { | ||
transition: value.transition, | ||
value: value.value, | ||
state: value.state, | ||
} | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,106 @@ | ||
mod action_recorder; | ||
|
||
use action_recorder::{ActionRecorderPlugin, AppTriggeredExt, RecordedActions}; | ||
use bevy::{input::InputPlugin, prelude::*}; | ||
use bevy_enhanced_input::prelude::*; | ||
|
||
#[test] | ||
fn passthrough() { | ||
let mut app = App::new(); | ||
app.add_plugins(( | ||
MinimalPlugins, | ||
InputPlugin, | ||
EnhancedInputPlugin, | ||
ActionRecorderPlugin, | ||
)) | ||
.add_input_context::<ConsumeThenPassthrough>() | ||
.record_action::<Consume>() | ||
.record_action::<Passthrough>(); | ||
|
||
let entity = app.world_mut().spawn(ConsumeThenPassthrough).id(); | ||
|
||
app.update(); | ||
|
||
app.world_mut() | ||
.resource_mut::<ButtonInput<KeyCode>>() | ||
.press(KEY); | ||
|
||
app.update(); | ||
|
||
let recorded = app.world().resource::<RecordedActions>(); | ||
assert_eq!(recorded.last::<Consume>(entity).state, ActionState::Fired); | ||
assert!( | ||
recorded.is_empty::<Passthrough>(entity), | ||
"action should be consumed" | ||
); | ||
} | ||
|
||
#[test] | ||
fn consume() { | ||
let mut app = App::new(); | ||
app.add_plugins(( | ||
MinimalPlugins, | ||
InputPlugin, | ||
EnhancedInputPlugin, | ||
ActionRecorderPlugin, | ||
)) | ||
.add_input_context::<PassthroughThenConsume>() | ||
.record_action::<Consume>() | ||
.record_action::<Passthrough>(); | ||
|
||
let entity = app.world_mut().spawn(PassthroughThenConsume).id(); | ||
|
||
app.update(); | ||
|
||
app.world_mut() | ||
.resource_mut::<ButtonInput<KeyCode>>() | ||
.press(KEY); | ||
|
||
app.update(); | ||
|
||
let recorded = app.world().resource::<RecordedActions>(); | ||
assert_eq!(recorded.last::<Consume>(entity).state, ActionState::Fired); | ||
assert_eq!( | ||
recorded.last::<Passthrough>(entity).state, | ||
ActionState::Fired | ||
); | ||
} | ||
|
||
/// A key used by both [`Consume`] and [`Passthrough`] actions. | ||
const KEY: KeyCode = KeyCode::Space; | ||
|
||
#[derive(Debug, Component)] | ||
struct PassthroughThenConsume; | ||
|
||
impl InputContext for PassthroughThenConsume { | ||
fn context_instance(_world: &World, _entity: Entity) -> ContextInstance { | ||
let mut ctx = ContextInstance::default(); | ||
|
||
ctx.bind::<Passthrough>().with(KEY); | ||
ctx.bind::<Consume>().with(KEY); | ||
|
||
ctx | ||
} | ||
} | ||
|
||
#[derive(Debug, Component)] | ||
struct ConsumeThenPassthrough; | ||
|
||
impl InputContext for ConsumeThenPassthrough { | ||
fn context_instance(_world: &World, _entity: Entity) -> ContextInstance { | ||
let mut ctx = ContextInstance::default(); | ||
|
||
ctx.bind::<Consume>().with(KEY); | ||
ctx.bind::<Passthrough>().with(KEY); | ||
|
||
ctx | ||
} | ||
} | ||
|
||
#[derive(Debug, InputAction)] | ||
#[input_action(dim = Bool, consume_input = true)] | ||
struct Consume; | ||
|
||
#[derive(Debug, InputAction)] | ||
#[input_action(dim = Bool, consume_input = false)] | ||
struct Passthrough; |
Oops, something went wrong.