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:
PROMETHIA-27
2025-12-10 14:41:21 -05:00
committed by GitHub
parent b177c880e3
commit 668ed93475
19 changed files with 433 additions and 486 deletions

View File

@@ -74,7 +74,7 @@ pub fn plugin(app: &mut App) {
//
fn on_connected_state(mut commands: Commands, mut game_state: ResMut<NextState<GameState>>) {
commands.client_trigger(ClientEnteredPlaying::default());
commands.client_trigger(ClientEnteredPlaying);
game_state.set(GameState::Playing);
}

View File

@@ -1,8 +1,8 @@
use bevy::prelude::*;
use shared::{
GameState,
control::{ControlState, ControllerSet, LookDirMovement},
player::PlayerBodyMesh,
control::{ControllerSet, Inputs, LookDirMovement},
player::{LocalPlayer, PlayerBodyMesh},
};
use std::f32::consts::PI;
@@ -17,35 +17,42 @@ pub fn plugin(app: &mut App) {
}
fn rotate_rig(
controls: Res<ControlState>,
inputs: Single<&Inputs, With<LocalPlayer>>,
look_dir: Res<LookDirMovement>,
mut player: Query<&mut Transform, With<PlayerBodyMesh>>,
local_player: Single<&Children, With<LocalPlayer>>,
mut player_mesh: Query<&mut Transform, With<PlayerBodyMesh>>,
) {
for mut rig_transform in player.iter_mut() {
if controls.view_mode {
continue;
}
let look_dir = look_dir.0;
// todo: Make consistent with the running controller
let sensitivity = 0.001;
let max_pitch = 35.0 * PI / 180.0;
let min_pitch = -25.0 * PI / 180.0;
rig_transform.rotate_y(look_dir.x * -sensitivity);
let euler_rot = rig_transform.rotation.to_euler(EulerRot::YXZ);
let yaw = euler_rot.0;
let pitch = euler_rot.1 + look_dir.y * -sensitivity;
let pitch_clamped = pitch.clamp(min_pitch, max_pitch);
rig_transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch_clamped, 0.0);
// The following can be used to limit the amount of rotation per frame
// let target_rotation = rig_transform.rotation
// * Quat::from_rotation_x(controls.keyboard_state.look_dir.y * sensitivity);
// let clamped_rotation = rig_transform.rotation.rotate_towards(target_rotation, 0.01);
// rig_transform.rotation = clamped_rotation;
if inputs.view_mode {
return;
}
local_player.iter().find(|&child| {
if let Ok(mut rig_transform) = player_mesh.get_mut(child) {
let look_dir = look_dir.0;
// todo: Make consistent with the running controller
let sensitivity = 0.001;
let max_pitch = 35.0 * PI / 180.0;
let min_pitch = -25.0 * PI / 180.0;
rig_transform.rotate_y(look_dir.x * -sensitivity);
let euler_rot = rig_transform.rotation.to_euler(EulerRot::YXZ);
let yaw = euler_rot.0;
let pitch = euler_rot.1 + look_dir.y * -sensitivity;
let pitch_clamped = pitch.clamp(min_pitch, max_pitch);
rig_transform.rotation = Quat::from_euler(EulerRot::YXZ, yaw, pitch_clamped, 0.0);
// The following can be used to limit the amount of rotation per frame
// let target_rotation = rig_transform.rotation
// * Quat::from_rotation_x(controls.keyboard_state.look_dir.y * sensitivity);
// let clamped_rotation = rig_transform.rotation.rotate_towards(target_rotation, 0.01);
// rig_transform.rotation = clamped_rotation;
true
} else {
false
}
});
}

View File

@@ -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);
}
}

View File

@@ -1,17 +1,11 @@
use crate::GameState;
use bevy::prelude::*;
use bevy_replicon::client::ClientSystems;
use shared::control::{ControlState, ControllerSet};
use shared::control::ControllerSet;
mod controller_flying;
pub mod controls;
#[derive(Resource, Debug, Default)]
struct Controls {
keyboard_state: ControlState,
gamepad_state: Option<ControlState>,
}
#[derive(Resource, Debug, PartialEq, Eq)]
pub enum CharacterInputEnabled {
On,

View File

@@ -5,23 +5,16 @@ use crate::{
};
use bevy::prelude::*;
use shared::{
player::{LocalPlayerId, PlayerBodyMesh},
player::{LocalPlayer, PlayerBodyMesh},
protocol::{PlaySound, PlayerId, events::ClientHeadChanged, messages::AssignClientPlayer},
};
pub fn plugin(app: &mut App) {
app.register_type::<LocalPlayerId>();
app.register_type::<LocalPlayer>();
app.init_state::<PlayerAssignmentState>();
app.add_systems(
Update,
receive_player_id.run_if(in_state(PlayerAssignmentState::Waiting)),
);
app.add_systems(
Update,
match_player_id.run_if(in_state(PlayerAssignmentState::IdReceived)),
receive_player_id.run_if(not(in_state(PlayerAssignmentState::Confirmed))),
);
global_observer!(app, on_update_head_mesh);
@@ -31,29 +24,26 @@ fn receive_player_id(
mut commands: Commands,
mut client_assignments: MessageReader<AssignClientPlayer>,
mut next: ResMut<NextState<PlayerAssignmentState>>,
mut local_id: Local<Option<PlayerId>>,
players: Query<(Entity, &PlayerId), Changed<PlayerId>>,
) {
for &AssignClientPlayer(id) in client_assignments.read() {
commands.insert_resource(LocalPlayerId { id });
next.set(PlayerAssignmentState::IdReceived);
info!("player id `{}` received", id.id);
}
}
fn match_player_id(
mut commands: Commands,
players: Query<(Entity, &PlayerId), Changed<PlayerId>>,
client: Res<LocalPlayerId>,
mut next: ResMut<NextState<PlayerAssignmentState>>,
) {
for (entity, player_id) in players.iter() {
if *player_id == client.id {
commands.entity(entity).insert(LocalPlayer);
next.set(PlayerAssignmentState::Confirmed);
info!(
"player entity {entity:?} confirmed with id `{}`",
player_id.id
);
break;
*local_id = Some(id);
}
if let Some(local_id) = *local_id {
for (entity, player_id) in players.iter() {
if *player_id == local_id {
commands.entity(entity).insert(LocalPlayer);
next.set(PlayerAssignmentState::Confirmed);
info!(
"player entity {entity:?} confirmed with id `{}`",
player_id.id
);
break;
}
}
}
}
@@ -64,19 +54,13 @@ fn match_player_id(
/// controller it owns.
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, States)]
pub enum PlayerAssignmentState {
/// Waiting for the server to send an [`AssignClientPlayer`] message
/// Waiting for the server to send an [`AssignClientPlayer`] message and replicate a [`PlayerId`]
#[default]
Waiting,
/// Received an [`AssignClientPlayer`], querying for a matching controller
IdReceived,
/// Matching controller confirmed; a [`LocalPlayer`] exists
Confirmed,
}
#[derive(Component, Debug, Reflect)]
#[reflect(Component)]
pub struct LocalPlayer;
fn on_update_head_mesh(
trigger: On<ClientHeadChanged>,
mut commands: Commands,