use bevy::prelude::*;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::hash::Hash;
pub trait Actionlike: Clone + Copy + PartialEq + Eq + Hash + Send + Sync + 'static {
fn all() -> Vec<Self>;
fn display_name(&self) -> &'static str;
fn is_remappable(&self) -> bool {
true
}
fn is_required(&self) -> bool {
false
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize, Reflect)]
pub enum GameAction {
Confirm,
Cancel,
Pause,
Select,
Up,
Down,
Left,
Right,
LookUp,
LookDown,
LookLeft,
LookRight,
Primary,
Secondary,
LeftShoulder,
RightShoulder,
LeftTrigger,
RightTrigger,
PageLeft,
PageRight,
Custom1,
Custom2,
Custom3,
Custom4,
}
impl Actionlike for GameAction {
fn all() -> Vec<Self> {
vec![
Self::Confirm,
Self::Cancel,
Self::Pause,
Self::Select,
Self::Up,
Self::Down,
Self::Left,
Self::Right,
Self::LookUp,
Self::LookDown,
Self::LookLeft,
Self::LookRight,
Self::Primary,
Self::Secondary,
Self::LeftShoulder,
Self::RightShoulder,
Self::LeftTrigger,
Self::RightTrigger,
Self::PageLeft,
Self::PageRight,
Self::Custom1,
Self::Custom2,
Self::Custom3,
Self::Custom4,
]
}
fn display_name(&self) -> &'static str {
match self {
Self::Confirm => "Confirm",
Self::Cancel => "Cancel",
Self::Pause => "Pause",
Self::Select => "Select",
Self::Up => "Up",
Self::Down => "Down",
Self::Left => "Left",
Self::Right => "Right",
Self::LookUp => "Look Up",
Self::LookDown => "Look Down",
Self::LookLeft => "Look Left",
Self::LookRight => "Look Right",
Self::Primary => "Primary Action",
Self::Secondary => "Secondary Action",
Self::LeftShoulder => "Left Shoulder",
Self::RightShoulder => "Right Shoulder",
Self::LeftTrigger => "Left Trigger",
Self::RightTrigger => "Right Trigger",
Self::PageLeft => "Page Left",
Self::PageRight => "Page Right",
Self::Custom1 => "Custom 1",
Self::Custom2 => "Custom 2",
Self::Custom3 => "Custom 3",
Self::Custom4 => "Custom 4",
}
}
fn is_remappable(&self) -> bool {
!matches!(self, Self::Pause) }
fn is_required(&self) -> bool {
matches!(self, Self::Confirm | Self::Cancel | Self::Pause)
}
}
impl GameAction {
#[must_use]
pub fn all() -> &'static [GameAction] {
&[
Self::Confirm,
Self::Cancel,
Self::Pause,
Self::Select,
Self::Up,
Self::Down,
Self::Left,
Self::Right,
Self::LookUp,
Self::LookDown,
Self::LookLeft,
Self::LookRight,
Self::Primary,
Self::Secondary,
Self::LeftShoulder,
Self::RightShoulder,
Self::LeftTrigger,
Self::RightTrigger,
Self::PageLeft,
Self::PageRight,
Self::Custom1,
Self::Custom2,
Self::Custom3,
Self::Custom4,
]
}
#[must_use]
#[deprecated(since = "0.2.0", note = "Use Actionlike::display_name() instead")]
pub const fn display_name_legacy(self) -> &'static str {
match self {
Self::Confirm => "Confirm",
Self::Cancel => "Cancel",
Self::Pause => "Pause",
Self::Select => "Select",
Self::Up => "Up",
Self::Down => "Down",
Self::Left => "Left",
Self::Right => "Right",
Self::LookUp => "Look Up",
Self::LookDown => "Look Down",
Self::LookLeft => "Look Left",
Self::LookRight => "Look Right",
Self::Primary => "Primary Action",
Self::Secondary => "Secondary Action",
Self::LeftShoulder => "Left Shoulder",
Self::RightShoulder => "Right Shoulder",
Self::LeftTrigger => "Left Trigger",
Self::RightTrigger => "Right Trigger",
Self::PageLeft => "Page Left",
Self::PageRight => "Page Right",
Self::Custom1 => "Custom 1",
Self::Custom2 => "Custom 2",
Self::Custom3 => "Custom 3",
Self::Custom4 => "Custom 4",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum InputBinding {
GamepadButton(GamepadButton),
GamepadAxis(GamepadAxis, AxisDirection),
Key(KeyCode),
MouseButton(MouseButton),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
pub enum AxisDirection {
Positive,
Negative,
}
#[derive(Debug, Clone, Resource, Serialize, Deserialize, Reflect)]
#[reflect(Resource)]
pub struct ActionMap {
#[reflect(ignore)]
#[serde(skip)]
pub gamepad_bindings: HashMap<GameAction, Vec<GamepadButton>>,
#[reflect(ignore)]
#[serde(skip)]
pub axis_bindings: HashMap<GameAction, Vec<(GamepadAxis, AxisDirection, f32)>>,
#[reflect(ignore)]
#[serde(skip)]
pub key_bindings: HashMap<GameAction, Vec<KeyCode>>,
#[reflect(ignore)]
#[serde(skip)]
pub mouse_bindings: HashMap<GameAction, Vec<MouseButton>>,
}
impl Default for ActionMap {
fn default() -> Self {
let mut map = Self {
gamepad_bindings: HashMap::new(),
axis_bindings: HashMap::new(),
key_bindings: HashMap::new(),
mouse_bindings: HashMap::new(),
};
map.bind_gamepad(GameAction::Confirm, GamepadButton::South);
map.bind_gamepad(GameAction::Cancel, GamepadButton::East);
map.bind_gamepad(GameAction::Pause, GamepadButton::Start);
map.bind_gamepad(GameAction::Select, GamepadButton::Select);
map.bind_gamepad(GameAction::Primary, GamepadButton::West);
map.bind_gamepad(GameAction::Secondary, GamepadButton::North);
map.bind_gamepad(GameAction::LeftShoulder, GamepadButton::LeftTrigger);
map.bind_gamepad(GameAction::RightShoulder, GamepadButton::RightTrigger);
map.bind_gamepad(GameAction::LeftTrigger, GamepadButton::LeftTrigger2);
map.bind_gamepad(GameAction::RightTrigger, GamepadButton::RightTrigger2);
map.bind_gamepad(GameAction::PageLeft, GamepadButton::LeftTrigger);
map.bind_gamepad(GameAction::PageRight, GamepadButton::RightTrigger);
map.bind_gamepad(GameAction::Up, GamepadButton::DPadUp);
map.bind_gamepad(GameAction::Down, GamepadButton::DPadDown);
map.bind_gamepad(GameAction::Left, GamepadButton::DPadLeft);
map.bind_gamepad(GameAction::Right, GamepadButton::DPadRight);
map.bind_axis(
GameAction::Up,
GamepadAxis::LeftStickY,
AxisDirection::Positive,
0.5,
);
map.bind_axis(
GameAction::Down,
GamepadAxis::LeftStickY,
AxisDirection::Negative,
0.5,
);
map.bind_axis(
GameAction::Left,
GamepadAxis::LeftStickX,
AxisDirection::Negative,
0.5,
);
map.bind_axis(
GameAction::Right,
GamepadAxis::LeftStickX,
AxisDirection::Positive,
0.5,
);
map.bind_axis(
GameAction::LookUp,
GamepadAxis::RightStickY,
AxisDirection::Positive,
0.5,
);
map.bind_axis(
GameAction::LookDown,
GamepadAxis::RightStickY,
AxisDirection::Negative,
0.5,
);
map.bind_axis(
GameAction::LookLeft,
GamepadAxis::RightStickX,
AxisDirection::Negative,
0.5,
);
map.bind_axis(
GameAction::LookRight,
GamepadAxis::RightStickX,
AxisDirection::Positive,
0.5,
);
map.bind_key(GameAction::Confirm, KeyCode::Enter);
map.bind_key(GameAction::Confirm, KeyCode::Space);
map.bind_key(GameAction::Cancel, KeyCode::Escape);
map.bind_key(GameAction::Pause, KeyCode::Escape);
map.bind_key(GameAction::Up, KeyCode::ArrowUp);
map.bind_key(GameAction::Up, KeyCode::KeyW);
map.bind_key(GameAction::Down, KeyCode::ArrowDown);
map.bind_key(GameAction::Down, KeyCode::KeyS);
map.bind_key(GameAction::Left, KeyCode::ArrowLeft);
map.bind_key(GameAction::Left, KeyCode::KeyA);
map.bind_key(GameAction::Right, KeyCode::ArrowRight);
map.bind_key(GameAction::Right, KeyCode::KeyD);
map.bind_key(GameAction::PageLeft, KeyCode::KeyQ);
map.bind_key(GameAction::PageRight, KeyCode::KeyE);
map
}
}
impl ActionMap {
pub fn bind_gamepad(&mut self, action: GameAction, button: GamepadButton) {
self.gamepad_bindings
.entry(action)
.or_default()
.push(button);
}
pub fn bind_axis(
&mut self,
action: GameAction,
axis: GamepadAxis,
direction: AxisDirection,
threshold: f32,
) {
self.axis_bindings
.entry(action)
.or_default()
.push((axis, direction, threshold));
}
pub fn bind_key(&mut self, action: GameAction, key: KeyCode) {
self.key_bindings.entry(action).or_default().push(key);
}
pub fn bind_mouse(&mut self, action: GameAction, button: MouseButton) {
self.mouse_bindings.entry(action).or_default().push(button);
}
pub fn clear_bindings(&mut self, action: GameAction) {
self.gamepad_bindings.remove(&action);
self.axis_bindings.remove(&action);
self.key_bindings.remove(&action);
self.mouse_bindings.remove(&action);
}
pub fn clear_gamepad_bindings(&mut self, action: GameAction) {
self.gamepad_bindings.remove(&action);
self.axis_bindings.remove(&action);
}
#[must_use]
pub fn primary_gamepad_button(&self, action: GameAction) -> Option<GamepadButton> {
self.gamepad_bindings
.get(&action)
.and_then(|buttons| buttons.first().copied())
}
}
#[derive(Debug, Clone, Default, Resource, Reflect)]
#[reflect(Resource)]
pub struct ActionState {
#[reflect(ignore)]
pressed: HashMap<GameAction, bool>,
#[reflect(ignore)]
just_pressed: HashMap<GameAction, bool>,
#[reflect(ignore)]
just_released: HashMap<GameAction, bool>,
#[reflect(ignore)]
values: HashMap<GameAction, f32>,
}
impl ActionState {
#[must_use]
pub fn pressed(&self, action: GameAction) -> bool {
self.pressed.get(&action).copied().unwrap_or(false)
}
#[must_use]
pub fn just_pressed(&self, action: GameAction) -> bool {
self.just_pressed.get(&action).copied().unwrap_or(false)
}
#[must_use]
pub fn just_released(&self, action: GameAction) -> bool {
self.just_released.get(&action).copied().unwrap_or(false)
}
#[must_use]
pub fn value(&self, action: GameAction) -> f32 {
self.values.get(&action).copied().unwrap_or(0.0)
}
pub(crate) fn reset_frame_state(&mut self) {
self.just_pressed.clear();
self.just_released.clear();
}
pub(crate) fn set_pressed(&mut self, action: GameAction, pressed: bool) {
let was_pressed = self.pressed.get(&action).copied().unwrap_or(false);
if pressed && !was_pressed {
self.just_pressed.insert(action, true);
} else if !pressed && was_pressed {
self.just_released.insert(action, true);
}
self.pressed.insert(action, pressed);
}
pub(crate) fn set_value(&mut self, action: GameAction, value: f32) {
self.values.insert(action, value.clamp(0.0, 1.0));
}
}
pub fn update_action_state(
mut state: ResMut<ActionState>,
action_map: Res<ActionMap>,
keyboard: Res<ButtonInput<KeyCode>>,
mouse_buttons: Res<ButtonInput<MouseButton>>,
gamepads: Query<&Gamepad>,
) {
state.reset_frame_state();
for action in GameAction::all() {
let mut pressed = false;
let mut value = 0.0f32;
if let Some(keys) = action_map.key_bindings.get(action) {
for key in keys {
if keyboard.pressed(*key) {
pressed = true;
value = 1.0;
break;
}
}
}
if !pressed && let Some(buttons) = action_map.mouse_bindings.get(action) {
for button in buttons {
if mouse_buttons.pressed(*button) {
pressed = true;
value = 1.0;
break;
}
}
}
if !pressed {
for gamepad in gamepads.iter() {
if let Some(buttons) = action_map.gamepad_bindings.get(action) {
for button_type in buttons {
if gamepad.pressed(*button_type) {
pressed = true;
value = 1.0;
break;
}
}
}
if !pressed && let Some(axes) = action_map.axis_bindings.get(action) {
for (axis_type, direction, threshold) in axes {
if let Some(axis_value) = gamepad.get(*axis_type) {
let check_value = match direction {
AxisDirection::Positive => axis_value,
AxisDirection::Negative => -axis_value,
};
if check_value > *threshold {
pressed = true;
value = value.max(check_value);
}
}
}
}
if pressed {
break;
}
}
}
state.set_pressed(*action, pressed);
state.set_value(*action, value);
}
}
pub(crate) fn register_action_types(app: &mut App) {
app.register_type::<GameAction>()
.register_type::<ActionMap>()
.register_type::<ActionState>()
.init_resource::<ActionMap>()
.init_resource::<ActionState>();
}
pub(crate) fn add_action_systems(app: &mut App) {
app.add_systems(PreUpdate, update_action_state);
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_game_action_all_contains_all_variants() {
let all_actions = GameAction::all();
assert!(all_actions.contains(&GameAction::Confirm));
assert!(all_actions.contains(&GameAction::Cancel));
assert!(all_actions.contains(&GameAction::Custom4));
assert_eq!(all_actions.len(), 24);
}
#[test]
fn test_game_action_display_names() {
assert_eq!(GameAction::Confirm.display_name(), "Confirm");
assert_eq!(GameAction::LeftTrigger.display_name(), "Left Trigger");
assert_eq!(GameAction::Custom1.display_name(), "Custom 1");
}
#[test]
fn test_game_action_remappable() {
assert!(GameAction::Confirm.is_remappable());
assert!(GameAction::Primary.is_remappable());
assert!(!GameAction::Pause.is_remappable());
}
#[test]
fn test_game_action_required() {
assert!(GameAction::Confirm.is_required());
assert!(GameAction::Cancel.is_required());
assert!(!GameAction::Custom1.is_required());
assert!(!GameAction::Custom4.is_required());
}
#[test]
fn test_action_binding_new() {
let binding = InputBinding::GamepadButton(GamepadButton::South);
assert!(matches!(binding, InputBinding::GamepadButton(_)));
}
#[test]
fn test_action_binding_matches_button() {
let binding = InputBinding::GamepadButton(GamepadButton::South);
if let InputBinding::GamepadButton(btn) = binding {
assert_eq!(btn, GamepadButton::South);
}
}
#[test]
fn test_action_binding_matches_key() {
let binding = InputBinding::Key(KeyCode::Space);
if let InputBinding::Key(key) = binding {
assert_eq!(key, KeyCode::Space);
}
}
#[test]
fn test_action_map_default_bindings() {
let map = ActionMap::default();
assert!(map.primary_gamepad_button(GameAction::Confirm).is_some());
assert!(map.primary_gamepad_button(GameAction::Cancel).is_some());
}
#[test]
fn test_action_map_bind_gamepad() {
let mut map = ActionMap::default();
map.bind_gamepad(GameAction::Custom1, GamepadButton::West);
let button = map.primary_gamepad_button(GameAction::Custom1);
assert_eq!(button, Some(GamepadButton::West));
}
#[test]
fn test_action_map_bind_key() {
let mut map = ActionMap::default();
map.bind_key(GameAction::Custom2, KeyCode::KeyG);
let bindings = &map.key_bindings[&GameAction::Custom2];
assert!(bindings.contains(&KeyCode::KeyG));
}
#[test]
fn test_action_map_bind_mouse() {
let mut map = ActionMap::default();
map.bind_mouse(GameAction::Primary, MouseButton::Left);
assert!(map.mouse_bindings.contains_key(&GameAction::Primary));
}
#[test]
fn test_action_map_clear_bindings() {
let mut map = ActionMap::default();
map.bind_key(GameAction::Custom3, KeyCode::KeyH);
map.clear_bindings(GameAction::Custom3);
assert!(
map.key_bindings
.get(&GameAction::Custom3)
.map_or(true, |v| v.is_empty())
);
}
#[test]
fn test_action_state_pressed() {
let mut state = ActionState::default();
state.set_pressed(GameAction::Confirm, true);
assert!(state.pressed(GameAction::Confirm));
assert!(!state.pressed(GameAction::Cancel));
}
#[test]
fn test_action_state_just_pressed() {
let mut state = ActionState::default();
state.just_pressed.insert(GameAction::Primary, true);
assert!(state.just_pressed(GameAction::Primary));
assert!(!state.just_pressed(GameAction::Secondary));
}
#[test]
fn test_action_state_just_released() {
let mut state = ActionState::default();
state.just_released.insert(GameAction::LeftShoulder, true);
assert!(state.just_released(GameAction::LeftShoulder));
assert!(!state.just_released(GameAction::RightShoulder));
}
#[test]
fn test_action_state_value() {
let mut state = ActionState::default();
state.set_value(GameAction::LeftTrigger, 0.75);
assert_eq!(state.value(GameAction::LeftTrigger), 0.75);
assert_eq!(state.value(GameAction::RightTrigger), 0.0);
}
#[test]
fn test_action_state_set_pressed_updates_state() {
let mut state = ActionState::default();
state.set_pressed(GameAction::Confirm, true);
assert!(state.pressed(GameAction::Confirm));
state.set_pressed(GameAction::Confirm, false);
assert!(!state.pressed(GameAction::Confirm));
}
#[test]
fn test_axis_direction_variants() {
let pos = AxisDirection::Positive;
let neg = AxisDirection::Negative;
assert_ne!(pos, neg);
}
}