move clientside gated parts of control to the client (#70)

This commit is contained in:
PROMETHIA-27
2025-10-08 18:54:39 -04:00
committed by GitHub
parent 8f24f4e03a
commit 11684b80a9
8 changed files with 104 additions and 115 deletions

View File

@@ -3,7 +3,7 @@ use crate::{
GameState,
abilities::TriggerStateRes,
animation::AnimationFlags,
control::{ControlState, SelectedController, controls::ControllerSettings},
control::{ControlState, ControllerSettings, SelectedController},
head::ActiveHead,
heads_database::{HeadControls, HeadsDatabase},
physics_layers::GameLayer,

View File

@@ -2,7 +2,7 @@ use super::{ControlState, ControllerSet};
use crate::{
GameState,
animation::AnimationFlags,
control::{controller_common::MovementSpeedFactor, controls::ControllerSettings},
control::{ControllerSettings, controller_common::MovementSpeedFactor},
player::PlayerBodyMesh,
};
use bevy::prelude::*;

View File

@@ -1,377 +0,0 @@
use super::{ControlState, Controls};
#[cfg(feature = "client")]
use crate::control::ControllerSet;
use crate::{GameState, control::CharacterInputEnabled};
#[cfg(feature = "client")]
use bevy::input::{
ButtonState,
gamepad::{GamepadConnection, GamepadEvent},
mouse::{MouseButtonInput, MouseMotion},
};
use bevy::prelude::*;
#[cfg(feature = "client")]
use lightyear::input::client::InputSet;
#[cfg(feature = "client")]
use lightyear::prelude::input::native::{ActionState, InputMarker};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
#[cfg(feature = "client")]
use std::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.register_type::<ControllerSettings>();
app.add_systems(PreUpdate, (cache_keyboard_state, cache_gamepad_state));
#[cfg(feature = "client")]
{
app.add_systems(
FixedPreUpdate,
(
gamepad_controls,
keyboard_controls,
mouse_rotate,
mouse_click,
gamepad_connections.run_if(on_event::<GamepadEvent>),
combine_controls,
clear_keyboard_state,
clear_gamepad_state,
)
.chain()
.in_set(ControllerSet::CollectInputs)
.before(InputSet::WriteClientInputs)
.run_if(
in_state(GameState::Playing)
.and(resource_exists_and_equals(CharacterInputEnabled::On)),
),
)
.add_systems(
FixedPreUpdate,
buffer_inputs.in_set(InputSet::WriteClientInputs),
);
}
app.add_systems(
Update,
reset_control_state_on_disable.run_if(in_state(GameState::Playing)),
);
}
#[derive(Component, Clone, PartialEq, Reflect, Serialize, Deserialize)]
#[reflect(Component)]
pub struct ControllerSettings {
pub deceleration_factor: f32,
pub jump_force: f32,
}
#[cfg(feature = "client")]
/// Write inputs from combined keyboard/gamepad state into the networked input buffer
/// for the local player.
fn buffer_inputs(
mut player: Single<&mut ActionState<ControlState>, With<InputMarker<ControlState>>>,
controls: Res<ControlState>,
) {
player.0 = *controls;
}
/// 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>,
) {
if state.is_changed() && matches!(*state, CharacterInputEnabled::Off) {
*controls = Controls::default();
*control_state = ControlState::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() }
}
}
#[cfg(feature = "client")]
impl<Button: Hash + Eq> InputStateCache<Button> {
fn clear(&mut self) {
for state in self.map.values_mut() {
*state = InputState::default();
}
}
fn pressed(&self, button: Button) -> bool {
self.map
.get(&button)
.map(|state| state.pressed)
.unwrap_or_default()
}
fn just_pressed(&self, button: Button) -> bool {
self.map
.get(&button)
.map(|state| state.just_pressed)
.unwrap_or_default()
}
}
#[derive(Default)]
struct InputState {
pressed: bool,
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().pressed |= keyboard.pressed(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);
}
#[cfg(feature = "client")]
fn clear_keyboard_state(mut cache: ResMut<InputStateCache<KeyCode>>) {
cache.clear();
}
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().pressed |= gamepad.pressed(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);
}
}
#[cfg(feature = "client")]
fn clear_gamepad_state(mut caches: Query<&mut InputStateCache<GamepadButton>>) {
for mut cache in caches.iter_mut() {
cache.clear();
}
}
#[cfg(feature = "client")]
/// 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 = gamepad.look_dir + keyboard.look_dir;
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;
}
#[cfg(feature = "client")]
/// Applies a square deadzone to a Vec2
fn deadzone_square(v: Vec2, min: f32) -> Vec2 {
Vec2::new(
if v.x.abs() < min { 0. } else { v.x },
if v.y.abs() < min { 0. } else { v.y },
)
}
#[cfg(feature = "client")]
/// Collect gamepad inputs
fn gamepad_controls(
gamepads: Query<(Entity, &Gamepad, &InputStateCache<GamepadButton>)>,
mut controls: ResMut<Controls>,
) {
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();
// 8BitDo Ultimate wireless Controller for PC
let look_dir = 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 state = ControlState {
move_dir: deadzone_square(gamepad.left_stick(), deadzone_left_stick),
look_dir,
jump: cache.pressed(GamepadButton::South),
view_mode: cache.pressed(GamepadButton::LeftTrigger2),
trigger: cache.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),
};
if controls
.gamepad_state
.as_ref()
.map(|last_state| *last_state != state)
.unwrap_or(true)
{
controls.gamepad_state = Some(state);
}
}
#[cfg(feature = "client")]
/// Collect mouse movement input
fn mouse_rotate(mut mouse: EventReader<MouseMotion>, mut controls: ResMut<Controls>) {
controls.keyboard_state.look_dir = Vec2::ZERO;
for ev in mouse.read() {
controls.keyboard_state.look_dir += ev.delta;
}
}
#[cfg(feature = "client")]
/// Collect keyboard input
fn keyboard_controls(
keyboard: Res<ButtonInput<KeyCode>>,
cache: Res<InputStateCache<KeyCode>>,
mut controls: ResMut<Controls>,
) {
let up_binds = [KeyCode::KeyW, KeyCode::ArrowUp];
let down_binds = [KeyCode::KeyS, KeyCode::ArrowDown];
let left_binds = [KeyCode::KeyA, KeyCode::ArrowLeft];
let right_binds = [KeyCode::KeyD, KeyCode::ArrowRight];
let up = keyboard.any_pressed(up_binds);
let down = keyboard.any_pressed(down_binds);
let left = keyboard.any_pressed(left_binds);
let right = keyboard.any_pressed(right_binds);
let horizontal = right as i8 - left as i8;
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 = cache.pressed(KeyCode::Space);
controls.keyboard_state.view_mode = cache.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);
}
#[cfg(feature = "client")]
/// Collect mouse button input when pressed
fn mouse_click(mut events: EventReader<MouseButtonInput>, mut controls: ResMut<Controls>) {
controls.keyboard_state.just_triggered = false;
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;
}
_ => {}
}
}
}
#[cfg(feature = "client")]
/// Receive gamepad connections and disconnections
fn gamepad_connections(mut evr_gamepad: EventReader<GamepadEvent>) {
for ev in evr_gamepad.read() {
if let GamepadEvent::Connection(connection) = ev {
match &connection.connection {
GamepadConnection::Connected {
name,
vendor_id,
product_id,
} => {
info!(
"New gamepad connected: {:?}, name: {name}, vendor: {vendor_id:?}, product: {product_id:?}",
connection.gamepad,
);
}
GamepadConnection::Disconnected => {
info!("Lost connection with gamepad: {:?}", connection.gamepad);
}
}
}
}
}

View File

@@ -1,25 +1,60 @@
use bevy::{ecs::entity::MapEntities, prelude::*};
use serde::{Deserialize, Serialize};
use crate::{
GameState,
head::ActiveHead,
heads_database::{HeadControls, HeadsDatabase},
player::Player,
};
use bevy::{ecs::entity::MapEntities, prelude::*};
use serde::{Deserialize, Serialize};
pub mod controller_common;
pub mod controller_flying;
pub mod controller_running;
pub mod controls;
#[derive(SystemSet, Debug, Hash, PartialEq, Eq, Clone, Default)]
enum ControllerSet {
pub enum ControllerSet {
CollectInputs,
ApplyControlsFly,
#[default]
ApplyControlsRun,
}
#[derive(Event)]
pub struct ControllerSwitchEvent;
#[derive(Resource, Debug, Default, PartialEq)]
pub struct SelectedController(pub ControllerSet);
pub fn plugin(app: &mut App) {
app.register_type::<ControllerSettings>();
app.init_resource::<SelectedController>();
app.init_resource::<ControlState>();
app.add_event::<ControllerSwitchEvent>();
app.add_plugins(controller_common::plugin);
app.add_plugins(controller_flying::CharacterControllerPlugin);
app.add_plugins(controller_running::CharacterControllerPlugin);
app.configure_sets(
FixedUpdate,
(
ControllerSet::ApplyControlsFly.run_if(resource_equals(SelectedController(
ControllerSet::ApplyControlsFly,
))),
ControllerSet::ApplyControlsRun.run_if(resource_equals(SelectedController(
ControllerSet::ApplyControlsRun,
))),
)
.chain()
.run_if(in_state(GameState::Playing)),
);
app.add_systems(Update, head_change.run_if(in_state(GameState::Playing)));
}
#[derive(Resource, Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, Reflect)]
pub struct ControlState {
/// Movement direction with a maximum length of 1.0
@@ -43,57 +78,11 @@ impl MapEntities for ControlState {
fn map_entities<E: EntityMapper>(&mut self, _entity_mapper: &mut E) {}
}
#[derive(Resource, Debug, Default)]
struct Controls {
#[cfg(feature = "client")]
keyboard_state: ControlState,
#[cfg(feature = "client")]
gamepad_state: Option<ControlState>,
}
#[derive(Resource, Debug, PartialEq, Eq)]
pub enum CharacterInputEnabled {
On,
Off,
}
#[derive(Event)]
pub struct ControllerSwitchEvent;
#[derive(Resource, Debug, Default, PartialEq)]
pub struct SelectedController(ControllerSet);
pub fn plugin(app: &mut App) {
app.init_resource::<SelectedController>();
app.init_resource::<ControlState>();
app.insert_resource(CharacterInputEnabled::On);
app.add_plugins(controls::plugin);
app.add_plugins(controller_common::plugin);
app.add_plugins(controller_flying::CharacterControllerPlugin);
app.add_plugins(controller_running::CharacterControllerPlugin);
app.add_event::<ControllerSwitchEvent>();
app.configure_sets(
FixedPreUpdate,
ControllerSet::CollectInputs.run_if(in_state(GameState::Playing)),
)
.configure_sets(
FixedUpdate,
(
ControllerSet::ApplyControlsFly.run_if(resource_equals(SelectedController(
ControllerSet::ApplyControlsFly,
))),
ControllerSet::ApplyControlsRun.run_if(resource_equals(SelectedController(
ControllerSet::ApplyControlsRun,
))),
)
.chain()
.run_if(in_state(GameState::Playing)),
);
app.add_systems(Update, head_change.run_if(in_state(GameState::Playing)));
#[derive(Component, Clone, PartialEq, Reflect, Serialize, Deserialize)]
#[reflect(Component)]
pub struct ControllerSettings {
pub deceleration_factor: f32,
pub jump_force: f32,
}
fn head_change(

View File

@@ -12,9 +12,8 @@ use crate::{
cash::CashResource,
character::{self, AnimatedCharacter},
control::{
ControlState,
ControlState, ControllerSettings,
controller_common::{MovementSpeedFactor, PlayerCharacterController},
controls::ControllerSettings,
},
cutscene::StartCutscene,
global_observer,