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>>) { 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); game_state.set(GameState::Playing);
} }

View File

@@ -1,8 +1,8 @@
use bevy::prelude::*; use bevy::prelude::*;
use shared::{ use shared::{
GameState, GameState,
control::{ControlState, ControllerSet, LookDirMovement}, control::{ControllerSet, Inputs, LookDirMovement},
player::PlayerBodyMesh, player::{LocalPlayer, PlayerBodyMesh},
}; };
use std::f32::consts::PI; use std::f32::consts::PI;
@@ -17,15 +17,17 @@ pub fn plugin(app: &mut App) {
} }
fn rotate_rig( fn rotate_rig(
controls: Res<ControlState>, inputs: Single<&Inputs, With<LocalPlayer>>,
look_dir: Res<LookDirMovement>, 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 inputs.view_mode {
if controls.view_mode { return;
continue;
} }
local_player.iter().find(|&child| {
if let Ok(mut rig_transform) = player_mesh.get_mut(child) {
let look_dir = look_dir.0; let look_dir = look_dir.0;
// todo: Make consistent with the running controller // todo: Make consistent with the running controller
@@ -47,5 +49,10 @@ fn rotate_rig(
// * Quat::from_rotation_x(controls.keyboard_state.look_dir.y * sensitivity); // * Quat::from_rotation_x(controls.keyboard_state.look_dir.y * sensitivity);
// let clamped_rotation = rig_transform.rotation.rotate_towards(target_rotation, 0.01); // let clamped_rotation = rig_transform.rotation.rotate_towards(target_rotation, 0.01);
// rig_transform.rotation = clamped_rotation; // rig_transform.rotation = clamped_rotation;
true
} else {
false
} }
});
} }

View File

@@ -1,40 +1,30 @@
use super::{ControlState, Controls};
use crate::{GameState, control::CharacterInputEnabled}; use crate::{GameState, control::CharacterInputEnabled};
use bevy::{ use bevy::{
input::{ input::{
ButtonState,
gamepad::{GamepadConnection, GamepadEvent}, gamepad::{GamepadConnection, GamepadEvent},
mouse::{MouseButtonInput, MouseMotion}, mouse::MouseMotion,
}, },
prelude::*, prelude::*,
}; };
use shared::{ use shared::{
control::{ControllerSet, LookDirMovement}, control::{
player::PlayerBodyMesh, 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) { 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( app.add_systems(
PreUpdate, PreUpdate,
( (
reset_lookdir,
gamepad_controls,
keyboard_controls,
mouse_rotate,
mouse_click,
gamepad_connections.run_if(on_message::<GamepadEvent>), gamepad_connections.run_if(on_message::<GamepadEvent>),
combine_controls, reset_lookdir,
keyboard_controls,
gamepad_controls,
mouse_rotate,
get_lookdir, get_lookdir,
clear_keyboard_just,
clear_gamepad_just,
send_inputs, send_inputs,
) )
.chain() .chain()
@@ -43,7 +33,8 @@ pub fn plugin(app: &mut App) {
in_state(GameState::Playing) in_state(GameState::Playing)
.and(resource_exists_and_equals(CharacterInputEnabled::On)), .and(resource_exists_and_equals(CharacterInputEnabled::On)),
), ),
); )
.add_systems(PreUpdate, overwrite_local_inputs);
app.add_systems( app.add_systems(
Update, 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 /// Write inputs from combined keyboard/gamepad state into the networked input buffer
/// for the local player. /// for the local player.
fn send_inputs(mut writer: MessageWriter<ControlState>, controls: Res<ControlState>) { fn send_inputs(mut writer: MessageWriter<ClientInputs>, local_inputs: Single<&LocalInputs>) {
writer.write(*controls); writer.write(ClientInputs(local_inputs.0));
} }
fn reset_lookdir(mut look_dir: ResMut<LookDirMovement>) { 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. /// Reset character inputs to default when character input is disabled.
fn reset_control_state_on_disable( fn reset_control_state_on_disable(
state: Res<CharacterInputEnabled>, state: Res<CharacterInputEnabled>,
mut controls: ResMut<Controls>, mut inputs: Single<&mut LocalInputs>,
mut control_state: ResMut<ControlState>,
) { ) {
if state.is_changed() && matches!(*state, CharacterInputEnabled::Off) { if state.is_changed() && matches!(*state, CharacterInputEnabled::Off) {
*controls = Controls::default(); inputs.0 = Inputs {
*control_state = ControlState { look_dir: inputs.0.look_dir,
look_dir: control_state.look_dir,
..default() ..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( fn get_lookdir(
mut controls: ResMut<ControlState>, mut inputs: Single<&mut LocalInputs>,
rig_transform: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>, 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() rig_transform.forward().as_vec3()
} else { } else {
Vec3::NEG_Z Vec3::NEG_Z
@@ -201,21 +93,23 @@ fn deadzone_square(v: Vec2, min: f32) -> Vec2 {
} }
/// Collect gamepad inputs /// Collect gamepad inputs
#[allow(clippy::too_many_arguments)]
fn gamepad_controls( fn gamepad_controls(
gamepads: Query<(Entity, &Gamepad, &InputStateCache<GamepadButton>)>, gamepads: Query<&Gamepad>,
mut controls: ResMut<Controls>, mut inputs: Single<&mut LocalInputs>,
mut look_dir: ResMut<LookDirMovement>, 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_left_stick = 0.15;
let deadzone_right_stick = 0.15; let deadzone_right_stick = 0.15;
for gamepad in gamepads.iter() {
let rotate = gamepad let rotate = gamepad
.get(GamepadButton::RightTrigger2) .get(GamepadButton::RightTrigger2)
.unwrap_or_default(); .unwrap_or_default();
@@ -239,29 +133,38 @@ fn gamepad_controls(
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 { inputs.0.move_dir += move_dir.clamp_length_max(1.0);
move_dir, inputs.0.jump |= gamepad.pressed(GamepadButton::South);
look_dir: Vec3::NEG_Z, inputs.0.view_mode |= gamepad.pressed(GamepadButton::LeftTrigger2);
jump: gamepad.pressed(GamepadButton::South), inputs.0.trigger |= gamepad.pressed(GamepadButton::RightTrigger2);
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),
};
if controls if gamepad.just_pressed(GamepadButton::DPadUp) {
.gamepad_state backpack_toggle_pressed.write(BackpackTogglePressed);
.as_ref() }
.map(|last_state| *last_state != state)
.unwrap_or(true) if gamepad.just_pressed(GamepadButton::DPadDown) {
{ backpack_swap_pressed.write(BackpackSwapPressed);
controls.gamepad_state = Some(state); }
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 /// Collect keyboard input
#[allow(clippy::too_many_arguments)]
fn keyboard_controls( fn keyboard_controls(
keyboard: Res<ButtonInput<KeyCode>>, keyboard: Res<ButtonInput<KeyCode>>,
cache: Res<InputStateCache<KeyCode>>, mouse: Res<ButtonInput<MouseButton>>,
mut controls: ResMut<Controls>, 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 up_binds = [KeyCode::KeyW, KeyCode::ArrowUp];
let down_binds = [KeyCode::KeyS, KeyCode::ArrowDown]; let down_binds = [KeyCode::KeyS, KeyCode::ArrowDown];
@@ -292,41 +203,37 @@ fn keyboard_controls(
let vertical = up as i8 - down 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); let direction = Vec2::new(horizontal as f32, vertical as f32).clamp_length_max(1.0);
controls.keyboard_state.move_dir = direction; inputs.0.move_dir = direction;
controls.keyboard_state.jump = keyboard.pressed(KeyCode::Space); inputs.0.jump = keyboard.pressed(KeyCode::Space);
controls.keyboard_state.view_mode = keyboard.pressed(KeyCode::Tab); inputs.0.view_mode = keyboard.pressed(KeyCode::Tab);
controls.keyboard_state.backpack_toggle = cache.just_pressed(KeyCode::KeyB); inputs.0.trigger = mouse.pressed(MouseButton::Left);
controls.keyboard_state.backpack_swap = cache.just_pressed(KeyCode::Enter);
controls.keyboard_state.backpack_left = cache.just_pressed(KeyCode::Comma); if keyboard.just_pressed(KeyCode::KeyB) {
controls.keyboard_state.backpack_right = cache.just_pressed(KeyCode::Period); backpack_toggle_pressed.write(BackpackTogglePressed);
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);
} }
/// Collect mouse button input when pressed if keyboard.just_pressed(KeyCode::Enter) {
fn mouse_click(mut events: MessageReader<MouseButtonInput>, mut controls: ResMut<Controls>) { backpack_swap_pressed.write(BackpackSwapPressed);
controls.keyboard_state.just_triggered = false; }
for ev in events.read() { if keyboard.just_pressed(KeyCode::Comma) {
match ev { backpack_left_pressed.write(BackpackLeftPressed);
MouseButtonInput {
button: MouseButton::Left,
state: ButtonState::Pressed,
..
} => {
controls.keyboard_state.trigger = true;
controls.keyboard_state.just_triggered = true;
} }
MouseButtonInput {
button: MouseButton::Left, if keyboard.just_pressed(KeyCode::Period) {
state: ButtonState::Released, backpack_right_pressed.write(BackpackRightPressed);
..
} => {
controls.keyboard_state.trigger = false;
} }
_ => {}
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 crate::GameState;
use bevy::prelude::*; use bevy::prelude::*;
use bevy_replicon::client::ClientSystems; use bevy_replicon::client::ClientSystems;
use shared::control::{ControlState, ControllerSet}; use shared::control::ControllerSet;
mod controller_flying; mod controller_flying;
pub mod controls; pub mod controls;
#[derive(Resource, Debug, Default)]
struct Controls {
keyboard_state: ControlState,
gamepad_state: Option<ControlState>,
}
#[derive(Resource, Debug, PartialEq, Eq)] #[derive(Resource, Debug, PartialEq, Eq)]
pub enum CharacterInputEnabled { pub enum CharacterInputEnabled {
On, On,

View File

@@ -5,23 +5,16 @@ use crate::{
}; };
use bevy::prelude::*; use bevy::prelude::*;
use shared::{ use shared::{
player::{LocalPlayerId, PlayerBodyMesh}, player::{LocalPlayer, PlayerBodyMesh},
protocol::{PlaySound, PlayerId, events::ClientHeadChanged, messages::AssignClientPlayer}, protocol::{PlaySound, PlayerId, events::ClientHeadChanged, messages::AssignClientPlayer},
}; };
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.register_type::<LocalPlayerId>();
app.register_type::<LocalPlayer>();
app.init_state::<PlayerAssignmentState>(); app.init_state::<PlayerAssignmentState>();
app.add_systems( app.add_systems(
Update, Update,
receive_player_id.run_if(in_state(PlayerAssignmentState::Waiting)), receive_player_id.run_if(not(in_state(PlayerAssignmentState::Confirmed))),
);
app.add_systems(
Update,
match_player_id.run_if(in_state(PlayerAssignmentState::IdReceived)),
); );
global_observer!(app, on_update_head_mesh); global_observer!(app, on_update_head_mesh);
@@ -31,22 +24,18 @@ fn receive_player_id(
mut commands: Commands, mut commands: Commands,
mut client_assignments: MessageReader<AssignClientPlayer>, mut client_assignments: MessageReader<AssignClientPlayer>,
mut next: ResMut<NextState<PlayerAssignmentState>>, mut next: ResMut<NextState<PlayerAssignmentState>>,
mut local_id: Local<Option<PlayerId>>,
players: Query<(Entity, &PlayerId), Changed<PlayerId>>,
) { ) {
for &AssignClientPlayer(id) in client_assignments.read() { for &AssignClientPlayer(id) in client_assignments.read() {
commands.insert_resource(LocalPlayerId { id });
next.set(PlayerAssignmentState::IdReceived);
info!("player id `{}` received", id.id); info!("player id `{}` received", id.id);
}
*local_id = Some(id);
} }
fn match_player_id( if let Some(local_id) = *local_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() { for (entity, player_id) in players.iter() {
if *player_id == client.id { if *player_id == local_id {
commands.entity(entity).insert(LocalPlayer); commands.entity(entity).insert(LocalPlayer);
next.set(PlayerAssignmentState::Confirmed); next.set(PlayerAssignmentState::Confirmed);
info!( info!(
@@ -57,6 +46,7 @@ fn match_player_id(
} }
} }
} }
}
/// Various states while trying to assign and match an ID to the player character. /// Various states while trying to assign and match an ID to the player character.
/// Every client is given an ID (its player index in the match) and every character controller /// Every client is given an ID (its player index in the match) and every character controller
@@ -64,19 +54,13 @@ fn match_player_id(
/// controller it owns. /// controller it owns.
#[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, States)] #[derive(Clone, Copy, Debug, Default, Hash, PartialEq, Eq, PartialOrd, States)]
pub enum PlayerAssignmentState { 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] #[default]
Waiting, Waiting,
/// Received an [`AssignClientPlayer`], querying for a matching controller
IdReceived,
/// Matching controller confirmed; a [`LocalPlayer`] exists /// Matching controller confirmed; a [`LocalPlayer`] exists
Confirmed, Confirmed,
} }
#[derive(Component, Debug, Reflect)]
#[reflect(Component)]
pub struct LocalPlayer;
fn on_update_head_mesh( fn on_update_head_mesh(
trigger: On<ClientHeadChanged>, trigger: On<ClientHeadChanged>,
mut commands: Commands, mut commands: Commands,

View File

@@ -6,7 +6,9 @@ use shared::{
BackbackSwapEvent, Backpack, UiHeadState, BackbackSwapEvent, Backpack, UiHeadState,
backpack_ui::{BackpackUiState, HEAD_SLOTS}, backpack_ui::{BackpackUiState, HEAD_SLOTS},
}, },
control::ControlState, control::{
BackpackLeftPressed, BackpackRightPressed, BackpackSwapPressed, BackpackTogglePressed,
},
protocol::{ClientToController, PlaySound}, protocol::{ClientToController, PlaySound},
}; };
@@ -21,23 +23,23 @@ pub fn plugin(app: &mut App) {
); );
} }
#[allow(clippy::too_many_arguments)]
fn swap_head_inputs( fn swap_head_inputs(
backpacks: Query<Ref<Backpack>>, backpacks: Query<Ref<Backpack>>,
clients: ClientToController, clients: ClientToController,
mut inputs: MessageReader<FromClient<ControlState>>, mut backpack_toggles: MessageReader<FromClient<BackpackTogglePressed>>,
mut backpack_lefts: MessageReader<FromClient<BackpackLeftPressed>>,
mut backpack_rights: MessageReader<FromClient<BackpackRightPressed>>,
mut backpack_swaps: MessageReader<FromClient<BackpackSwapPressed>>,
mut commands: Commands, mut commands: Commands,
mut state: Single<&mut BackpackUiState>, mut state: Single<&mut BackpackUiState>,
time: Res<Time>, time: Res<Time>,
) { ) {
for controls in inputs.read() { for _ in backpack_toggles.read() {
let player = clients.get_controller(controls.client_id);
let backpack = backpacks.get(player).unwrap();
if state.count == 0 { if state.count == 0 {
return; return;
} }
if controls.backpack_toggle {
state.open = !state.open; state.open = !state.open;
commands.server_trigger(ToClients { commands.server_trigger(ToClients {
mode: SendMode::Broadcast, mode: SendMode::Broadcast,
@@ -45,24 +47,17 @@ fn swap_head_inputs(
}); });
} }
for press in backpack_lefts.read() {
let player = clients.get_controller(press.client_id);
let backpack = backpacks.get(player).unwrap();
if !state.open { if !state.open {
return; return;
} }
let mut changed = false; if state.current_slot > 0 {
if controls.backpack_left && state.current_slot > 0 {
state.current_slot -= 1; state.current_slot -= 1;
changed = true;
}
if controls.backpack_right && state.current_slot < state.count.saturating_sub(1) {
state.current_slot += 1;
changed = true;
}
if controls.backpack_swap {
commands.trigger(BackbackSwapEvent(state.current_slot));
}
if changed {
commands.server_trigger(ToClients { commands.server_trigger(ToClients {
mode: SendMode::Broadcast, mode: SendMode::Broadcast,
message: PlaySound::Selection, message: PlaySound::Selection,
@@ -70,6 +65,33 @@ fn swap_head_inputs(
sync(&backpack, &mut state, time.elapsed_secs()); sync(&backpack, &mut state, time.elapsed_secs());
} }
} }
for press in backpack_rights.read() {
let player = clients.get_controller(press.client_id);
let backpack = backpacks.get(player).unwrap();
if !state.open {
return;
}
if state.current_slot < state.count.saturating_sub(1) {
state.current_slot += 1;
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: PlaySound::Selection,
});
sync(&backpack, &mut state, time.elapsed_secs());
}
}
for _ in backpack_swaps.read() {
if !state.open {
return;
}
commands.trigger(BackbackSwapEvent(state.current_slot));
}
} }
fn sync_on_change( fn sync_on_change(

View File

@@ -3,7 +3,7 @@ use bevy_replicon::prelude::{Replicated, SendMode, ServerTriggerExt, ToClients};
use shared::{ use shared::{
backpack::{Backpack, backpack_ui::BackpackUiState}, backpack::{Backpack, backpack_ui::BackpackUiState},
camera::{CameraArmRotation, CameraTarget}, camera::{CameraArmRotation, CameraTarget},
cash::CashResource, cash::CashInventory,
character::AnimatedCharacter, character::AnimatedCharacter,
control::{Inputs, controller_common::PlayerCharacterController}, control::{Inputs, controller_common::PlayerCharacterController},
global_observer, global_observer,
@@ -46,7 +46,7 @@ pub fn spawn(
Some(HeadState::new(9, heads_db.as_ref())), Some(HeadState::new(9, heads_db.as_ref())),
]), ]),
Hitpoints::new(100), Hitpoints::new(100),
CashResource::default(), CashInventory::default(),
CameraTarget, CameraTarget,
transform, transform,
Visibility::default(), Visibility::default(),

View File

@@ -14,18 +14,10 @@ use crate::{
}; };
#[cfg(feature = "server")] #[cfg(feature = "server")]
use crate::{ use crate::{
aim::AimTarget, aim::AimTarget, character::CharacterHierarchy, control::Inputs, head::ActiveHead,
character::CharacterHierarchy, heads::ActiveHeads, heads_database::HeadsDatabase, player::Player,
control::{ControlState, Inputs},
head::ActiveHead,
heads::ActiveHeads,
heads_database::HeadsDatabase,
player::Player,
protocol::ClientToController,
}; };
use bevy::{light::NotShadowCaster, prelude::*}; use bevy::{light::NotShadowCaster, prelude::*};
#[cfg(feature = "server")]
use bevy_replicon::prelude::FromClient;
use bevy_replicon::prelude::{ClientState, SendMode, ServerTriggerExt, ToClients}; use bevy_replicon::prelude::{ClientState, SendMode, ServerTriggerExt, ToClients};
use bevy_sprite3d::Sprite3d; use bevy_sprite3d::Sprite3d;
pub use healing::Healing; pub use healing::Healing;
@@ -194,20 +186,10 @@ fn explode_projectiles(mut commands: Commands, query: Query<(Entity, &ExplodingP
#[cfg(feature = "server")] #[cfg(feature = "server")]
fn on_trigger_state( fn on_trigger_state(
mut res: ResMut<TriggerStateRes>, mut res: ResMut<TriggerStateRes>,
players: Query<&ActiveHead, With<Player>>, players: Query<(&ActiveHead, &Inputs), With<Player>>,
clients: ClientToController,
mut controls: MessageReader<FromClient<ControlState>>,
headdb: Res<HeadsDatabase>,
time: Res<Time>,
) { ) {
for controls in controls.read() { for (_, inputs) in players.iter() {
let player = clients.get_controller(controls.client_id); res.active = inputs.trigger;
let player_head = players.get(player).unwrap();
res.active = controls.trigger;
if controls.just_triggered {
let head_stats = headdb.head_stats(player_head.0);
res.next_trigger_timestamp = time.elapsed_secs() + head_stats.shoot_offset;
}
} }
} }

View File

@@ -1,11 +1,12 @@
use crate::GameState; use crate::GameState;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::control::Inputs;
#[cfg(feature = "client")]
use crate::physics_layers::GameLayer; use crate::physics_layers::GameLayer;
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::{ use crate::player::LocalPlayer;
control::{ControlState, LookDirMovement}, #[cfg(feature = "client")]
loading_assets::UIAssets, use crate::{control::LookDirMovement, loading_assets::UIAssets};
};
#[cfg(feature = "client")] #[cfg(feature = "client")]
use avian3d::prelude::SpatialQuery; use avian3d::prelude::SpatialQuery;
#[cfg(feature = "client")] #[cfg(feature = "client")]
@@ -80,8 +81,11 @@ fn startup(mut commands: Commands) {
} }
#[cfg(feature = "client")] #[cfg(feature = "client")]
fn update_look_around(controls: Res<ControlState>, mut cam_state: ResMut<CameraState>) { fn update_look_around(
let look_around = controls.view_mode; inputs: Single<&Inputs, With<LocalPlayer>>,
mut cam_state: ResMut<CameraState>,
) {
let look_around = inputs.view_mode;
if look_around != cam_state.look_around { if look_around != cam_state.look_around {
cam_state.look_around = look_around; cam_state.look_around = look_around;
@@ -176,11 +180,11 @@ fn update(
#[cfg(feature = "client")] #[cfg(feature = "client")]
fn rotate_view( fn rotate_view(
controls: Res<ControlState>, inputs: Single<&Inputs, With<LocalPlayer>>,
look_dir: Res<LookDirMovement>, look_dir: Res<LookDirMovement>,
mut cam: Single<&mut CameraRotationInput>, mut cam: Single<&mut CameraRotationInput>,
) { ) {
if !controls.view_mode { if !inputs.view_mode {
cam.x = 0.0; cam.x = 0.0;
return; return;
} }

View File

@@ -15,7 +15,7 @@ pub struct Cash;
struct CashText; struct CashText;
#[derive(Component, Reflect, Default, Serialize, Deserialize, PartialEq)] #[derive(Component, Reflect, Default, Serialize, Deserialize, PartialEq)]
pub struct CashResource { pub struct CashInventory {
pub cash: i32, pub cash: i32,
} }
@@ -37,7 +37,7 @@ pub fn plugin(app: &mut App) {
fn on_cash_collect( fn on_cash_collect(
_trigger: On<CashCollectEvent>, _trigger: On<CashCollectEvent>,
mut commands: Commands, mut commands: Commands,
mut cash: Single<&mut CashResource>, mut cash: Single<&mut CashInventory>,
) { ) {
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients}; use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients};
@@ -58,7 +58,7 @@ fn rotate(time: Res<Time>, mut query: Query<&mut Rotation, With<Cash>>) {
} }
fn update_ui( fn update_ui(
cash: Single<&CashResource, Changed<CashResource>>, cash: Single<&CashInventory, Changed<CashInventory>>,
text: Query<Entity, With<CashText>>, text: Query<Entity, With<CashText>>,
mut writer: TextUiWriter, mut writer: TextUiWriter,
) { ) {

View File

@@ -1,6 +1,9 @@
use crate::{ use crate::{
cash::CashResource, control::ControlState, hitpoints::Hitpoints, player::Player, cash::CashInventory,
protocol::PlaySound, control::CashHealPressed,
hitpoints::Hitpoints,
player::Player,
protocol::{ClientToController, PlaySound},
}; };
use bevy::prelude::*; use bevy::prelude::*;
use bevy_replicon::prelude::{FromClient, SendMode, ServerTriggerExt, ToClients}; use bevy_replicon::prelude::{FromClient, SendMode, ServerTriggerExt, ToClients};
@@ -17,15 +20,13 @@ struct HealAction {
fn on_heal_trigger( fn on_heal_trigger(
mut commands: Commands, mut commands: Commands,
mut cash: Single<&mut CashResource>, controllers: ClientToController,
mut query: Query<&mut Hitpoints, With<Player>>, mut query: Query<(&mut Hitpoints, &mut CashInventory), With<Player>>,
mut controls: MessageReader<FromClient<ControlState>>, mut inputs: MessageReader<FromClient<CashHealPressed>>,
) { ) {
for controls in controls.read() { for press in inputs.read() {
for mut hp in query.iter_mut() { let controller = controllers.get_controller(press.client_id);
if !controls.cash_heal { let (mut hp, mut cash) = query.get_mut(controller).unwrap();
continue;
}
if hp.max() || cash.cash == 0 { if hp.max() || cash.cash == 0 {
return; return;
@@ -44,7 +45,6 @@ fn on_heal_trigger(
}); });
} }
} }
}
fn heal(cash: i32, damage: u32) -> HealAction { fn heal(cash: i32, damage: u32) -> HealAction {
let cost = (damage as f32 / 10. * 25.) as i32; let cost = (damage as f32 / 10. * 25.) as i32;

View File

@@ -233,7 +233,7 @@ fn setup_once_loaded(
#[cfg(feature = "dbg")] #[cfg(feature = "dbg")]
fn debug_show_projectile_origin_and_trial( fn debug_show_projectile_origin_and_trial(
mut gizmos: Gizmos, mut gizmos: Gizmos,
query: Query<&GlobalTransform, Or<(With<ProjectileOrigin>, With<Trail>)>>, query: Query<&GlobalTransform, Or<(With<ProjectileOrigin>, With<crate::utils::trail::Trail>)>>,
) { ) {
for projectile_origin in query.iter() { for projectile_origin in query.iter() {
gizmos.sphere( gizmos.sphere(

View File

@@ -5,8 +5,8 @@ use crate::{
}; };
#[cfg(feature = "client")] #[cfg(feature = "client")]
use crate::{ use crate::{
control::{ControlState, LookDirMovement}, control::LookDirMovement,
player::PlayerBodyMesh, player::{LocalPlayer, PlayerBodyMesh},
}; };
use bevy::prelude::*; use bevy::prelude::*;
use bevy_replicon::prelude::ClientState; use bevy_replicon::prelude::ClientState;
@@ -36,17 +36,24 @@ impl Plugin for CharacterControllerPlugin {
#[cfg(feature = "client")] #[cfg(feature = "client")]
fn rotate_view( fn rotate_view(
controls: Res<ControlState>, controller: Single<(&Inputs, &Children), With<LocalPlayer>>,
mut player_mesh: Query<&mut Transform, With<PlayerBodyMesh>>,
look_dir: Res<LookDirMovement>, look_dir: Res<LookDirMovement>,
mut player: Query<&mut Transform, With<PlayerBodyMesh>>,
) { ) {
for mut tr in player.iter_mut() { let (inputs, children) = controller.into_inner();
if controls.view_mode {
continue; if inputs.view_mode {
return;
} }
tr.rotate_y(look_dir.0.x * -0.001); children.iter().find(|&child| {
if let Ok(mut body_transform) = player_mesh.get_mut(child) {
body_transform.rotate_y(look_dir.0.x * -0.001);
true
} else {
false
} }
});
} }
fn apply_controls( fn apply_controls(

View File

@@ -2,8 +2,8 @@ use crate::{
GameState, GameState,
head::ActiveHead, head::ActiveHead,
heads_database::{HeadControls, HeadsDatabase}, heads_database::{HeadControls, HeadsDatabase},
player::{LocalPlayerId, Player}, player::Player,
protocol::{ClientToController, PlayerIdMap}, protocol::ClientToController,
}; };
use bevy::{ecs::entity::MapEntities, prelude::*}; use bevy::{ecs::entity::MapEntities, prelude::*};
use bevy_replicon::{ use bevy_replicon::{
@@ -28,11 +28,13 @@ pub enum ControllerSet {
pub struct SelectedController(pub ControllerSet); pub struct SelectedController(pub ControllerSet);
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.register_type::<ControllerSettings>(); app.register_type::<ControllerSettings>()
app.register_type::<LookDirMovement>(); .register_type::<LookDirMovement>()
app.register_type::<Inputs>(); .register_type::<Inputs>();
#[cfg(feature = "client")]
app.register_type::<LocalInputs>();
app.init_resource::<ControlState>();
app.init_resource::<LookDirMovement>(); app.init_resource::<LookDirMovement>();
app.init_resource::<SelectedController>(); app.init_resource::<SelectedController>();
@@ -58,22 +60,17 @@ pub fn plugin(app: &mut App) {
app.add_systems( app.add_systems(
PreUpdate, PreUpdate,
( collect_player_inputs
collect_player_inputs.run_if(in_state(ClientState::Disconnected)), .run_if(in_state(ClientState::Disconnected).and(in_state(GameState::Playing)))
update_local_player_inputs
.run_if(resource_exists::<LocalPlayerId>)
.run_if(in_state(ClientState::Connected)),
)
.chain()
.run_if(in_state(GameState::Playing))
.after(ClientSystems::Receive), .after(ClientSystems::Receive),
); );
app.add_systems(Update, head_change.run_if(in_state(GameState::Playing))); app.add_systems(Update, head_change.run_if(in_state(GameState::Playing)));
} }
// TODO: Split this into an enum of individual input commands, e.g. `JustJumped` /// The continuous inputs of a client for a tick. The instant inputs are sent via messages like `BackpackTogglePressed`.
#[derive(Resource, Debug, Clone, Copy, Message, PartialEq, Serialize, Deserialize, Reflect)] #[derive(Component, Clone, Copy, Debug, Serialize, Deserialize, Reflect)]
pub struct ControlState { #[reflect(Component, Default)]
pub struct Inputs {
/// Movement direction with a maximum length of 1.0 /// Movement direction with a maximum length of 1.0
pub move_dir: Vec2, pub move_dir: Vec2,
/// The current direction that the character is facing /// The current direction that the character is facing
@@ -83,17 +80,9 @@ pub struct ControlState {
/// Determines if the camera can rotate freely around the player /// Determines if the camera can rotate freely around the player
pub view_mode: bool, pub view_mode: bool,
pub trigger: bool, pub trigger: bool,
pub just_triggered: bool,
pub select_left: bool,
pub select_right: bool,
pub backpack_toggle: bool,
pub backpack_swap: bool,
pub backpack_left: bool,
pub backpack_right: bool,
pub cash_heal: bool,
} }
impl Default for ControlState { impl Default for Inputs {
fn default() -> Self { fn default() -> Self {
Self { Self {
move_dir: Default::default(), move_dir: Default::default(),
@@ -101,25 +90,44 @@ impl Default for ControlState {
jump: Default::default(), jump: Default::default(),
view_mode: Default::default(), view_mode: Default::default(),
trigger: Default::default(), trigger: Default::default(),
just_triggered: Default::default(),
select_left: Default::default(),
select_right: Default::default(),
backpack_toggle: Default::default(),
backpack_swap: Default::default(),
backpack_left: Default::default(),
backpack_right: Default::default(),
cash_heal: Default::default(),
} }
} }
} }
impl MapEntities for ControlState { impl MapEntities for Inputs {
fn map_entities<E: EntityMapper>(&mut self, _entity_mapper: &mut E) {} fn map_entities<E: EntityMapper>(&mut self, _entity_mapper: &mut E) {}
} }
#[derive(Component, Default, Deref, DerefMut, Reflect, Serialize, Deserialize)] /// A message to tell the server what inputs the client pressed this tick
#[reflect(Component, Default)] #[derive(Debug, Clone, Copy, Message, Serialize, Deserialize, Reflect)]
pub struct Inputs(pub ControlState); pub struct ClientInputs(pub Inputs);
/// A cache to collect inputs into clientside, so that they don't get overwritten by replication from the server
#[cfg(feature = "client")]
#[derive(Component, Default, Reflect)]
#[reflect(Component)]
pub struct LocalInputs(pub Inputs);
#[derive(Message, Serialize, Deserialize)]
pub struct SelectLeftPressed;
#[derive(Message, Serialize, Deserialize)]
pub struct SelectRightPressed;
#[derive(Message, Serialize, Deserialize)]
pub struct BackpackTogglePressed;
#[derive(Message, Serialize, Deserialize)]
pub struct BackpackSwapPressed;
#[derive(Message, Serialize, Deserialize)]
pub struct BackpackLeftPressed;
#[derive(Message, Serialize, Deserialize)]
pub struct BackpackRightPressed;
#[derive(Message, Serialize, Deserialize)]
pub struct CashHealPressed;
#[derive(Resource, Default, Reflect)] #[derive(Resource, Default, Reflect)]
#[reflect(Resource)] #[reflect(Resource)]
@@ -147,29 +155,16 @@ pub struct ControllerSwitchEvent {
fn collect_player_inputs( fn collect_player_inputs(
mut players: Query<&mut Inputs>, mut players: Query<&mut Inputs>,
clients: ClientToController, clients: ClientToController,
mut input_messages: MessageReader<FromClient<ControlState>>, mut input_messages: MessageReader<FromClient<ClientInputs>>,
) { ) {
for msg in input_messages.read() { for msg in input_messages.read() {
let player = clients.get_controller(msg.client_id); let player = clients.get_controller(msg.client_id);
let mut inputs = players.get_mut(player).unwrap(); let mut inputs = players.get_mut(player).unwrap();
inputs.0 = msg.message; *inputs = msg.message.0;
} }
} }
/// Overwrite the input cache replicated to the local player with the actual current inputs
fn update_local_player_inputs(
mut players: Query<&mut Inputs>,
player_ids: Res<PlayerIdMap>,
local_id: Res<LocalPlayerId>,
control_state: Res<ControlState>,
) {
let player = player_ids.get(&local_id.id).unwrap();
let mut inputs = players.get_mut(*player).unwrap();
inputs.0 = *control_state;
}
fn head_change( fn head_change(
//TODO: needs a 'LocalPlayer' at some point for multiplayer //TODO: needs a 'LocalPlayer' at some point for multiplayer
query: Query<(Entity, &ActiveHead), (Changed<ActiveHead>, With<Player>)>, query: Query<(Entity, &ActiveHead), (Changed<ActiveHead>, With<Player>)>,

View File

@@ -1,5 +1,5 @@
#[cfg(feature = "server")] #[cfg(feature = "server")]
use crate::animation::AnimationFlags; use crate::protocol::PlaySound;
use crate::{ use crate::{
GameState, GameState,
backpack::{BackbackSwapEvent, Backpack}, backpack::{BackbackSwapEvent, Backpack},
@@ -9,7 +9,11 @@ use crate::{
player::Player, player::Player,
}; };
#[cfg(feature = "server")] #[cfg(feature = "server")]
use crate::{control::ControlState, protocol::PlaySound}; use crate::{
animation::AnimationFlags,
control::{SelectLeftPressed, SelectRightPressed},
protocol::ClientToController,
};
use bevy::prelude::*; use bevy::prelude::*;
#[cfg(feature = "server")] #[cfg(feature = "server")]
use bevy_replicon::prelude::FromClient; use bevy_replicon::prelude::FromClient;
@@ -249,24 +253,17 @@ fn reload(
fn on_select_active_head( fn on_select_active_head(
mut commands: Commands, mut commands: Commands,
mut query: Query<(&mut ActiveHeads, &mut Hitpoints), With<Player>>, mut query: Query<(&mut ActiveHeads, &mut Hitpoints), With<Player>>,
mut controls: MessageReader<FromClient<ControlState>>, mut select_lefts: MessageReader<FromClient<SelectLeftPressed>>,
mut select_rights: MessageReader<FromClient<SelectRightPressed>>,
controllers: ClientToController,
) { ) {
for controls in controls.read() { for press in select_lefts.read() {
for (mut active_heads, mut hp) in query.iter_mut() {
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients}; use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients};
if !controls.select_right && !controls.select_left { let player = controllers.get_controller(press.client_id);
continue; let (mut active_heads, mut hp) = query.get_mut(player).unwrap();
}
if controls.select_right { active_heads.selected_slot = (active_heads.selected_slot + (HEAD_SLOTS - 1)) % HEAD_SLOTS;
active_heads.selected_slot = (active_heads.selected_slot + 1) % HEAD_SLOTS;
}
if controls.select_left {
active_heads.selected_slot =
(active_heads.selected_slot + (HEAD_SLOTS - 1)) % HEAD_SLOTS;
}
commands.server_trigger(ToClients { commands.server_trigger(ToClients {
mode: SendMode::Broadcast, mode: SendMode::Broadcast,
@@ -282,6 +279,28 @@ fn on_select_active_head(
)); ));
} }
} }
for press in select_rights.read() {
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients};
let player = controllers.get_controller(press.client_id);
let (mut active_heads, mut hp) = query.get_mut(player).unwrap();
active_heads.selected_slot = (active_heads.selected_slot + 1) % HEAD_SLOTS;
commands.server_trigger(ToClients {
mode: SendMode::Broadcast,
message: PlaySound::Selection,
});
if active_heads.head(active_heads.selected_slot).is_some() {
active_heads.current_slot = active_heads.selected_slot;
hp.set_health(active_heads.current().unwrap().health);
commands.trigger(HeadChanged(
active_heads.heads[active_heads.current_slot].unwrap().head,
));
}
} }
} }

View File

@@ -1,3 +1,5 @@
#[cfg(feature = "client")]
use crate::control::LocalInputs;
use crate::{ use crate::{
GameState, GameState,
cash::{Cash, CashCollectEvent}, cash::{Cash, CashCollectEvent},
@@ -17,6 +19,12 @@ use serde::{Deserialize, Serialize};
#[require(HedzCharacter, DebugInput = DebugInput)] #[require(HedzCharacter, DebugInput = DebugInput)]
pub struct Player; pub struct Player;
#[cfg(feature = "client")]
#[derive(Component, Debug, Reflect)]
#[reflect(Component)]
#[require(LocalInputs)]
pub struct LocalPlayer;
#[derive(Component, Default, Serialize, Deserialize, PartialEq)] #[derive(Component, Default, Serialize, Deserialize, PartialEq)]
#[require(Transform, Visibility)] #[require(Transform, Visibility)]
pub struct PlayerBodyMesh; pub struct PlayerBodyMesh;
@@ -25,13 +33,6 @@ pub struct PlayerBodyMesh;
#[derive(Component, Clone, Copy)] #[derive(Component, Clone, Copy)]
pub struct ClientPlayerId(pub PlayerId); pub struct ClientPlayerId(pub PlayerId);
/// Client-side only; stores this client's id
#[derive(Resource, Reflect)]
#[reflect(Resource)]
pub struct LocalPlayerId {
pub id: PlayerId,
}
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_systems( app.add_systems(
OnEnter(GameState::Playing), OnEnter(GameState::Playing),

View File

@@ -1,6 +1,8 @@
#[cfg(feature = "client")]
use crate::player::LocalPlayer;
use crate::{ use crate::{
loading_assets::{GameAssets, HeadDropAssets}, loading_assets::{GameAssets, HeadDropAssets},
player::{ClientPlayerId, LocalPlayerId}, player::ClientPlayerId,
protocol::TbMapEntityMapping, protocol::TbMapEntityMapping,
}; };
use avian3d::prelude::Collider; use avian3d::prelude::Collider;
@@ -101,7 +103,8 @@ pub struct PlayerIdMap {
pub struct ClientToController<'w, 's> { pub struct ClientToController<'w, 's> {
clients: Query<'w, 's, &'static ClientPlayerId>, clients: Query<'w, 's, &'static ClientPlayerId>,
players: Res<'w, PlayerIdMap>, players: Res<'w, PlayerIdMap>,
local_id: Option<Res<'w, LocalPlayerId>>, #[cfg(feature = "client")]
local_id: Option<Single<'w, 's, &'static PlayerId, With<LocalPlayer>>>,
} }
impl ClientToController<'_, '_> { impl ClientToController<'_, '_> {
@@ -109,7 +112,16 @@ impl ClientToController<'_, '_> {
pub fn get_controller(&self, client: ClientId) -> Entity { pub fn get_controller(&self, client: ClientId) -> Entity {
let player_id = match client.entity() { let player_id = match client.entity() {
Some(client) => self.clients.get(client).unwrap().0, Some(client) => self.clients.get(client).unwrap().0,
None => self.local_id.as_ref().unwrap().id, None => {
#[cfg(not(feature = "client"))]
{
error!("attempted to look up the local controller on a dedicated server");
PlayerId { id: 0 }
}
#[cfg(feature = "client")]
***self.local_id.as_ref().unwrap()
}
}; };
*self.players.get(&player_id).unwrap() *self.players.get(&player_id).unwrap()
} }

View File

@@ -4,10 +4,12 @@ use crate::{
animation::AnimationFlags, animation::AnimationFlags,
backpack::{Backpack, backpack_ui::BackpackUiState}, backpack::{Backpack, backpack_ui::BackpackUiState},
camera::{CameraArmRotation, CameraTarget}, camera::{CameraArmRotation, CameraTarget},
cash::CashResource, cash::CashInventory,
character::{AnimatedCharacter, HedzCharacter}, character::{AnimatedCharacter, HedzCharacter},
control::{ control::{
ControlState, ControllerSettings, Inputs, BackpackLeftPressed, BackpackRightPressed, BackpackSwapPressed, BackpackTogglePressed,
CashHealPressed, ClientInputs, ControllerSettings, Inputs, SelectLeftPressed,
SelectRightPressed,
controller_common::{MovementSpeedFactor, PlayerCharacterController}, controller_common::{MovementSpeedFactor, PlayerCharacterController},
}, },
cutscene::StartCutscene, cutscene::StartCutscene,
@@ -49,7 +51,14 @@ pub mod events;
pub mod messages; pub mod messages;
pub fn plugin(app: &mut App) { pub fn plugin(app: &mut App) {
app.add_client_message::<ControlState>(Channel::Unreliable); app.add_client_message::<ClientInputs>(Channel::Unreliable)
.add_client_message::<SelectLeftPressed>(Channel::Ordered)
.add_client_message::<SelectRightPressed>(Channel::Ordered)
.add_client_message::<BackpackTogglePressed>(Channel::Ordered)
.add_client_message::<BackpackSwapPressed>(Channel::Ordered)
.add_client_message::<BackpackLeftPressed>(Channel::Ordered)
.add_client_message::<BackpackRightPressed>(Channel::Ordered)
.add_client_message::<CashHealPressed>(Channel::Ordered);
app.add_client_event::<ClientEnteredPlaying>(Channel::Ordered); app.add_client_event::<ClientEnteredPlaying>(Channel::Ordered);
@@ -89,7 +98,7 @@ pub fn plugin(app: &mut App) {
.replicate::<Billboard>() .replicate::<Billboard>()
.replicate_once::<CameraArmRotation>() .replicate_once::<CameraArmRotation>()
.replicate_once::<CameraTarget>() .replicate_once::<CameraTarget>()
.replicate::<CashResource>() .replicate::<CashInventory>()
.replicate_once::<HedzCharacter>() .replicate_once::<HedzCharacter>()
.replicate_once::<Healing>() .replicate_once::<Healing>()
.replicate::<Hitpoints>() .replicate::<Hitpoints>()

View File

@@ -91,7 +91,11 @@ fn attach_trail(
)) ))
.id(); .id();
commands.entity(entity).add_child(id); commands
.entity(entity)
.queue_silenced(move |mut world: EntityWorldMut| {
world.add_child(id);
});
} }
} }