cloudiful-bevy-input 0.1.3

Reusable Bevy input abstraction for game-defined actions.
Documentation
use crate::action_state::{ActionData, ActionState, InputSettings};
use crate::bindings::{InputAction, InputBinding, InputButton, InputMap};
use crate::device::{ActiveInputDevice, InputDevice, PrimaryGamepad, PrimaryGamepadMode};
use bevy::input::ButtonInput;
use bevy::input::gamepad::Gamepad;
use bevy::input::keyboard::KeyCode;
use bevy::prelude::*;

pub(crate) fn sync_primary_gamepad(
    mut primary: ResMut<PrimaryGamepad>,
    gamepads: Query<Entity, With<Gamepad>>,
) {
    let mut connected = gamepads.iter().collect::<Vec<_>>();
    connected.sort_unstable_by_key(|entity| entity.index());

    if let Some(selected) = primary.selected() {
        if connected.contains(&selected) {
            return;
        }
    }

    match primary.mode() {
        PrimaryGamepadMode::Auto => primary.set_selected(connected.into_iter().next()),
        PrimaryGamepadMode::Manual => primary.set_selected(None),
    }
}

pub(crate) fn update_action_state<A: InputAction>(
    map: Res<InputMap<A>>,
    settings: Res<InputSettings>,
    keyboard: Res<ButtonInput<KeyCode>>,
    primary: Res<PrimaryGamepad>,
    gamepads: Query<&Gamepad>,
    mut active_device: ResMut<ActiveInputDevice>,
    mut state: ResMut<ActionState<A>>,
) {
    let primary_gamepad = primary
        .selected()
        .and_then(|entity| gamepads.get(entity).ok());
    let mut keyboard_active = false;
    let mut gamepad_active = false;

    for action in map.actions() {
        let previous = state.data(action);
        let (value, action_keyboard_active, action_gamepad_active) = action_value(
            map.bindings(action),
            &keyboard,
            primary_gamepad,
            settings.axis_activity_threshold,
        );
        let pressed = value.abs() >= settings.action_press_threshold;
        let next = ActionData {
            pressed,
            just_pressed: !previous.pressed && pressed,
            just_released: previous.pressed && !pressed,
            value,
        };

        keyboard_active |= action_keyboard_active;
        gamepad_active |= action_gamepad_active;
        state.set(action, next);
    }

    if keyboard_active {
        active_device.set_current(Some(InputDevice::KeyboardMouse));
    } else if gamepad_active {
        active_device.set_current(primary.selected().map(InputDevice::Gamepad));
    }
}

fn action_value(
    bindings: &[InputBinding],
    keyboard: &ButtonInput<KeyCode>,
    gamepad: Option<&Gamepad>,
    axis_activity_threshold: f32,
) -> (f32, bool, bool) {
    let mut value = 0.0;
    let mut keyboard_active = false;
    let mut gamepad_active = false;

    for binding in bindings {
        let (binding_value, binding_keyboard_active, binding_gamepad_active) =
            binding_value(*binding, keyboard, gamepad, axis_activity_threshold);
        value = merge_value(value, binding_value);
        keyboard_active |= binding_keyboard_active;
        gamepad_active |= binding_gamepad_active;
    }

    (value, keyboard_active, gamepad_active)
}

fn binding_value(
    binding: InputBinding,
    keyboard: &ButtonInput<KeyCode>,
    gamepad: Option<&Gamepad>,
    axis_activity_threshold: f32,
) -> (f32, bool, bool) {
    match binding {
        InputBinding::Button(button) => {
            let pressed = button_pressed(button, keyboard, gamepad);
            let value = if pressed { 1.0 } else { 0.0 };
            let device_flags = button_flags(button, pressed);
            (value, device_flags.0, device_flags.1)
        }
        InputBinding::GamepadAxis(axis) => {
            let value = gamepad.and_then(|pad| pad.get(axis)).unwrap_or(0.0);
            (value, false, value.abs() >= axis_activity_threshold)
        }
        InputBinding::ButtonAxis { negative, positive } => {
            let negative_pressed = button_pressed(negative, keyboard, gamepad);
            let positive_pressed = button_pressed(positive, keyboard, gamepad);
            let value = match (negative_pressed, positive_pressed) {
                (true, false) => -1.0,
                (false, true) => 1.0,
                _ => 0.0,
            };
            let negative_flags = button_flags(negative, negative_pressed);
            let positive_flags = button_flags(positive, positive_pressed);
            (
                value,
                negative_flags.0 || positive_flags.0,
                negative_flags.1 || positive_flags.1,
            )
        }
    }
}

fn button_pressed(
    button: InputButton,
    keyboard: &ButtonInput<KeyCode>,
    gamepad: Option<&Gamepad>,
) -> bool {
    match button {
        InputButton::Key(key) => keyboard.pressed(key),
        InputButton::Gamepad(button) => gamepad.is_some_and(|pad| pad.pressed(button)),
    }
}

fn button_flags(button: InputButton, pressed: bool) -> (bool, bool) {
    match button {
        InputButton::Key(_) => (pressed, false),
        InputButton::Gamepad(_) => (false, pressed),
    }
}

fn merge_value(current: f32, next: f32) -> f32 {
    if next.abs() > current.abs() {
        next
    } else {
        current
    }
}

#[cfg(test)]
mod tests {
    use super::*;
    use crate::plugin::InputBindingsPlugin;
    use bevy::input::gamepad::{GamepadAxis, GamepadButton};

    #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
    enum TestAction {
        Jump,
        MoveX,
    }

    fn test_app() -> App {
        let mut app = App::new();
        app.add_plugins(MinimalPlugins)
            .add_plugins(InputBindingsPlugin::<TestAction>::default());
        app
    }

    #[test]
    fn keyboard_binding_updates_action_state() {
        let mut app = test_app();
        app.world_mut()
            .resource_mut::<InputMap<TestAction>>()
            .bind_key(TestAction::Jump, KeyCode::Space);

        app.world_mut()
            .resource_mut::<ButtonInput<KeyCode>>()
            .press(KeyCode::Space);
        app.update();

        let state = app.world().resource::<ActionState<TestAction>>();
        assert!(state.pressed(TestAction::Jump));
        assert!(state.just_pressed(TestAction::Jump));
        assert_eq!(
            app.world().resource::<ActiveInputDevice>().current(),
            Some(InputDevice::KeyboardMouse)
        );
    }

    #[test]
    fn rebind_changes_trigger_key() {
        let mut app = test_app();
        {
            let mut map = app.world_mut().resource_mut::<InputMap<TestAction>>();
            map.bind_key(TestAction::Jump, KeyCode::Space);
            assert!(map.rebind_button(
                TestAction::Jump,
                InputButton::Key(KeyCode::Space),
                InputButton::Key(KeyCode::Enter),
            ));
        }

        app.world_mut()
            .resource_mut::<ButtonInput<KeyCode>>()
            .press(KeyCode::Enter);
        app.update();

        assert!(
            app.world()
                .resource::<ActionState<TestAction>>()
                .pressed(TestAction::Jump)
        );
    }

    #[test]
    fn primary_gamepad_defaults_to_first_connected() {
        let mut app = test_app();
        let second = app.world_mut().spawn(Gamepad::default()).id();
        let first = app.world_mut().spawn(Gamepad::default()).id();

        app.update();

        let selected = app.world().resource::<PrimaryGamepad>().selected();
        let expected = if first.index() < second.index() {
            first
        } else {
            second
        };
        assert_eq!(selected, Some(expected));
    }

    #[test]
    fn gamepad_axis_updates_value_and_active_device() {
        let mut app = test_app();
        app.world_mut()
            .resource_mut::<InputMap<TestAction>>()
            .bind_gamepad_axis(TestAction::MoveX, GamepadAxis::LeftStickX);
        let gamepad = app.world_mut().spawn(Gamepad::default()).id();

        app.update();
        app.world_mut()
            .resource_mut::<PrimaryGamepad>()
            .select(gamepad);
        app.world_mut()
            .get_mut::<Gamepad>(gamepad)
            .unwrap()
            .analog_mut()
            .set(GamepadAxis::LeftStickX, 0.75);
        app.update();

        let state = app.world().resource::<ActionState<TestAction>>();
        assert_eq!(state.value(TestAction::MoveX), 0.75);
        assert!(state.pressed(TestAction::MoveX));
        assert_eq!(
            app.world().resource::<ActiveInputDevice>().current(),
            Some(InputDevice::Gamepad(gamepad))
        );
    }

    #[test]
    fn manual_primary_gamepad_is_respected() {
        let mut app = test_app();
        app.world_mut()
            .resource_mut::<InputMap<TestAction>>()
            .bind_gamepad_button(TestAction::Jump, GamepadButton::South);
        let first = app.world_mut().spawn(Gamepad::default()).id();
        let second = app.world_mut().spawn(Gamepad::default()).id();

        app.world_mut()
            .resource_mut::<PrimaryGamepad>()
            .select(second);
        app.update();

        app.world_mut()
            .get_mut::<Gamepad>(first)
            .unwrap()
            .digital_mut()
            .press(GamepadButton::South);
        app.update();
        assert!(
            !app.world()
                .resource::<ActionState<TestAction>>()
                .pressed(TestAction::Jump)
        );

        app.world_mut()
            .get_mut::<Gamepad>(second)
            .unwrap()
            .digital_mut()
            .press(GamepadButton::South);
        app.update();
        assert!(
            app.world()
                .resource::<ActionState<TestAction>>()
                .pressed(TestAction::Jump)
        );
    }
}