Simpler + Better Inputs (#82)
* switch to events for instantaneous inputs * input simplification + improvements * fix trail crash * fix clippy warnings * qualify `Trail` in fn signature
This commit is contained in:
@@ -1,40 +1,30 @@
|
||||
use super::{ControlState, Controls};
|
||||
use crate::{GameState, control::CharacterInputEnabled};
|
||||
use bevy::{
|
||||
input::{
|
||||
ButtonState,
|
||||
gamepad::{GamepadConnection, GamepadEvent},
|
||||
mouse::{MouseButtonInput, MouseMotion},
|
||||
mouse::MouseMotion,
|
||||
},
|
||||
prelude::*,
|
||||
};
|
||||
use shared::{
|
||||
control::{ControllerSet, LookDirMovement},
|
||||
player::PlayerBodyMesh,
|
||||
control::{
|
||||
BackpackLeftPressed, BackpackRightPressed, BackpackSwapPressed, BackpackTogglePressed,
|
||||
CashHealPressed, ClientInputs, ControllerSet, Inputs, LocalInputs, LookDirMovement,
|
||||
SelectLeftPressed, SelectRightPressed,
|
||||
},
|
||||
player::{LocalPlayer, PlayerBodyMesh},
|
||||
};
|
||||
use std::{collections::HashMap, hash::Hash};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.init_resource::<Controls>();
|
||||
app.init_resource::<InputStateCache<KeyCode>>();
|
||||
|
||||
app.register_required_components::<Gamepad, InputStateCache<GamepadButton>>();
|
||||
|
||||
app.add_systems(PreUpdate, (cache_keyboard_state, cache_gamepad_state));
|
||||
|
||||
app.add_systems(
|
||||
PreUpdate,
|
||||
(
|
||||
reset_lookdir,
|
||||
gamepad_controls,
|
||||
keyboard_controls,
|
||||
mouse_rotate,
|
||||
mouse_click,
|
||||
gamepad_connections.run_if(on_message::<GamepadEvent>),
|
||||
combine_controls,
|
||||
reset_lookdir,
|
||||
keyboard_controls,
|
||||
gamepad_controls,
|
||||
mouse_rotate,
|
||||
get_lookdir,
|
||||
clear_keyboard_just,
|
||||
clear_gamepad_just,
|
||||
send_inputs,
|
||||
)
|
||||
.chain()
|
||||
@@ -43,7 +33,8 @@ pub fn plugin(app: &mut App) {
|
||||
in_state(GameState::Playing)
|
||||
.and(resource_exists_and_equals(CharacterInputEnabled::On)),
|
||||
),
|
||||
);
|
||||
)
|
||||
.add_systems(PreUpdate, overwrite_local_inputs);
|
||||
|
||||
app.add_systems(
|
||||
Update,
|
||||
@@ -51,10 +42,18 @@ pub fn plugin(app: &mut App) {
|
||||
);
|
||||
}
|
||||
|
||||
/// Overwrite inputs for this client that were replicated from the server with the local inputs
|
||||
fn overwrite_local_inputs(
|
||||
mut inputs: Single<&mut Inputs, With<LocalPlayer>>,
|
||||
local_inputs: Single<&LocalInputs>,
|
||||
) {
|
||||
**inputs = local_inputs.0;
|
||||
}
|
||||
|
||||
/// Write inputs from combined keyboard/gamepad state into the networked input buffer
|
||||
/// for the local player.
|
||||
fn send_inputs(mut writer: MessageWriter<ControlState>, controls: Res<ControlState>) {
|
||||
writer.write(*controls);
|
||||
fn send_inputs(mut writer: MessageWriter<ClientInputs>, local_inputs: Single<&LocalInputs>) {
|
||||
writer.write(ClientInputs(local_inputs.0));
|
||||
}
|
||||
|
||||
fn reset_lookdir(mut look_dir: ResMut<LookDirMovement>) {
|
||||
@@ -64,128 +63,21 @@ fn reset_lookdir(mut look_dir: ResMut<LookDirMovement>) {
|
||||
/// Reset character inputs to default when character input is disabled.
|
||||
fn reset_control_state_on_disable(
|
||||
state: Res<CharacterInputEnabled>,
|
||||
mut controls: ResMut<Controls>,
|
||||
mut control_state: ResMut<ControlState>,
|
||||
mut inputs: Single<&mut LocalInputs>,
|
||||
) {
|
||||
if state.is_changed() && matches!(*state, CharacterInputEnabled::Off) {
|
||||
*controls = Controls::default();
|
||||
*control_state = ControlState {
|
||||
look_dir: control_state.look_dir,
|
||||
inputs.0 = Inputs {
|
||||
look_dir: inputs.0.look_dir,
|
||||
..default()
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
/// Caches information that depends on the Update schedule so that it can be read safely from the Fixed schedule
|
||||
/// without losing/duplicating info
|
||||
#[derive(Component, Resource)]
|
||||
struct InputStateCache<Button> {
|
||||
map: HashMap<Button, InputState>,
|
||||
}
|
||||
|
||||
impl<Button> Default for InputStateCache<Button> {
|
||||
fn default() -> Self {
|
||||
Self { map: default() }
|
||||
}
|
||||
}
|
||||
|
||||
impl<Button: Hash + Eq> InputStateCache<Button> {
|
||||
fn clear_just(&mut self) {
|
||||
for state in self.map.values_mut() {
|
||||
state.just_pressed = false;
|
||||
}
|
||||
}
|
||||
|
||||
fn just_pressed(&self, button: Button) -> bool {
|
||||
self.map
|
||||
.get(&button)
|
||||
.map(|state| state.just_pressed)
|
||||
.unwrap_or_default()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Default)]
|
||||
struct InputState {
|
||||
just_pressed: bool,
|
||||
}
|
||||
|
||||
fn cache_keyboard_state(
|
||||
mut cache: ResMut<InputStateCache<KeyCode>>,
|
||||
keyboard: Res<ButtonInput<KeyCode>>,
|
||||
) {
|
||||
let mut cache_key = |key| {
|
||||
cache.map.entry(key).or_default().just_pressed |= keyboard.just_pressed(key);
|
||||
};
|
||||
cache_key(KeyCode::Space);
|
||||
cache_key(KeyCode::Tab);
|
||||
cache_key(KeyCode::KeyB);
|
||||
cache_key(KeyCode::Enter);
|
||||
cache_key(KeyCode::Comma);
|
||||
cache_key(KeyCode::Period);
|
||||
cache_key(KeyCode::KeyQ);
|
||||
cache_key(KeyCode::KeyE);
|
||||
}
|
||||
|
||||
fn clear_keyboard_just(mut cache: ResMut<InputStateCache<KeyCode>>) {
|
||||
cache.clear_just();
|
||||
}
|
||||
|
||||
fn cache_gamepad_state(mut gamepads: Query<(&Gamepad, &mut InputStateCache<GamepadButton>)>) {
|
||||
for (gamepad, mut cache) in gamepads.iter_mut() {
|
||||
let mut cache_button = |button| {
|
||||
cache.map.entry(button).or_default().just_pressed |= gamepad.just_pressed(button);
|
||||
};
|
||||
|
||||
cache_button(GamepadButton::North);
|
||||
cache_button(GamepadButton::East);
|
||||
cache_button(GamepadButton::South);
|
||||
cache_button(GamepadButton::West);
|
||||
cache_button(GamepadButton::DPadUp);
|
||||
cache_button(GamepadButton::DPadRight);
|
||||
cache_button(GamepadButton::DPadDown);
|
||||
cache_button(GamepadButton::DPadLeft);
|
||||
cache_button(GamepadButton::LeftTrigger);
|
||||
cache_button(GamepadButton::LeftTrigger2);
|
||||
cache_button(GamepadButton::RightTrigger);
|
||||
cache_button(GamepadButton::RightTrigger2);
|
||||
cache_button(GamepadButton::Select);
|
||||
cache_button(GamepadButton::Start);
|
||||
cache_button(GamepadButton::LeftThumb);
|
||||
cache_button(GamepadButton::RightThumb);
|
||||
}
|
||||
}
|
||||
|
||||
fn clear_gamepad_just(mut caches: Query<&mut InputStateCache<GamepadButton>>) {
|
||||
for mut cache in caches.iter_mut() {
|
||||
cache.clear_just();
|
||||
}
|
||||
}
|
||||
|
||||
/// Take keyboard and gamepad state and combine them into unified input state
|
||||
fn combine_controls(controls: Res<Controls>, mut combined_controls: ResMut<ControlState>) {
|
||||
let keyboard = controls.keyboard_state;
|
||||
let gamepad = controls.gamepad_state.unwrap_or_default();
|
||||
|
||||
combined_controls.look_dir = Vec3::NEG_Z;
|
||||
combined_controls.move_dir = gamepad.move_dir + keyboard.move_dir;
|
||||
combined_controls.jump = gamepad.jump | keyboard.jump;
|
||||
combined_controls.view_mode = gamepad.view_mode | keyboard.view_mode;
|
||||
combined_controls.trigger = keyboard.trigger | gamepad.trigger;
|
||||
combined_controls.just_triggered = keyboard.just_triggered | gamepad.just_triggered;
|
||||
combined_controls.select_left = gamepad.select_left | keyboard.select_left;
|
||||
combined_controls.select_right = gamepad.select_right | keyboard.select_right;
|
||||
combined_controls.backpack_toggle = gamepad.backpack_toggle | keyboard.backpack_toggle;
|
||||
combined_controls.backpack_swap = gamepad.backpack_swap | keyboard.backpack_swap;
|
||||
combined_controls.backpack_left = gamepad.backpack_left | keyboard.backpack_left;
|
||||
combined_controls.backpack_right = gamepad.backpack_right | keyboard.backpack_right;
|
||||
combined_controls.cash_heal = gamepad.cash_heal | keyboard.cash_heal;
|
||||
}
|
||||
|
||||
fn get_lookdir(
|
||||
mut controls: ResMut<ControlState>,
|
||||
mut inputs: Single<&mut LocalInputs>,
|
||||
rig_transform: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>,
|
||||
) {
|
||||
controls.look_dir = if let Some(ref rig_transform) = rig_transform {
|
||||
inputs.0.look_dir = if let Some(ref rig_transform) = rig_transform {
|
||||
rig_transform.forward().as_vec3()
|
||||
} else {
|
||||
Vec3::NEG_Z
|
||||
@@ -201,67 +93,78 @@ fn deadzone_square(v: Vec2, min: f32) -> Vec2 {
|
||||
}
|
||||
|
||||
/// Collect gamepad inputs
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn gamepad_controls(
|
||||
gamepads: Query<(Entity, &Gamepad, &InputStateCache<GamepadButton>)>,
|
||||
mut controls: ResMut<Controls>,
|
||||
gamepads: Query<&Gamepad>,
|
||||
mut inputs: Single<&mut LocalInputs>,
|
||||
mut look_dir: ResMut<LookDirMovement>,
|
||||
mut backpack_toggle_pressed: MessageWriter<BackpackTogglePressed>,
|
||||
mut backpack_swap_pressed: MessageWriter<BackpackSwapPressed>,
|
||||
mut backpack_left_pressed: MessageWriter<BackpackLeftPressed>,
|
||||
mut backpack_right_pressed: MessageWriter<BackpackRightPressed>,
|
||||
mut select_left_pressed: MessageWriter<SelectLeftPressed>,
|
||||
mut select_right_pressed: MessageWriter<SelectRightPressed>,
|
||||
mut cash_heal_pressed: MessageWriter<CashHealPressed>,
|
||||
) {
|
||||
let Some((_e, gamepad, cache)) = gamepads.iter().next() else {
|
||||
if controls.gamepad_state.is_some() {
|
||||
controls.gamepad_state = None;
|
||||
}
|
||||
return;
|
||||
};
|
||||
|
||||
let deadzone_left_stick = 0.15;
|
||||
let deadzone_right_stick = 0.15;
|
||||
|
||||
let rotate = gamepad
|
||||
.get(GamepadButton::RightTrigger2)
|
||||
.unwrap_or_default();
|
||||
for gamepad in gamepads.iter() {
|
||||
let rotate = gamepad
|
||||
.get(GamepadButton::RightTrigger2)
|
||||
.unwrap_or_default();
|
||||
|
||||
// 8BitDo Ultimate wireless Controller for PC
|
||||
look_dir.0 = if gamepad.vendor_id() == Some(11720) && gamepad.product_id() == Some(12306) {
|
||||
const EPSILON: f32 = 0.015;
|
||||
Vec2::new(
|
||||
if rotate < 0.5 - EPSILON {
|
||||
40. * (rotate - 0.5)
|
||||
} else if rotate > 0.5 + EPSILON {
|
||||
-40. * (rotate - 0.5)
|
||||
} else {
|
||||
0.
|
||||
},
|
||||
0.,
|
||||
)
|
||||
} else {
|
||||
deadzone_square(gamepad.right_stick(), deadzone_right_stick) * 40.
|
||||
};
|
||||
// 8BitDo Ultimate wireless Controller for PC
|
||||
look_dir.0 = if gamepad.vendor_id() == Some(11720) && gamepad.product_id() == Some(12306) {
|
||||
const EPSILON: f32 = 0.015;
|
||||
Vec2::new(
|
||||
if rotate < 0.5 - EPSILON {
|
||||
40. * (rotate - 0.5)
|
||||
} else if rotate > 0.5 + EPSILON {
|
||||
-40. * (rotate - 0.5)
|
||||
} else {
|
||||
0.
|
||||
},
|
||||
0.,
|
||||
)
|
||||
} else {
|
||||
deadzone_square(gamepad.right_stick(), deadzone_right_stick) * 40.
|
||||
};
|
||||
|
||||
let move_dir = deadzone_square(gamepad.left_stick(), deadzone_left_stick);
|
||||
let move_dir = deadzone_square(gamepad.left_stick(), deadzone_left_stick);
|
||||
|
||||
let state = ControlState {
|
||||
move_dir,
|
||||
look_dir: Vec3::NEG_Z,
|
||||
jump: gamepad.pressed(GamepadButton::South),
|
||||
view_mode: gamepad.pressed(GamepadButton::LeftTrigger2),
|
||||
trigger: gamepad.pressed(GamepadButton::RightTrigger2),
|
||||
just_triggered: cache.just_pressed(GamepadButton::RightTrigger2),
|
||||
select_left: cache.just_pressed(GamepadButton::LeftTrigger),
|
||||
select_right: cache.just_pressed(GamepadButton::RightTrigger),
|
||||
backpack_left: cache.just_pressed(GamepadButton::DPadLeft),
|
||||
backpack_right: cache.just_pressed(GamepadButton::DPadRight),
|
||||
backpack_swap: cache.just_pressed(GamepadButton::DPadDown),
|
||||
backpack_toggle: cache.just_pressed(GamepadButton::DPadUp),
|
||||
cash_heal: cache.just_pressed(GamepadButton::East),
|
||||
};
|
||||
inputs.0.move_dir += move_dir.clamp_length_max(1.0);
|
||||
inputs.0.jump |= gamepad.pressed(GamepadButton::South);
|
||||
inputs.0.view_mode |= gamepad.pressed(GamepadButton::LeftTrigger2);
|
||||
inputs.0.trigger |= gamepad.pressed(GamepadButton::RightTrigger2);
|
||||
|
||||
if controls
|
||||
.gamepad_state
|
||||
.as_ref()
|
||||
.map(|last_state| *last_state != state)
|
||||
.unwrap_or(true)
|
||||
{
|
||||
controls.gamepad_state = Some(state);
|
||||
if gamepad.just_pressed(GamepadButton::DPadUp) {
|
||||
backpack_toggle_pressed.write(BackpackTogglePressed);
|
||||
}
|
||||
|
||||
if gamepad.just_pressed(GamepadButton::DPadDown) {
|
||||
backpack_swap_pressed.write(BackpackSwapPressed);
|
||||
}
|
||||
|
||||
if gamepad.just_pressed(GamepadButton::DPadLeft) {
|
||||
backpack_left_pressed.write(BackpackLeftPressed);
|
||||
}
|
||||
|
||||
if gamepad.just_pressed(GamepadButton::DPadRight) {
|
||||
backpack_right_pressed.write(BackpackRightPressed);
|
||||
}
|
||||
|
||||
if gamepad.just_pressed(GamepadButton::LeftTrigger) {
|
||||
select_left_pressed.write(SelectLeftPressed);
|
||||
}
|
||||
|
||||
if gamepad.just_pressed(GamepadButton::RightTrigger) {
|
||||
select_right_pressed.write(SelectRightPressed);
|
||||
}
|
||||
|
||||
if gamepad.just_pressed(GamepadButton::East) {
|
||||
cash_heal_pressed.write(CashHealPressed);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,10 +176,18 @@ fn mouse_rotate(mut mouse: MessageReader<MouseMotion>, mut look_dir: ResMut<Look
|
||||
}
|
||||
|
||||
/// Collect keyboard input
|
||||
#[allow(clippy::too_many_arguments)]
|
||||
fn keyboard_controls(
|
||||
keyboard: Res<ButtonInput<KeyCode>>,
|
||||
cache: Res<InputStateCache<KeyCode>>,
|
||||
mut controls: ResMut<Controls>,
|
||||
mouse: Res<ButtonInput<MouseButton>>,
|
||||
mut inputs: Single<&mut LocalInputs>,
|
||||
mut backpack_toggle_pressed: MessageWriter<BackpackTogglePressed>,
|
||||
mut backpack_swap_pressed: MessageWriter<BackpackSwapPressed>,
|
||||
mut backpack_left_pressed: MessageWriter<BackpackLeftPressed>,
|
||||
mut backpack_right_pressed: MessageWriter<BackpackRightPressed>,
|
||||
mut select_left_pressed: MessageWriter<SelectLeftPressed>,
|
||||
mut select_right_pressed: MessageWriter<SelectRightPressed>,
|
||||
mut cash_heal_pressed: MessageWriter<CashHealPressed>,
|
||||
) {
|
||||
let up_binds = [KeyCode::KeyW, KeyCode::ArrowUp];
|
||||
let down_binds = [KeyCode::KeyS, KeyCode::ArrowDown];
|
||||
@@ -292,41 +203,37 @@ fn keyboard_controls(
|
||||
let vertical = up as i8 - down as i8;
|
||||
let direction = Vec2::new(horizontal as f32, vertical as f32).clamp_length_max(1.0);
|
||||
|
||||
controls.keyboard_state.move_dir = direction;
|
||||
controls.keyboard_state.jump = keyboard.pressed(KeyCode::Space);
|
||||
controls.keyboard_state.view_mode = keyboard.pressed(KeyCode::Tab);
|
||||
controls.keyboard_state.backpack_toggle = cache.just_pressed(KeyCode::KeyB);
|
||||
controls.keyboard_state.backpack_swap = cache.just_pressed(KeyCode::Enter);
|
||||
controls.keyboard_state.backpack_left = cache.just_pressed(KeyCode::Comma);
|
||||
controls.keyboard_state.backpack_right = cache.just_pressed(KeyCode::Period);
|
||||
controls.keyboard_state.select_left = cache.just_pressed(KeyCode::KeyQ);
|
||||
controls.keyboard_state.select_right = cache.just_pressed(KeyCode::KeyE);
|
||||
controls.keyboard_state.cash_heal = cache.just_pressed(KeyCode::Enter);
|
||||
}
|
||||
inputs.0.move_dir = direction;
|
||||
inputs.0.jump = keyboard.pressed(KeyCode::Space);
|
||||
inputs.0.view_mode = keyboard.pressed(KeyCode::Tab);
|
||||
inputs.0.trigger = mouse.pressed(MouseButton::Left);
|
||||
|
||||
/// Collect mouse button input when pressed
|
||||
fn mouse_click(mut events: MessageReader<MouseButtonInput>, mut controls: ResMut<Controls>) {
|
||||
controls.keyboard_state.just_triggered = false;
|
||||
if keyboard.just_pressed(KeyCode::KeyB) {
|
||||
backpack_toggle_pressed.write(BackpackTogglePressed);
|
||||
}
|
||||
|
||||
for ev in events.read() {
|
||||
match ev {
|
||||
MouseButtonInput {
|
||||
button: MouseButton::Left,
|
||||
state: ButtonState::Pressed,
|
||||
..
|
||||
} => {
|
||||
controls.keyboard_state.trigger = true;
|
||||
controls.keyboard_state.just_triggered = true;
|
||||
}
|
||||
MouseButtonInput {
|
||||
button: MouseButton::Left,
|
||||
state: ButtonState::Released,
|
||||
..
|
||||
} => {
|
||||
controls.keyboard_state.trigger = false;
|
||||
}
|
||||
_ => {}
|
||||
}
|
||||
if keyboard.just_pressed(KeyCode::Enter) {
|
||||
backpack_swap_pressed.write(BackpackSwapPressed);
|
||||
}
|
||||
|
||||
if keyboard.just_pressed(KeyCode::Comma) {
|
||||
backpack_left_pressed.write(BackpackLeftPressed);
|
||||
}
|
||||
|
||||
if keyboard.just_pressed(KeyCode::Period) {
|
||||
backpack_right_pressed.write(BackpackRightPressed);
|
||||
}
|
||||
|
||||
if keyboard.just_pressed(KeyCode::KeyQ) {
|
||||
select_left_pressed.write(SelectLeftPressed);
|
||||
}
|
||||
|
||||
if keyboard.just_pressed(KeyCode::KeyE) {
|
||||
select_right_pressed.write(SelectRightPressed);
|
||||
}
|
||||
|
||||
if keyboard.just_pressed(KeyCode::Enter) {
|
||||
cash_heal_pressed.write(CashHealPressed);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user