Skip to content

Commit

Permalink
Remove methods from Actionlike to support more complex Action types (
Browse files Browse the repository at this point in the history
…#452)

* Remove n_variants

* Remove Actionlike::index

* Remove Actionlike::variants

* Remove get_at method on Actionlike

* Fix up tests and examples

* Add tests that validate that the trait is more flexible now

* Remove fragile test

* Clippy

* Remove outdated docs

---------

Co-authored-by: Alice Cecile <[email protected]>
  • Loading branch information
alice-i-cecile and Alice Cecile authored Jan 24, 2024
1 parent a5edba0 commit 779ebc1
Show file tree
Hide file tree
Showing 15 changed files with 146 additions and 254 deletions.
1 change: 1 addition & 0 deletions RELEASES.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
- `InputMap`s are now constructed with `(Action, Input)` pairs, rather than `(Input, Action)` pairs, which directly matches the underlying data model
- registered types in the reflection system
- added `InputMap::clear`
- added `ActionState::keys`

### Bugs

Expand Down
7 changes: 7 additions & 0 deletions benches/action_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,6 +21,13 @@ enum TestAction {
J,
}

impl TestAction {
fn variants() -> impl Iterator<Item = TestAction> {
use TestAction::*;
[A, B, C, D, E, F, G, H, I, J].iter().copied()
}
}

fn pressed(action_state: &ActionState<TestAction>) -> bool {
action_state.pressed(&TestAction::A)
}
Expand Down
12 changes: 11 additions & 1 deletion examples/arpg_indirection.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,16 @@ enum Slot {
Ability4,
}

impl Slot {
/// You could use the `strum` crate to derive this automatically!
fn variants() -> impl Iterator<Item = Slot> {
use Slot::*;
[Primary, Secondary, Ability1, Ability2, Ability3, Ability4]
.iter()
.copied()
}
}

// The list of possible abilities is typically longer than the list of slots
#[derive(Actionlike, PartialEq, Eq, Hash, Clone, Debug, Copy, Reflect)]
enum Ability {
Expand Down Expand Up @@ -113,7 +123,7 @@ fn copy_action_state(
// This copies the `ActionData` between the ActionStates,
// including information about how long the buttons have been pressed or released
ability_state.set_action_data(
matching_ability.clone(),
*matching_ability,
slot_state.action_data(&slot).unwrap().clone(),
);
}
Expand Down
6 changes: 1 addition & 5 deletions examples/clash_handling.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,9 +54,5 @@ fn report_pressed_actions(
query: Query<&ActionState<TestAction>, Changed<ActionState<TestAction>>>,
) {
let action_state = query.single();
for action in TestAction::variants() {
if action_state.just_pressed(&action) {
dbg!(action);
}
}
dbg!(action_state.get_just_pressed());
}
19 changes: 13 additions & 6 deletions examples/default_controls.rs
Original file line number Diff line number Diff line change
Expand Up @@ -19,20 +19,27 @@ enum PlayerAction {
UseItem,
}

impl PlayerAction {
// The `strum` crate provides a deriveable trait for this!
fn variants() -> &'static [PlayerAction] {
&[Self::Run, Self::Jump, Self::UseItem]
}
}

// Exhaustively match `PlayerAction` and define the default binding to the input
impl PlayerAction {
fn default_keyboard_mouse_input(action: PlayerAction) -> UserInput {
fn default_keyboard_mouse_input(&self) -> UserInput {
// Match against the provided action to get the correct default keyboard-mouse input
match action {
match self {
Self::Run => UserInput::VirtualDPad(VirtualDPad::wasd()),
Self::Jump => UserInput::Single(InputKind::Keyboard(KeyCode::Space)),
Self::UseItem => UserInput::Single(InputKind::Mouse(MouseButton::Left)),
}
}

fn default_gamepad_input(action: PlayerAction) -> UserInput {
fn default_gamepad_input(&self) -> UserInput {
// Match against the provided action to get the correct default gamepad input
match action {
match self {
Self::Run => UserInput::Single(InputKind::DualAxis(DualAxis::left_stick())),
Self::Jump => UserInput::Single(InputKind::GamepadButton(GamepadButtonType::South)),
Self::UseItem => {
Expand All @@ -52,8 +59,8 @@ fn spawn_player(mut commands: Commands) {
// Loop through each action in `PlayerAction` and get the default `UserInput`,
// then insert each default input into input_map
for action in PlayerAction::variants() {
input_map.insert(action, PlayerAction::default_keyboard_mouse_input(action));
input_map.insert(action, PlayerAction::default_gamepad_input(action));
input_map.insert(*action, PlayerAction::default_keyboard_mouse_input(action));
input_map.insert(*action, PlayerAction::default_gamepad_input(action));
}

// Spawn the player with the populated input_map
Expand Down
13 changes: 10 additions & 3 deletions examples/twin_stick_controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -38,6 +38,13 @@ pub enum PlayerAction {
Shoot,
}

impl PlayerAction {
// The `strum` crate provides a deriveable trait for this!
fn variants() -> &'static [PlayerAction] {
&[Self::Move, Self::Look, Self::Shoot]
}
}

// Exhaustively match `PlayerAction` and define the default binding to the input
impl PlayerAction {
fn default_gamepad_binding(&self) -> UserInput {
Expand All @@ -51,7 +58,7 @@ impl PlayerAction {
}
}

fn default_mkb_binding(&self) -> UserInput {
fn default_kbm_binding(&self) -> UserInput {
// Match against the provided action to get the correct default gamepad input
match self {
Self::Move => UserInput::VirtualDPad(VirtualDPad::wasd()),
Expand All @@ -64,8 +71,8 @@ impl PlayerAction {
let mut input_map = InputMap::default();

for variant in PlayerAction::variants() {
input_map.insert(variant, variant.default_mkb_binding());
input_map.insert(variant, variant.default_gamepad_binding());
input_map.insert(*variant, variant.default_kbm_binding());
input_map.insert(*variant, variant.default_gamepad_binding());
}
input_map
}
Expand Down
84 changes: 2 additions & 82 deletions macros/src/actionlike.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@ use proc_macro2::Span;
use proc_macro2::TokenStream;
use proc_macro_crate::{crate_name, FoundCrate};
use quote::quote;
use syn::{Data, DeriveInput, Ident};
use syn::{DeriveInput, Ident};

/// This approach and implementation is inspired by the `strum` crate,
/// Copyright (c) 2019 Peter Glotfelty
Expand Down Expand Up @@ -34,87 +34,7 @@ pub(crate) fn actionlike_inner(ast: &DeriveInput) -> TokenStream {
quote!(leafwing_input_manager)
};

let variants = match &ast.data {
Data::Enum(v) => &v.variants,
_ => panic!("`Actionlike` cannot be derived for non-enum types. Manually implement the trait instead."),
};

// Populate the array
let mut get_at_match_items = Vec::new();
let mut index_match_items = Vec::new();

for (index, variant) in variants.iter().enumerate() {
// The name of the enum variant
let variant_identifier = variant.ident.clone();

let get_at_params = match &variant.fields {
// Unit fields have no parameters
syn::Fields::Unit => quote! {},
// Use the default values for tuple-like fields
syn::Fields::Unnamed(fields) => {
let defaults = ::std::iter::repeat(quote!(::core::default::Default::default()))
.take(fields.unnamed.len());
quote! { (#(#defaults),*) }
}
// Use the default values for tuple-like fields
syn::Fields::Named(fields) => {
let fields = fields
.named
.iter()
.map(|field| field.ident.as_ref().unwrap());
quote! { {#(#fields: ::core::default::Default::default()),*} }
}
};

let index_params = match &variant.fields {
// Unit fields have no parameters
syn::Fields::Unit => quote! {},
// Use the default values for tuple-like fields
syn::Fields::Unnamed(fields) => {
let underscores = ::std::iter::repeat(quote!(_)).take(fields.unnamed.len());
quote! { (#(#underscores),*) }
}
// Use the default values for tuple-like fields
syn::Fields::Named(fields) => {
let fields = fields
.named
.iter()
.map(|field| field.ident.as_ref().unwrap());
quote! { {#(#fields: _),*} }
}
};

// Match items
get_at_match_items.push(quote! {
#index => Some(#enum_name::#variant_identifier #get_at_params),
});

index_match_items.push(quote! {
#enum_name::#variant_identifier #index_params => #index,
});
}

let n_variants = variants.iter().len();

quote! {
impl #impl_generics #crate_path::Actionlike for #enum_name #type_generics #where_clause {
fn n_variants() -> usize {
#n_variants
}

fn get_at(index: usize) -> Option<Self> {
match index {
#(#get_at_match_items)*
_ => None,
}
}

fn index(&self) -> usize {
match self {
#(#index_match_items)*
_ => unreachable!()
}
}
}
impl #impl_generics #crate_path::Actionlike for #enum_name #type_generics #where_clause {}
}
}
37 changes: 29 additions & 8 deletions src/action_state.rs
Original file line number Diff line number Diff line change
Expand Up @@ -85,8 +85,6 @@ pub struct ActionData {
#[derive(Resource, Component, Clone, Debug, PartialEq, Serialize, Deserialize, Reflect)]
pub struct ActionState<A: Actionlike> {
/// The [`ActionData`] of each action
///
/// The position in this vector corresponds to [`Actionlike::index`].
action_data: HashMap<A, ActionData>,
}

Expand Down Expand Up @@ -425,14 +423,14 @@ impl<A: Actionlike> ActionState<A> {
/// Consumes all actions
#[inline]
pub fn consume_all(&mut self) {
for action in A::variants() {
for action in self.keys() {
self.consume(&action);
}
}

/// Releases all actions
pub fn release_all(&mut self) {
for action in A::variants() {
for action in self.keys() {
self.release(&action);
}
}
Expand Down Expand Up @@ -492,25 +490,41 @@ impl<A: Actionlike> ActionState<A> {
#[must_use]
/// Which actions are currently pressed?
pub fn get_pressed(&self) -> Vec<A> {
A::variants().filter(|a| self.pressed(a)).collect()
self.action_data
.iter()
.filter(|(_action, data)| data.state.pressed())
.map(|(action, _data)| action.clone())
.collect()
}

#[must_use]
/// Which actions were just pressed?
pub fn get_just_pressed(&self) -> Vec<A> {
A::variants().filter(|a| self.just_pressed(a)).collect()
self.action_data
.iter()
.filter(|(_action, data)| data.state.just_pressed())
.map(|(action, _data)| action.clone())
.collect()
}

#[must_use]
/// Which actions are currently released?
pub fn get_released(&self) -> Vec<A> {
A::variants().filter(|a| self.released(a)).collect()
self.action_data
.iter()
.filter(|(_action, data)| data.state.released())
.map(|(action, _data)| action.clone())
.collect()
}

#[must_use]
/// Which actions were just released?
pub fn get_just_released(&self) -> Vec<A> {
A::variants().filter(|a| self.just_released(a)).collect()
self.action_data
.iter()
.filter(|(_action, data)| data.state.just_released())
.map(|(action, _data)| action.clone())
.collect()
}

/// The [`Instant`] that the action was last pressed or released
Expand Down Expand Up @@ -582,6 +596,13 @@ impl<A: Actionlike> ActionState<A> {
}
};
}

/// Returns an owned list of the [`Actionlike`] keys in this [`ActionState`].
#[inline]
#[must_use]
pub fn keys(&self) -> Vec<A> {
self.action_data.keys().cloned().collect()
}
}

/// A component that allows the attached entity to drive the [`ActionState`] of the associated entity
Expand Down
Loading

0 comments on commit 779ebc1

Please sign in to comment.