From 36e7d9668ca7e55baee7a04ac3186662221e1793 Mon Sep 17 00:00:00 2001 From: Patrick Zhao Date: Mon, 7 Oct 2024 14:45:18 +0200 Subject: [PATCH] Add ability to filter entities for which action diffs should be generated (#601) * Support query filters in SummarizedActionState::summarize * Add filtered version of generate_action_diffs (with tests) * Change dbg! to debug! for more controllable debug prints * Fix unused variable in unit test * Delete explicit type args in function call as it can be inferred * Make separate filtered/nonfiltered summarize fns for SummarizedActionState * Add release notes for filtered action diffs * Improve documenting comments * Add backticks to documentation, make clippy happy * Move filtered action diffs to Usability (0.16) in RELEASES.md * Improve unit test comments * Fix indentation in RELEASES.md * Add integration test for generate_action_diffs_filtered * Add missing ActionDiff variant to filtered action diff test * Reword filtered action diff release note for coherent style --- RELEASES.md | 3 +++ src/action_diff.rs | 34 ++++++++++++++++++++++++- src/systems.rs | 28 +++++++++++++++++++-- tests/action_diffs.rs | 58 ++++++++++++++++++++++++++++++++++++++++++- 4 files changed, 119 insertions(+), 4 deletions(-) diff --git a/RELEASES.md b/RELEASES.md index 67ba0f83..90dcd932 100644 --- a/RELEASES.md +++ b/RELEASES.md @@ -9,6 +9,9 @@ - removed `KeyboardVirtualDPad` and `GamepadVirtualDPad` in favor of `VirtualDPad` - removed `KeyboardVirtualDPad3D` in favor of `VirtualDPad3D` - added `threshold` value for `GamepadControlDirection`, `MouseMoveDirection`, and `MouseScrollDirection` to be considered pressed. +- added ability to filter entities to generate action diffs for: + - added new `generate_action_diffs_filtered` system, which accepts a `QueryFilter`, so that only entities matching QueryFilter `F` (and with `ActionState`) generate action diffs + - added new `summarize_filtered` function for `SummarizedActionState` (alongside the original `summarize`). ## Version 0.15.1 diff --git a/src/action_diff.rs b/src/action_diff.rs index d1a8ef6d..ae990aa3 100644 --- a/src/action_diff.rs +++ b/src/action_diff.rs @@ -9,6 +9,7 @@ use bevy::{ ecs::{ entity::{Entity, MapEntities}, event::Event, + query::QueryFilter, }, math::{Vec2, Vec3}, prelude::{EntityMapper, EventWriter, Query, Res}, @@ -112,10 +113,19 @@ impl SummarizedActionState { entities } - /// Captures the raw values for each action in the current frame. + /// Captures the raw values for each action in the current frame, for all entities with `ActionState`. pub fn summarize( global_action_state: Option>>, action_state_query: Query<(Entity, &ActionState)>, + ) -> Self { + Self::summarize_filtered(global_action_state, action_state_query) + } + + /// Captures the raw values for each action in the current frame, for entities with `ActionState` + /// matching the query filter. + pub fn summarize_filtered( + global_action_state: Option>>, + action_state_query: Query<(Entity, &ActionState), F>, ) -> Self { let mut button_state_map = HashMap::default(); let mut axis_state_map = HashMap::default(); @@ -389,6 +399,9 @@ mod tests { action_state } + #[derive(Component)] + struct NotSummarized; + fn expected_summary(entity: Entity) -> SummarizedActionState { let mut button_state_map = HashMap::default(); let mut axis_state_map = HashMap::default(); @@ -449,6 +462,25 @@ mod tests { assert_eq!(summarized, expected_summary(entity)); } + #[test] + fn summarize_filtered_entities_from_component() { + // Spawn two entities, one to be summarized and one to be filtered out + let mut world = World::new(); + let entity = world.spawn(test_action_state()).id(); + world.spawn((test_action_state(), NotSummarized)); + + let mut system_state: SystemState<( + Option>>, + Query<(Entity, &ActionState), Without>, + )> = SystemState::new(&mut world); + let (global_action_state, action_state_query) = system_state.get(&world); + let summarized = + SummarizedActionState::summarize_filtered(global_action_state, action_state_query); + + // Check that only the entity without NotSummarized was summarized + assert_eq!(summarized, expected_summary(entity)); + } + #[test] fn diffs_are_sent() { let mut world = World::new(); diff --git a/src/systems.rs b/src/systems.rs index 7bc69ba5..61546844 100644 --- a/src/systems.rs +++ b/src/systems.rs @@ -3,8 +3,10 @@ use crate::prelude::updating::CentralInputStore; #[cfg(feature = "mouse")] use crate::user_input::{AccumulatedMouseMovement, AccumulatedMouseScroll}; +use bevy::ecs::query::QueryFilter; #[cfg(feature = "mouse")] use bevy::input::mouse::{MouseMotion, MouseWheel}; +use bevy::log::debug; use crate::{ action_state::ActionState, clashing_inputs::ClashStrategy, input_map::InputMap, Actionlike, @@ -169,13 +171,35 @@ pub fn filter_captured_input( pub fn generate_action_diffs( global_action_state: Option>>, action_state_query: Query<(Entity, &ActionState)>, + previous_action_state: Local>, + action_diff_events: EventWriter>, +) { + generate_action_diffs_filtered( + global_action_state, + action_state_query, + previous_action_state, + action_diff_events, + ) +} + +/// Generates an [`Events`] stream of [`ActionDiff`s](crate::action_diff::ActionDiff) from the [`ActionState`] of certain entities. +/// +/// This system is not part of the [`InputManagerPlugin`](crate::plugin::InputManagerPlugin) and must be added manually. +/// Generally speaking, this should be added as part of [`PostUpdate`](bevy::prelude::PostUpdate), +/// to ensure that all inputs have been processed and any manual actions have been sent. +/// +/// This system accepts a [`QueryFilter`] to limit which entities should have action diffs generated. +pub fn generate_action_diffs_filtered( + global_action_state: Option>>, + action_state_query: Query<(Entity, &ActionState), F>, mut previous_action_state: Local>, mut action_diff_events: EventWriter>, ) { let current_action_state = - SummarizedActionState::summarize(global_action_state, action_state_query); + SummarizedActionState::summarize_filtered(global_action_state, action_state_query); current_action_state.send_diffs(&previous_action_state, &mut action_diff_events); - dbg!(&previous_action_state, ¤t_action_state); + debug!("previous_action_state: {:?}", previous_action_state); + debug!("current_action_state: {:?}", current_action_state); *previous_action_state = current_action_state; } diff --git a/tests/action_diffs.rs b/tests/action_diffs.rs index f2b18122..d8e91f09 100644 --- a/tests/action_diffs.rs +++ b/tests/action_diffs.rs @@ -1,6 +1,9 @@ use bevy::{input::InputPlugin, prelude::*}; use leafwing_input_manager::action_diff::{ActionDiff, ActionDiffEvent}; -use leafwing_input_manager::{prelude::*, systems::generate_action_diffs}; +use leafwing_input_manager::{ + prelude::*, + systems::{generate_action_diffs, generate_action_diffs_filtered}, +}; #[derive(Actionlike, Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)] enum Action { @@ -11,6 +14,9 @@ enum Action { DualAxis, } +#[derive(Component)] +pub struct NoActionDiffs; + fn spawn_test_entity(mut commands: Commands) { commands.spawn(ActionState::::default()); } @@ -265,6 +271,56 @@ fn generate_axis_action_diffs() { }); } +#[test] +fn generate_filtered_binary_action_diffs() { + let mut app = create_app(); + app.add_systems( + PostUpdate, + generate_action_diffs_filtered::>, + ); + + let entity = app + .world_mut() + .query_filtered::>>() + .single(app.world()); + // Also spawn an entity that will not send action diffs + app.world_mut() + .spawn((ActionState::::default(), NoActionDiffs)); + + // Press both entities + for mut action_state in app + .world_mut() + .query::<&mut ActionState>() + .iter_mut(app.world_mut()) + { + action_state.press(&Action::Button); + } + app.update(); + + // Expect only `entity` to have an action diff event + assert_action_diff_created(&mut app, |action_diff_event| { + assert_eq!(action_diff_event.owner, Some(entity)); + assert_eq!(action_diff_event.action_diffs.len(), 1); + match action_diff_event.action_diffs.first().unwrap().clone() { + ActionDiff::Pressed { action } => { + assert_eq!(action, Action::Button); + } + ActionDiff::Released { .. } => { + panic!("Expected a `Pressed` variant got a `Released` variant") + } + ActionDiff::AxisChanged { .. } => { + panic!("Expected a `Pressed` variant got a `ValueChanged` variant") + } + ActionDiff::DualAxisChanged { .. } => { + panic!("Expected a `Pressed` variant got a `AxisPairChanged` variant") + } + ActionDiff::TripleAxisChanged { .. } => { + panic!("Expected a `Pressed` variant got a `TripleAxisChanged` variant") + } + } + }); +} + #[test] fn process_binary_action_diffs() { let mut app = create_app();