Input replication (#62)

This commit is contained in:
PROMETHIA-27
2025-08-25 03:41:28 -04:00
committed by GitHub
parent c650924d68
commit 7f6c00b5d6
20 changed files with 345 additions and 280 deletions

View File

@@ -27,6 +27,7 @@ happy_feet = { git = "https://github.com/atornity/happy_feet.git", rev = "1b24ed
"serde",
] }
lightyear = { version = "0.22.4", default-features = false, features = [
"input_native",
"interpolation",
"netcode",
"prediction",

View File

@@ -2,9 +2,12 @@ use bevy::prelude::*;
use lightyear::{
connection::client::ClientState,
netcode::Key,
prelude::{client::NetcodeConfig, *},
prelude::{client::NetcodeConfig, input::native::InputMarker, *},
};
use shared::{
GameState, control::ControlState, global_observer, heads_database::HeadsDatabase,
player::Player, tb_entities::SpawnPoint,
};
use shared::{GameState, heads_database::HeadsDatabase, tb_entities::SpawnPoint};
use std::net::{IpAddr, Ipv4Addr, SocketAddr};
pub fn plugin(app: &mut App) {
@@ -13,6 +16,8 @@ pub fn plugin(app: &mut App) {
FixedUpdate,
spawn_disconnected_player.run_if(in_state(GameState::Playing)),
);
global_observer!(app, temp_give_player_marker);
}
fn temp_connect_on_startup(mut commands: Commands) -> Result {
@@ -68,3 +73,9 @@ fn spawn_disconnected_player(
shared::player::spawn(commands, Entity::PLACEHOLDER, query, asset_server, heads_db)
}
}
fn temp_give_player_marker(trigger: Trigger<OnAdd, Player>, mut commands: Commands) {
commands
.entity(trigger.target())
.insert(InputMarker::<ControlState>::default());
}

View File

@@ -47,7 +47,7 @@ fn main() {
..default()
})
.set(bevy::log::LogPlugin {
filter: "info,lightyear_replication=warn".into(),
filter: "info,lightyear_replication=off".into(),
level: bevy::log::Level::INFO,
// provide custom log layer to receive logging events
custom_layer: bevy_debug_log::log_capture_layer,

View File

@@ -9,6 +9,7 @@ use crate::{
GameState,
aim::AimTarget,
character::CharacterHierarchy,
control::ControlState,
global_observer,
head::ActiveHead,
heads::ActiveHeads,
@@ -17,23 +18,18 @@ use crate::{
physics_layers::GameLayer,
player::{Player, PlayerBodyMesh},
sounds::PlaySound,
utils::{billboards::Billboard, sprite_3d_animation::AnimationTimer},
utils::{billboards::Billboard, commands::IsServer, sprite_3d_animation::AnimationTimer},
};
use bevy::{pbr::NotShadowCaster, prelude::*};
use bevy_sprite3d::{Sprite3dBuilder, Sprite3dParams};
pub use healing::Healing;
use healing::HealingStateChanged;
use lightyear::{
connection::client::ClientState,
prelude::{Client, input::native::ActionState},
};
use serde::{Deserialize, Serialize};
#[derive(Event, Reflect)]
pub enum TriggerState {
Active,
Inactive,
}
#[derive(Event, Reflect)]
pub struct TriggerCashHeal;
#[derive(Debug, Copy, Clone, PartialEq, Reflect, Default, Serialize, Deserialize)]
pub enum HeadAbility {
#[default]
@@ -117,22 +113,34 @@ pub fn plugin(app: &mut App) {
Update,
(update, update_heal_ability).run_if(in_state(GameState::Playing)),
);
app.add_systems(
FixedUpdate,
on_trigger_state.run_if(in_state(GameState::Playing)),
);
global_observer!(app, on_trigger_state);
global_observer!(app, build_explosion_sprite);
}
fn on_trigger_state(
trigger: Trigger<TriggerState>,
mut res: ResMut<TriggerStateRes>,
player_head: Single<&ActiveHead, With<Player>>,
player: Query<(&ActiveHead, &ActionState<ControlState>), With<Player>>,
headdb: Res<HeadsDatabase>,
time: Res<Time>,
is_server: Option<Res<IsServer>>,
client: Query<&Client>,
) {
res.active = matches!(trigger.event(), TriggerState::Active);
if res.active {
let head_stats = headdb.head_stats(player_head.0);
res.next_trigger_timestamp = time.elapsed_secs() + head_stats.shoot_offset;
if let Ok(client) = client.single()
&& (client.state == ClientState::Connected && is_server.is_none())
{
return;
}
for (player_head, controls) in player.iter() {
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

@@ -46,11 +46,9 @@ fn on_trigger_thrown(
let pos = state.pos;
let vel = if let Some(target) = state.target {
let t = query_transform
.get(target)
.expect("target must have transform");
let vel = if let Some(target) = state.target
&& let Ok(t) = query_transform.get(target)
{
launch_velocity(pos, t.translation, SPEED, 9.81)
.map(|(low, _)| low)
.unwrap()

View File

@@ -1,20 +1,13 @@
use super::{BackbackSwapEvent, Backpack, UiHeadState};
use super::{Backpack, UiHeadState};
use crate::{
GameState, HEDZ_GREEN, global_observer, heads::HeadsImages, loading_assets::UIAssets,
sounds::PlaySound,
GameState, HEDZ_GREEN, backpack::BackbackSwapEvent, control::ControlState, heads::HeadsImages,
loading_assets::UIAssets, sounds::PlaySound,
};
use bevy::{ecs::spawn::SpawnIter, prelude::*};
use lightyear::prelude::input::native::ActionState;
static HEAD_SLOTS: usize = 5;
#[derive(Event, Clone, Copy, Reflect, PartialEq)]
pub enum BackpackAction {
Left,
Right,
Swap,
OpenClose,
}
#[derive(Component, Default)]
struct BackpackMarker;
@@ -30,7 +23,8 @@ struct HeadImage(pub usize);
#[derive(Component, Default)]
struct HeadDamage(pub usize);
#[derive(Resource, Default, Debug)]
#[derive(Resource, Default, Debug, Reflect)]
#[reflect(Resource, Default)]
struct BackpackUiState {
heads: [Option<UiHeadState>; 5],
scroll: usize,
@@ -46,6 +40,7 @@ impl BackpackUiState {
}
pub fn plugin(app: &mut App) {
app.register_type::<BackpackUiState>();
app.init_resource::<BackpackUiState>();
app.add_systems(OnEnter(GameState::Playing), setup);
app.add_systems(
@@ -53,8 +48,7 @@ pub fn plugin(app: &mut App) {
(update, sync_on_change, update_visibility, update_count)
.run_if(in_state(GameState::Playing)),
);
global_observer!(app, swap_head_inputs);
app.add_systems(FixedUpdate, swap_head_inputs);
}
fn setup(mut commands: Commands, assets: Res<UIAssets>) {
@@ -262,52 +256,58 @@ fn update(
}
fn swap_head_inputs(
trigger: Trigger<BackpackAction>,
backpack: Res<Backpack>,
player: Query<(&ActionState<ControlState>, Ref<Backpack>)>,
mut commands: Commands,
mut state: ResMut<BackpackUiState>,
time: Res<Time>,
) {
if state.count == 0 {
return;
}
for (controls, backpack) in player.iter() {
if state.count == 0 {
return;
}
let action = *trigger.event();
if action == BackpackAction::OpenClose {
state.open = !state.open;
commands.trigger(PlaySound::Backpack { open: state.open });
}
if controls.backpack_toggle {
state.open = !state.open;
commands.trigger(PlaySound::Backpack { open: state.open });
}
if !state.open {
return;
}
if !state.open {
return;
}
let mut changed = false;
if action == BackpackAction::Left && state.current_slot > 0 {
state.current_slot -= 1;
changed = true;
}
if action == BackpackAction::Right && state.current_slot < state.count.saturating_sub(1) {
state.current_slot += 1;
changed = true;
}
if action == BackpackAction::Swap {
commands.trigger(BackbackSwapEvent(state.current_slot));
}
let mut changed = false;
if controls.backpack_left && state.current_slot > 0 {
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.trigger(PlaySound::Selection);
sync(&backpack, &mut state, time.elapsed_secs());
if changed {
commands.trigger(PlaySound::Selection);
sync(&backpack, &mut state, time.elapsed_secs());
}
}
}
fn sync_on_change(backpack: Res<Backpack>, mut state: ResMut<BackpackUiState>, time: Res<Time>) {
if backpack.is_changed() || backpack.reloading() {
sync(&backpack, &mut state, time.elapsed_secs());
fn sync_on_change(
backpack: Query<Ref<Backpack>>,
mut state: ResMut<BackpackUiState>,
time: Res<Time>,
) {
for backpack in backpack.iter() {
if backpack.is_changed() || backpack.reloading() {
sync(&backpack, &mut state, time.elapsed_secs());
}
}
}
fn sync(backpack: &Res<Backpack>, state: &mut ResMut<BackpackUiState>, time: f32) {
fn sync(backpack: &Backpack, state: &mut ResMut<BackpackUiState>, time: f32) {
state.count = backpack.heads.len();
state.scroll = state.scroll.min(state.count.saturating_sub(HEAD_SLOTS));

View File

@@ -5,11 +5,12 @@ use crate::{
cash::CashCollectEvent, global_observer, head_drop::HeadCollected, heads::HeadState,
heads_database::HeadsDatabase,
};
pub use backpack_ui::BackpackAction;
use bevy::prelude::*;
use serde::{Deserialize, Serialize};
pub use ui_head_state::UiHeadState;
#[derive(Resource, Default)]
#[derive(Component, Default, Reflect, Serialize, Deserialize, PartialEq)]
#[reflect(Component)]
pub struct Backpack {
pub heads: Vec<HeadState>,
}
@@ -38,7 +39,7 @@ impl Backpack {
pub struct BackbackSwapEvent(pub usize);
pub fn plugin(app: &mut App) {
app.init_resource::<Backpack>();
app.register_type::<Backpack>();
app.add_plugins(backpack_ui::plugin);
@@ -48,14 +49,18 @@ pub fn plugin(app: &mut App) {
fn on_head_collect(
trigger: Trigger<HeadCollected>,
mut cmds: Commands,
mut backpack: ResMut<Backpack>,
mut backpack: Query<&mut Backpack>,
heads_db: Res<HeadsDatabase>,
) {
) -> Result {
let HeadCollected(head) = *trigger.event();
let mut backpack = backpack.get_mut(trigger.target())?;
if backpack.contains(head) {
cmds.trigger(CashCollectEvent);
} else {
backpack.insert(head, heads_db.as_ref());
}
Ok(())
}

View File

@@ -1,11 +1,12 @@
use crate::{
abilities::TriggerCashHeal, cash::CashResource, global_observer, hitpoints::Hitpoints,
player::Player, sounds::PlaySound,
cash::CashResource, control::ControlState, hitpoints::Hitpoints, player::Player,
sounds::PlaySound,
};
use bevy::prelude::*;
use lightyear::prelude::input::native::ActionState;
pub fn plugin(app: &mut App) {
global_observer!(app, on_heal_trigger);
app.add_systems(FixedUpdate, on_heal_trigger);
}
#[derive(Debug, PartialEq, Eq)]
@@ -15,27 +16,28 @@ struct HealAction {
}
fn on_heal_trigger(
_trigger: Trigger<TriggerCashHeal>,
mut cmds: Commands,
mut cash: ResMut<CashResource>,
mut query: Query<&mut Hitpoints, With<Player>>,
mut query: Query<(&mut Hitpoints, &ActionState<ControlState>), With<Player>>,
) {
let Ok(mut hp) = query.single_mut() else {
return;
};
for (mut hp, controls) in query.iter_mut() {
if !controls.cash_heal {
continue;
}
if hp.max() || cash.cash == 0 {
return;
if hp.max() || cash.cash == 0 {
return;
}
let action = heal(cash.cash, hp.get().1 - hp.get().0);
hp.heal(action.damage_healed);
cash.cash = cash.cash.saturating_sub(action.cost);
//TODO: trigger ui cost animation
cmds.trigger(PlaySound::CashHeal);
}
let action = heal(cash.cash, hp.get().1 - hp.get().0);
hp.heal(action.damage_healed);
cash.cash = cash.cash.saturating_sub(action.cost);
//TODO: trigger ui cost animation
cmds.trigger(PlaySound::CashHeal);
}
fn heal(cash: i32, damage: u32) -> HealAction {

View File

@@ -84,7 +84,7 @@ pub fn plugin(app: &mut App) {
fn spawn(
mut commands: Commands,
query: Query<(Entity, &AnimatedCharacter), Added<AnimatedCharacter>>,
query: Query<(Entity, &AnimatedCharacter), Changed<AnimatedCharacter>>,
gltf_assets: Res<Assets<Gltf>>,
assets: Res<GameAssets>,
heads_db: Res<HeadsDatabase>,

View File

@@ -3,7 +3,7 @@ use crate::{
GameState,
abilities::TriggerStateRes,
animation::AnimationFlags,
control::{Controls, SelectedController, controls::ControllerSettings},
control::{ControlState, SelectedController, controls::ControllerSettings},
head::ActiveHead,
heads_database::{HeadControls, HeadsDatabase},
physics_layers::GameLayer,
@@ -16,7 +16,7 @@ use happy_feet::prelude::{
GroundFriction, Grounding, GroundingConfig, KinematicVelocity, MoveInput, SteppingBehaviour,
SteppingConfig,
};
use lightyear::prelude::Replicated;
use lightyear::prelude::{Replicated, input::native::ActionState};
use serde::{Deserialize, Serialize};
pub fn plugin(app: &mut App) {
@@ -26,58 +26,51 @@ pub fn plugin(app: &mut App) {
app.add_systems(
PreUpdate,
reset_upon_switch
.run_if(in_state(GameState::Playing))
.before(ControllerSet::ApplyControlsRun)
.before(ControllerSet::ApplyControlsFly),
reset_upon_switch.run_if(in_state(GameState::Playing)),
);
app.add_systems(
PreUpdate,
set_animation_flags
.run_if(in_state(GameState::Playing))
.after(ControllerSet::ApplyControlsRun)
.after(ControllerSet::ApplyControlsFly),
set_animation_flags.run_if(in_state(GameState::Playing)),
);
app.add_systems(
FixedPreUpdate,
decelerate.run_if(in_state(GameState::Playing)),
FixedUpdate,
decelerate
.after(ControllerSet::ApplyControlsRun)
.after(ControllerSet::ApplyControlsFly)
.run_if(in_state(GameState::Playing)),
);
app.add_systems(Update, add_controller_bundle);
}
fn set_animation_flags(
controls: Res<Controls>,
trigger: Res<TriggerStateRes>,
player: Single<(&Grounding, &mut AnimationFlags), (With<Player>, Without<Replicated>)>,
mut player: Query<
(&Grounding, &mut AnimationFlags, &ActionState<ControlState>),
(With<Player>, Without<Replicated>),
>,
) {
let mut direction = controls.keyboard_state.move_dir;
let deadzone = 0.2;
for (grounding, mut flags, controls) in player.iter_mut() {
let direction = controls.move_dir;
let deadzone = 0.2;
let (grounding, mut flags) = player.into_inner();
if let Some(gamepad) = controls.gamepad_state {
direction += gamepad.move_dir;
}
if flags.any_direction {
if direction.length_squared() < deadzone {
flags.any_direction = false;
if flags.any_direction {
if direction.length_squared() < deadzone {
flags.any_direction = false;
}
} else if direction.length_squared() > deadzone {
flags.any_direction = true;
}
} else if direction.length_squared() > deadzone {
flags.any_direction = true;
}
if flags.shooting != trigger.is_active() {
flags.shooting = trigger.is_active();
}
// `apply_controls` sets the jump flag when the player actually jumps.
// Unset the flag on hitting the ground
if grounding.is_grounded() {
flags.jumping = false;
// `apply_controls` sets the jump flag when the player actually jumps.
// Unset the flag on hitting the ground
if grounding.is_grounded() {
flags.jumping = false;
}
}
}

View File

@@ -2,6 +2,7 @@ use super::{ControlState, ControllerSet};
use crate::{GameState, control::controller_common::MovementSpeedFactor, player::PlayerBodyMesh};
use bevy::prelude::*;
use happy_feet::prelude::MoveInput;
use lightyear::prelude::input::native::ActionState;
use std::f32::consts::PI;
pub struct CharacterControllerPlugin;
@@ -9,7 +10,7 @@ pub struct CharacterControllerPlugin;
impl Plugin for CharacterControllerPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
PreUpdate,
FixedUpdate,
(rotate_rig, apply_controls)
.chain()
.in_set(ControllerSet::ApplyControlsFly)
@@ -19,16 +20,18 @@ impl Plugin for CharacterControllerPlugin {
}
fn rotate_rig(
mut rig_transform_q: Option<Single<&mut Transform, With<PlayerBodyMesh>>>,
controls: Res<ControlState>,
actions: Query<&ActionState<ControlState>>,
mut player: Query<(&mut Transform, &ChildOf), With<PlayerBodyMesh>>,
) {
if controls.view_mode {
return;
}
for (mut rig_transform, child_of) in player.iter_mut() {
let controls = actions.get(child_of.parent()).unwrap();
let look_dir = controls.look_dir;
if controls.view_mode {
continue;
}
let look_dir = controls.look_dir;
if let Some(ref mut rig_transform) = rig_transform_q {
// todo: Make consistent with the running controller
let sensitivity = 0.001;
let max_pitch = 35.0 * PI / 180.0;

View File

@@ -8,13 +8,14 @@ use crate::{
};
use bevy::prelude::*;
use happy_feet::prelude::{Grounding, KinematicVelocity, MoveInput};
use lightyear::prelude::input::native::ActionState;
pub struct CharacterControllerPlugin;
impl Plugin for CharacterControllerPlugin {
fn build(&self, app: &mut App) {
app.add_systems(
PreUpdate,
FixedUpdate,
(rotate_view, apply_controls)
.chain()
.in_set(ControllerSet::ApplyControlsRun)
@@ -24,20 +25,21 @@ impl Plugin for CharacterControllerPlugin {
}
fn rotate_view(
controls: Res<ControlState>,
mut player: Query<&mut Transform, With<PlayerBodyMesh>>,
actions: Query<&ActionState<ControlState>>,
mut player: Query<(&mut Transform, &ChildOf), With<PlayerBodyMesh>>,
) {
if controls.view_mode {
return;
}
for (mut tr, child_of) in player.iter_mut() {
let controls = actions.get(child_of.parent()).unwrap();
if controls.view_mode {
continue;
}
for mut tr in player.iter_mut() {
tr.rotate_y(controls.look_dir.x * -0.001);
}
}
fn apply_controls(
controls: Res<ControlState>,
mut character: Query<(
&mut MoveInput,
&mut Grounding,
@@ -45,12 +47,20 @@ fn apply_controls(
&mut AnimationFlags,
&ControllerSettings,
&MovementSpeedFactor,
&ActionState<ControlState>,
)>,
rig_transform_q: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>,
is_server: Option<Res<IsServer>>,
) {
let Ok((mut move_input, mut grounding, mut velocity, mut flags, settings, move_factor)) =
character.single_mut()
let Ok((
mut move_input,
mut grounding,
mut velocity,
mut flags,
settings,
move_factor,
controls,
)) = character.single_mut()
else {
return;
};

View File

@@ -1,10 +1,8 @@
use super::{ControlState, Controls};
use crate::{
GameState,
abilities::{TriggerCashHeal, TriggerState},
backpack::BackpackAction,
control::{CharacterInputEnabled, ControllerSet},
heads::SelectActiveHead,
player::Player,
};
use bevy::{
input::{
@@ -14,6 +12,7 @@ use bevy::{
},
prelude::*,
};
use lightyear::prelude::{client::input::InputSet::WriteClientInputs, input::native::ActionState};
use serde::{Deserialize, Serialize};
pub fn plugin(app: &mut App) {
@@ -22,12 +21,12 @@ pub fn plugin(app: &mut App) {
app.register_type::<ControllerSettings>();
app.add_systems(
PreUpdate,
FixedUpdate,
(
gamepad_controls,
keyboard_controls,
mouse_rotate,
mouse_click.run_if(on_event::<MouseButtonInput>),
mouse_click,
gamepad_connections.run_if(on_event::<GamepadEvent>),
combine_controls,
)
@@ -38,9 +37,12 @@ pub fn plugin(app: &mut App) {
.and(resource_exists_and_equals(CharacterInputEnabled::On)),
),
);
app.add_systems(FixedPreUpdate, buffer_inputs.in_set(WriteClientInputs));
app.add_systems(
Update,
char_controls_state.run_if(in_state(GameState::Playing)),
reset_control_state_on_disable.run_if(in_state(GameState::Playing)),
);
}
@@ -51,7 +53,17 @@ pub struct ControllerSettings {
pub jump_force: f32,
}
fn char_controls_state(
/// 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<Player>>,
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>,
@@ -62,20 +74,24 @@ fn char_controls_state(
}
}
/// 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();
if let Some(gamepad) = controls.gamepad_state {
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;
} else {
combined_controls.look_dir = keyboard.look_dir;
combined_controls.move_dir = keyboard.move_dir;
combined_controls.jump = keyboard.jump;
combined_controls.view_mode = keyboard.view_mode;
};
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;
}
/// Applies a square deadzone to a Vec2
@@ -86,11 +102,8 @@ fn deadzone_square(v: Vec2, min: f32) -> Vec2 {
)
}
fn gamepad_controls(
mut commands: Commands,
gamepads: Query<(Entity, &Gamepad)>,
mut controls: ResMut<Controls>,
) {
/// Collect gamepad inputs
fn gamepad_controls(gamepads: Query<(Entity, &Gamepad)>, mut controls: ResMut<Controls>) {
let Some((_e, gamepad)) = gamepads.iter().next() else {
if controls.gamepad_state.is_some() {
controls.gamepad_state = None;
@@ -101,8 +114,6 @@ fn gamepad_controls(
let deadzone_left_stick = 0.15;
let deadzone_right_stick = 0.15;
// info!("gamepad: {:?}", gamepad);
let rotate = gamepad
.get(GamepadButton::RightTrigger2)
.unwrap_or_default();
@@ -129,47 +140,28 @@ fn gamepad_controls(
look_dir,
jump: gamepad.pressed(GamepadButton::South),
view_mode: gamepad.pressed(GamepadButton::LeftTrigger2),
trigger: gamepad.pressed(GamepadButton::RightTrigger2),
just_triggered: gamepad.just_pressed(GamepadButton::RightTrigger2),
select_left: gamepad.just_pressed(GamepadButton::LeftTrigger),
select_right: gamepad.just_pressed(GamepadButton::RightTrigger),
backpack_left: gamepad.just_pressed(GamepadButton::DPadLeft),
backpack_right: gamepad.just_pressed(GamepadButton::DPadRight),
backpack_swap: gamepad.just_pressed(GamepadButton::DPadDown),
backpack_toggle: gamepad.just_pressed(GamepadButton::DPadUp),
cash_heal: gamepad.just_pressed(GamepadButton::East),
};
if gamepad.just_pressed(GamepadButton::RightTrigger2) {
commands.trigger(TriggerState::Active);
}
if gamepad.just_released(GamepadButton::RightTrigger2) {
commands.trigger(TriggerState::Inactive);
}
if gamepad.just_pressed(GamepadButton::LeftTrigger) {
commands.trigger(SelectActiveHead::Left);
}
if gamepad.just_pressed(GamepadButton::RightTrigger) {
commands.trigger(SelectActiveHead::Right);
}
if gamepad.just_pressed(GamepadButton::DPadLeft) {
commands.trigger(BackpackAction::Left);
}
if gamepad.just_pressed(GamepadButton::DPadRight) {
commands.trigger(BackpackAction::Right);
}
if gamepad.just_pressed(GamepadButton::DPadDown) {
commands.trigger(BackpackAction::Swap);
}
if gamepad.just_pressed(GamepadButton::DPadUp) {
commands.trigger(BackpackAction::OpenClose);
}
if gamepad.just_pressed(GamepadButton::East) {
commands.trigger(TriggerCashHeal);
}
if controls
.gamepad_state
.as_ref()
.map(|last_state| *last_state != state)
.unwrap_or(true)
{
// info!("gamepad state changed: {:?}", state);
controls.gamepad_state = Some(state);
}
}
/// Collect mouse movement input
fn mouse_rotate(mut mouse: EventReader<MouseMotion>, mut controls: ResMut<Controls>) {
controls.keyboard_state.look_dir = Vec2::ZERO;
@@ -178,11 +170,8 @@ fn mouse_rotate(mut mouse: EventReader<MouseMotion>, mut controls: ResMut<Contro
}
}
fn keyboard_controls(
mut commands: Commands,
keyboard: Res<ButtonInput<KeyCode>>,
mut controls: ResMut<Controls>,
) {
/// Collect keyboard input
fn keyboard_controls(keyboard: Res<ButtonInput<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];
@@ -197,35 +186,22 @@ 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);
if keyboard.just_pressed(KeyCode::KeyB) {
commands.trigger(BackpackAction::OpenClose);
}
if keyboard.just_pressed(KeyCode::Enter) {
commands.trigger(BackpackAction::Swap);
}
if keyboard.just_pressed(KeyCode::Comma) {
commands.trigger(BackpackAction::Left);
}
if keyboard.just_pressed(KeyCode::Period) {
commands.trigger(BackpackAction::Right);
}
if keyboard.just_pressed(KeyCode::KeyQ) {
commands.trigger(SelectActiveHead::Left);
}
if keyboard.just_pressed(KeyCode::KeyE) {
commands.trigger(SelectActiveHead::Right);
}
if keyboard.just_pressed(KeyCode::Enter) {
commands.trigger(TriggerCashHeal);
}
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 = keyboard.just_pressed(KeyCode::KeyB);
controls.keyboard_state.backpack_swap = keyboard.just_pressed(KeyCode::Enter);
controls.keyboard_state.backpack_left = keyboard.just_pressed(KeyCode::Comma);
controls.keyboard_state.backpack_right = keyboard.just_pressed(KeyCode::Period);
controls.keyboard_state.select_left = keyboard.just_pressed(KeyCode::KeyQ);
controls.keyboard_state.select_right = keyboard.just_pressed(KeyCode::KeyE);
controls.keyboard_state.cash_heal = keyboard.just_pressed(KeyCode::Enter);
}
fn mouse_click(mut events: EventReader<MouseButtonInput>, mut commands: Commands) {
/// 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 {
@@ -233,20 +209,23 @@ fn mouse_click(mut events: EventReader<MouseButtonInput>, mut commands: Commands
state: ButtonState::Pressed,
..
} => {
commands.trigger(TriggerState::Active);
controls.keyboard_state.trigger = true;
controls.keyboard_state.just_triggered = true;
}
MouseButtonInput {
button: MouseButton::Left,
state: ButtonState::Released,
..
} => {
commands.trigger(TriggerState::Inactive);
controls.keyboard_state.trigger = false;
controls.keyboard_state.just_triggered = false;
}
_ => {}
}
}
}
/// 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 {

View File

@@ -4,7 +4,8 @@ use crate::{
heads_database::{HeadControls, HeadsDatabase},
player::Player,
};
use bevy::prelude::*;
use bevy::{ecs::entity::MapEntities, prelude::*};
use serde::{Deserialize, Serialize};
pub mod controller_common;
pub mod controller_flying;
@@ -19,7 +20,7 @@ enum ControllerSet {
ApplyControlsRun,
}
#[derive(Resource, Debug, Clone, Copy, Default, PartialEq)]
#[derive(Resource, Debug, Clone, Copy, Default, PartialEq, Serialize, Deserialize, Reflect)]
pub struct ControlState {
/// Movement direction with a maximum length of 1.0
pub move_dir: Vec2,
@@ -27,6 +28,19 @@ pub struct ControlState {
pub jump: bool,
/// Determines if the camera can rotate freely around the player
pub view_mode: 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 MapEntities for ControlState {
fn map_entities<E: EntityMapper>(&mut self, _entity_mapper: &mut E) {}
}
#[derive(Resource, Debug, Default)]
@@ -60,7 +74,7 @@ pub fn plugin(app: &mut App) {
app.add_event::<ControllerSwitchEvent>();
app.configure_sets(
PreUpdate,
FixedUpdate,
(
ControllerSet::CollectInputs,
ControllerSet::ApplyControlsFly.run_if(resource_equals(SelectedController(

View File

@@ -183,7 +183,9 @@ fn on_collect_head(
commands.trigger(PlaySound::HeadCollect);
}
commands.trigger(HeadCollected(drop.head_id));
commands
.entity(collider)
.trigger(HeadCollected(drop.head_id));
commands.entity(child_of.parent()).despawn();
}
}

View File

@@ -4,6 +4,7 @@ use crate::{
GameState,
animation::AnimationFlags,
backpack::{BackbackSwapEvent, Backpack},
control::ControlState,
global_observer,
heads_database::HeadsDatabase,
hitpoints::Hitpoints,
@@ -11,6 +12,7 @@ use crate::{
sounds::PlaySound,
};
use bevy::prelude::*;
use lightyear::prelude::input::native::ActionState;
use serde::{Deserialize, Serialize};
pub static HEAD_COUNT: usize = 18;
@@ -171,12 +173,6 @@ impl ActiveHeads {
}
}
#[derive(Event, Reflect)]
pub enum SelectActiveHead {
Left,
Right,
}
#[derive(Event)]
pub struct HeadChanged(pub usize);
@@ -188,8 +184,8 @@ pub fn plugin(app: &mut App) {
Update,
(reload, sync_hp).run_if(in_state(GameState::Playing)),
);
app.add_systems(FixedUpdate, on_select_active_head);
global_observer!(app, on_select_active_head);
global_observer!(app, on_swap_backpack);
}
@@ -237,50 +233,49 @@ fn reload(
}
fn on_select_active_head(
trigger: Trigger<SelectActiveHead>,
mut commands: Commands,
mut query: Query<(&mut ActiveHeads, &mut Hitpoints), With<Player>>,
mut query: Query<(&mut ActiveHeads, &mut Hitpoints, &ActionState<ControlState>), With<Player>>,
) {
let Ok((mut active_heads, mut hp)) = query.single_mut() else {
return;
};
for (mut active_heads, mut hp, controls) in query.iter_mut() {
if !controls.select_right && !controls.select_left {
continue;
}
match trigger.event() {
SelectActiveHead::Right => {
if controls.select_right {
active_heads.selected_slot = (active_heads.selected_slot + 1) % HEAD_SLOTS;
}
SelectActiveHead::Left => {
if controls.select_left {
active_heads.selected_slot =
(active_heads.selected_slot + (HEAD_SLOTS - 1)) % HEAD_SLOTS;
}
}
commands.trigger(PlaySound::Selection);
commands.trigger(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);
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,
));
commands.trigger(HeadChanged(
active_heads.heads[active_heads.current_slot].unwrap().head,
));
}
}
}
fn on_swap_backpack(
trigger: Trigger<BackbackSwapEvent>,
mut commands: Commands,
mut query: Query<(&mut ActiveHeads, &mut Hitpoints), With<Player>>,
mut backpack: ResMut<Backpack>,
mut query: Query<(&mut ActiveHeads, &mut Hitpoints, &mut Backpack), With<Player>>,
) {
let backpack_slot = trigger.event().0;
let head = backpack.heads.get(backpack_slot).unwrap();
let Ok((mut active_heads, mut hp)) = query.single_mut() else {
let Ok((mut active_heads, mut hp, mut backpack)) = query.single_mut() else {
return;
};
let head = backpack.heads.get(backpack_slot).unwrap();
let selected_slot = active_heads.selected_slot;
let selected_head = active_heads.heads[selected_slot];

View File

@@ -18,7 +18,7 @@ use crate::{
},
};
use bevy::{pbr::NotShadowCaster, prelude::*};
use lightyear::prelude::{NetworkTarget, Replicate};
use lightyear::prelude::{Client, Connected, Disconnected, NetworkTarget, Replicate};
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
@@ -38,34 +38,58 @@ struct NpcSpawning {
pub struct SpawningBeam(pub f32);
#[derive(Event)]
struct OnCheckSpawns;
struct OnCheckSpawns {
on_client: bool,
}
#[derive(Event)]
pub struct SpawnCharacter(pub Vec3);
pub fn plugin(app: &mut App) {
app.init_resource::<NpcSpawning>();
app.add_systems(OnEnter(GameState::Playing), setup);
app.add_systems(FixedUpdate, setup.run_if(in_state(GameState::Playing)));
app.add_systems(Update, update_beams.run_if(in_state(GameState::Playing)));
global_observer!(app, on_spawn_check);
global_observer!(app, on_spawn);
}
fn setup(mut commands: Commands) {
commands.init_resource::<NpcSpawning>();
commands.trigger(OnCheckSpawns);
fn setup(
mut commands: Commands,
is_server: Option<Res<IsServer>>,
client: Query<(Option<&Connected>, Option<&Disconnected>), With<Client>>,
mut spawned: Local<bool>,
) {
if *spawned {
return;
}
if is_server.is_some() {
commands.init_resource::<NpcSpawning>();
commands.trigger(OnCheckSpawns { on_client: false });
*spawned = true;
} else if let Ok((connected, disconnected)) = client.single()
&& (connected.is_some() || disconnected.is_some())
{
commands.init_resource::<NpcSpawning>();
commands.trigger(OnCheckSpawns {
on_client: disconnected.is_some(),
});
*spawned = true;
}
}
fn on_spawn_check(
_trigger: Trigger<OnCheckSpawns>,
trigger: Trigger<OnCheckSpawns>,
mut commands: Commands,
query: Query<(Entity, &EnemySpawn, &Transform), Without<Npc>>,
heads_db: Res<HeadsDatabase>,
spawning: Res<NpcSpawning>,
is_server: Option<Res<IsServer>>,
) {
if is_server.is_none() {
if is_server.is_none() && !trigger.event().on_client {
return;
}
@@ -109,6 +133,8 @@ fn on_spawn_check(
fn on_kill(
trigger: Trigger<Kill>,
is_server: Option<Res<IsServer>>,
disconnected: Option<Single<&Disconnected>>,
mut commands: Commands,
query: Query<(&Transform, &EnemySpawn, &ActiveHead)>,
) {
@@ -123,7 +149,9 @@ fn on_kill(
}
commands.trigger(HeadDrops::new(transform.translation, head.0));
commands.trigger(OnCheckSpawns);
commands.trigger(OnCheckSpawns {
on_client: is_server.is_some() || disconnected.is_some(),
});
commands.entity(trigger.target()).despawn();

View File

@@ -1,9 +1,10 @@
use crate::{
GameState,
backpack::Backpack,
camera::{CameraArmRotation, CameraTarget},
cash::{Cash, CashCollectEvent},
character::{AnimatedCharacter, Character},
control::controller_common::PlayerCharacterController,
control::{ControlState, controller_common::PlayerCharacterController},
global_observer,
head::ActiveHead,
head_drop::HeadDrops,
@@ -22,7 +23,9 @@ use bevy::{
prelude::*,
window::{CursorGrabMode, PrimaryWindow},
};
use lightyear::prelude::{ControlledBy, Lifetime, NetworkTarget, PredictionTarget, Replicate};
use lightyear::prelude::{
ControlledBy, Lifetime, NetworkTarget, PredictionTarget, Replicate, input::native::ActionState,
};
use serde::{Deserialize, Serialize};
#[derive(Component, Default, Serialize, Deserialize, PartialEq)]
@@ -98,6 +101,8 @@ fn player_bundle(transform: Transform, heads_db: &Res<HeadsDatabase>) -> impl Bu
transform,
Visibility::default(),
PlayerCharacterController,
ActionState::<ControlState>::default(),
Backpack::default(),
children![(
Name::new("player-rig"),
PlayerBodyMesh,
@@ -197,6 +202,7 @@ fn on_update_head_mesh(
mut player: Single<&mut ActiveHead, With<Player>>,
head_db: Res<HeadsDatabase>,
audio_assets: Res<AudioAssets>,
sfx: Query<&AudioPlayer>,
) -> Result {
let (body_mesh, mesh_children) = *body_mesh;
@@ -213,10 +219,12 @@ fn on_update_head_mesh(
commands
.entity(animated_char)
.remove::<AnimatedCharacter>()
.insert(AnimatedCharacter::new(trigger.0));
//TODO: make part of full character mesh later
for child in mesh_children.iter().filter(|child| sfx.contains(*child)) {
commands.entity(child).despawn();
}
if head_db.head_stats(trigger.0).controls == HeadControls::Plane {
commands.entity(body_mesh).with_child((
Name::new("sfx"),

View File

@@ -1,9 +1,11 @@
use crate::{
abilities::BuildExplosionSprite,
animation::AnimationFlags,
backpack::Backpack,
camera::{CameraArmRotation, CameraTarget},
character::{self, AnimatedCharacter},
control::{
ControlState,
controller_common::{MovementSpeedFactor, PlayerCharacterController},
controls::ControllerSettings,
},
@@ -19,12 +21,13 @@ use bevy::prelude::*;
use happy_feet::{
grounding::GroundingState,
prelude::{
Character, CharacterDrag, CharacterGravity, CharacterMovement, GroundFriction, Grounding,
CharacterDrag, CharacterGravity, CharacterMovement, GroundFriction, Grounding,
GroundingConfig, KinematicVelocity, MoveInput, SteppingConfig,
},
};
use lightyear::prelude::{
ActionsChannel, AppComponentExt, PredictionMode, PredictionRegistrationExt,
input::native::InputPlugin,
};
use lightyear_serde::{
SerializationError, reader::ReadInteger, registry::SerializeFns, writer::WriteInteger,
@@ -32,14 +35,17 @@ use lightyear_serde::{
use serde::{Deserialize, Serialize};
pub fn plugin(app: &mut App) {
app.add_plugins(InputPlugin::<ControlState>::default());
app.register_component::<ActiveHead>();
app.register_component::<ActiveHeads>();
app.register_component::<AngularVelocity>();
app.register_component::<AnimatedCharacter>();
app.register_component::<AnimationFlags>();
app.register_component::<Backpack>();
app.register_component::<CameraArmRotation>();
app.register_component::<CameraTarget>();
app.register_component::<Character>();
app.register_component::<happy_feet::prelude::Character>();
app.register_component::<character::Character>();
app.register_component::<CharacterDrag>();
app.register_component::<CharacterGravity>();

View File

@@ -5,6 +5,7 @@ use bevy::ecs::{
system::{Commands, EntityCommands},
world::{EntityWorldMut, World},
};
use lightyear::prelude::Disconnected;
#[derive(Default, Resource)]
pub struct IsServer;
@@ -16,7 +17,8 @@ pub trait CommandExt {
impl<'w, 's> CommandExt for Commands<'w, 's> {
fn trigger_server(&mut self, event: impl Event) -> &mut Self {
self.queue(|world: &mut World| {
if world.contains_resource::<IsServer>() {
let mut query_state = world.query::<&Disconnected>();
if world.contains_resource::<IsServer>() || !query_state.query(world).is_empty() {
world.trigger(event);
}
});