leafwing-input-manager 0.21.0

A powerful, flexible and ergonomic way to manage action-input keybindings for the Bevy game engine.
Documentation
#![cfg(feature = "keyboard")]

use bevy::MinimalPlugins;
use bevy::input::InputPlugin;
use bevy::prelude::*;
use bevy::time::TimeUpdateStrategy;
use leafwing_input_manager::action_state::ActionState;
use leafwing_input_manager::input_map::InputMap;
use leafwing_input_manager::plugin::InputManagerPlugin;
use leafwing_input_manager::prelude::Buttonlike;
use leafwing_input_manager_macros::Actionlike;
use std::time::Duration;

#[derive(Actionlike, Clone, Copy, Debug, Reflect, PartialEq, Eq, Hash)]
enum TestAction {
    Up,
}

#[derive(Resource, Default)]
struct UpdateCounter {
    just_pressed: usize,
}

#[derive(Resource, Default)]
struct FixedUpdateCounter {
    /// how many times did the FixedUpdate schedule run?
    run: usize,
    /// how many times did the Up button get just_pressed?
    just_pressed: usize,
}

fn build_app(fixed_timestep: Duration, frame_timestep: Duration) -> App {
    let mut app = App::new();
    app.add_plugins(MinimalPlugins)
        .add_plugins(InputPlugin)
        .add_plugins(InputManagerPlugin::<TestAction>::default())
        .init_resource::<UpdateCounter>()
        .init_resource::<FixedUpdateCounter>()
        .insert_resource(Time::<Fixed>::from_duration(fixed_timestep))
        .insert_resource(TimeUpdateStrategy::ManualDuration(frame_timestep));

    // Spawn a single input entity. The `InputMap` requires an `ActionState`,
    // so it is added automatically.
    app.world_mut().spawn(InputMap::<TestAction>::new([(
        TestAction::Up,
        KeyCode::ArrowUp,
    )]));

    app.add_systems(Update, update_counter);
    app.add_systems(FixedUpdate, fixed_update_counter);

    // we have to set an initial time for TimeUpdateStrategy::ManualDuration to work properly
    let startup = app.world().resource::<Time<Real>>().startup();
    app.world_mut()
        .resource_mut::<Time<Real>>()
        .update_with_instant(startup);
    app
}

/// Keep track of some messages that happen in the FixedUpdate schedule
/// - did the FixedUpdate schedule run during the frame?
/// - was the button marked as `just_pressed` in the FixedUpdate schedule?
fn fixed_update_counter(
    mut counter: ResMut<FixedUpdateCounter>,
    action: Single<&ActionState<TestAction>>,
) {
    if action.just_pressed(&TestAction::Up) {
        counter.just_pressed += 1;
    }
    counter.run += 1;
}

/// Keep track of some messages that happen in the Update schedule
/// - was the button marked as `just_pressed` in the Update schedule?
fn update_counter(mut counter: ResMut<UpdateCounter>, action: Single<&ActionState<TestAction>>) {
    if action.just_pressed(&TestAction::Up) {
        counter.just_pressed += 1;
    }
}

/// Returns a clone of the single [`ActionState<A>`] component in the app.
#[cfg(feature = "timing")]
fn get_action_state<A: leafwing_input_manager::Actionlike>(app: &mut App) -> ActionState<A> {
    let world = app.world_mut();
    let mut query = world.query::<&ActionState<A>>();
    query.single(world).unwrap().clone()
}

/// Reset the counters at the end of the frame
fn reset_counters(app: &mut App) {
    let mut fixed_update_counters = app.world_mut().resource_mut::<FixedUpdateCounter>();
    fixed_update_counters.run = 0;
    fixed_update_counters.just_pressed = 0;
    let mut update_counters = app.world_mut().resource_mut::<UpdateCounter>();
    update_counters.just_pressed = 0;
}

/// Assert that the FixedUpdate schedule run the expected number of times
fn check_fixed_update_run_count(app: &mut App, expected: usize) {
    assert_eq!(
        app.world()
            .get_resource::<FixedUpdateCounter>()
            .unwrap()
            .run,
        expected,
        "FixedUpdate schedule should have run {expected} times"
    );
}

/// Assert that the button was just_pressed the expected number of times during the FixedUpdate schedule
fn check_fixed_update_just_pressed_count(app: &mut App, expected: usize) {
    assert_eq!(
        app.world()
            .get_resource::<FixedUpdateCounter>()
            .unwrap()
            .just_pressed,
        expected,
        "Button should have been just_pressed {expected} times during the FixedUpdate schedule"
    );
}

/// Assert that the button was just_pressed the expected number of times during the Update schedule
fn check_update_just_pressed_count(app: &mut App, expected: usize) {
    assert_eq!(
        app.world()
            .get_resource::<UpdateCounter>()
            .unwrap()
            .just_pressed,
        expected,
        "Button should have been just_pressed {expected} times during the Update schedule"
    );
}

/// We have 2 frames without a FixedUpdate schedule in between (F1 - F2 - FU - F3)
///
/// A button pressed in F1 should still be marked as `just_pressed` in FU
#[test]
fn frame_without_fixed_timestep() {
    let mut app = build_app(Duration::from_millis(10), Duration::from_millis(9));

    KeyCode::ArrowUp.press(app.world_mut());

    // Frame 1: button is just_pressed and the FixedUpdate schedule does not run
    app.update();
    check_update_just_pressed_count(&mut app, 1);
    check_fixed_update_run_count(&mut app, 0);
    reset_counters(&mut app);

    // Frame 2: the FixedUpdate schedule should run once, the button is `just_pressed` for FixedUpdate
    // because we tick independently in FixedUpdate and Update.
    // Button is not `just_pressed` anymore in the Update schedule.
    app.update();
    check_update_just_pressed_count(&mut app, 0);
    check_fixed_update_run_count(&mut app, 1);
    check_fixed_update_just_pressed_count(&mut app, 1);
    reset_counters(&mut app);

    // Frame 3: the FixedUpdate schedule should run once, the button should now be `pressed` in both schedules
    app.update();
    check_update_just_pressed_count(&mut app, 0);
    check_fixed_update_run_count(&mut app, 1);
    check_fixed_update_just_pressed_count(&mut app, 0);
    reset_counters(&mut app);

    // make sure that the timings didn't get updated twice (once in Update and once in FixedUpdate)
    // (the `tick` function has been called twice, but it uses `Time<Real>` to update the time,
    // which is only updated in `PreUpdate`, which is what we want)
    #[cfg(feature = "timing")]
    assert_eq!(
        get_action_state::<TestAction>(&mut app).current_duration(&TestAction::Up),
        Duration::from_millis(18)
    );
}

/// We have a frame with two FixedUpdate schedule executions in between (F1 - FU1 - FU2 - F2)
///
/// A button pressed in F1 should still be marked as `just_pressed` in FU1, and should just be
/// `pressed` in FU2
#[test]
fn frame_with_two_fixed_timestep() {
    let mut app = build_app(Duration::from_millis(4), Duration::from_millis(9));

    KeyCode::ArrowUp.press(app.world_mut());

    // Frame 1: the FixedUpdate schedule should run twice, but the button should be just_pressed only once
    // in FixedUpdate
    app.update();
    check_update_just_pressed_count(&mut app, 1);
    check_fixed_update_run_count(&mut app, 2);
    check_fixed_update_just_pressed_count(&mut app, 1);
    reset_counters(&mut app);

    // Frame 2: the FixedUpdate schedule should run twice, but the button is not just_pressed anymore
    app.update();
    check_update_just_pressed_count(&mut app, 0);
    check_fixed_update_run_count(&mut app, 2);
    check_fixed_update_just_pressed_count(&mut app, 0);
    reset_counters(&mut app);

    // make sure that the timings didn't get updated twice (once in Update and once in FixedUpdate)
    // (the `tick` function has been called twice, but it uses `Time<Real>` to update the time,
    // which is only updated in `PreUpdate`, which is what we want)
    #[cfg(feature = "timing")]
    assert_eq!(
        get_action_state::<TestAction>(&mut app).current_duration(&TestAction::Up),
        Duration::from_millis(18)
    );
}