Switch to replicon (#80)
This commit is contained in:
2056
Cargo.lock
generated
2056
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
15
Cargo.toml
15
Cargo.toml
@@ -55,26 +55,17 @@ bevy_asset_loader = "=0.24.0-rc.1"
|
||||
bevy_ballistic = { git = "https://github.com/rustunit/bevy_ballistic.git", rev = "b08ffec" }
|
||||
bevy_common_assets = { version = "0.14.0", features = ["ron"] }
|
||||
bevy_debug_log = { git = "https://github.com/rustunit/bevy_debug_log.git", rev = "86051a0" }
|
||||
bevy_replicon = "0.36.1"
|
||||
bevy_replicon_renet = "0.12.0"
|
||||
bevy_sprite3d = "7.0.0"
|
||||
bevy_trenchbroom = { version = "0.10", default-features = false, features = [
|
||||
"physics-integration",
|
||||
] }
|
||||
bevy_trenchbroom_avian = "0.10"
|
||||
clap = { version = "=4.5.47", features = ["derive"] }
|
||||
happy_feet = { git = "https://github.com/PROMETHIA-27/happy_feet.git", rev = "48a96cc", features = [
|
||||
"serde",
|
||||
] }
|
||||
lightyear = { version = "0.25", default-features = false, features = [
|
||||
"avian3d",
|
||||
"input_native",
|
||||
"interpolation",
|
||||
"netcode",
|
||||
"prediction",
|
||||
"replication",
|
||||
"std",
|
||||
"udp",
|
||||
"frame_interpolation",
|
||||
] }
|
||||
lightyear_serde = "0.25"
|
||||
nil = "0.14.0"
|
||||
rand = "=0.8.5"
|
||||
ron = "0.8"
|
||||
|
||||
@@ -21,11 +21,13 @@ bevy_asset_loader = { workspace = true }
|
||||
bevy_ballistic = { workspace = true }
|
||||
bevy_common_assets = { workspace = true }
|
||||
bevy_debug_log = { workspace = true }
|
||||
bevy_replicon = { workspace = true, features = ["client"] }
|
||||
bevy_replicon_renet = { workspace = true, features = ["client"] }
|
||||
bevy_sprite3d = { workspace = true }
|
||||
bevy_trenchbroom = { workspace = true, features = ["client"] }
|
||||
bevy_trenchbroom_avian = { workspace = true }
|
||||
clap = { workspace = true }
|
||||
happy_feet = { workspace = true }
|
||||
lightyear = { workspace = true }
|
||||
nil = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
ron = { workspace = true }
|
||||
|
||||
@@ -1,48 +1,50 @@
|
||||
use crate::config::ClientConfig;
|
||||
use avian3d::prelude::{
|
||||
Collider, ColliderAabb, ColliderDensity, ColliderMarker, ColliderOf, ColliderTransform,
|
||||
CollisionEventsEnabled, CollisionLayers, Position, Rotation, Sensor,
|
||||
CollisionEventsEnabled, CollisionLayers, Sensor,
|
||||
};
|
||||
use bevy::{
|
||||
ecs::bundle::BundleFromComponents, platform::cell::SyncCell, prelude::*, scene::SceneInstance,
|
||||
};
|
||||
use bevy_trenchbroom::geometry::Brushes;
|
||||
use lightyear::{
|
||||
frame_interpolation::FrameInterpolate,
|
||||
link::{LinkConditioner, prelude::*},
|
||||
netcode::Key,
|
||||
prelude::{
|
||||
client::{Input, InputDelayConfig, NetcodeConfig},
|
||||
input::native::InputMarker,
|
||||
*,
|
||||
},
|
||||
use bevy_replicon::{
|
||||
RepliconPlugins,
|
||||
client::{ClientSystems, confirm_history::ConfirmHistory},
|
||||
prelude::{ClientState, ClientTriggerExt, RepliconChannels},
|
||||
};
|
||||
use bevy_replicon_renet::{
|
||||
RenetChannelsExt, RepliconRenetPlugins,
|
||||
netcode::{ClientAuthentication, NetcodeClientTransport, NetcodeError},
|
||||
renet::{ConnectionConfig, RenetClient},
|
||||
};
|
||||
use bevy_trenchbroom::geometry::Brushes;
|
||||
use nil::prelude::Mutex;
|
||||
use shared::{
|
||||
GameState,
|
||||
control::ControlState,
|
||||
global_observer,
|
||||
player::Player,
|
||||
GameState, global_observer,
|
||||
protocol::{
|
||||
ClientEnteredPlaying, TbMapEntityId, TbMapEntityMapping,
|
||||
channels::UnorderedReliableChannel, messages::DespawnTbMapEntity,
|
||||
ClientEnteredPlaying, TbMapEntityId, TbMapEntityMapping, messages::DespawnTbMapEntity,
|
||||
},
|
||||
tb_entities::{Platform, PlatformTarget},
|
||||
tb_entities::{Movable, Platform, PlatformTarget},
|
||||
};
|
||||
use std::{
|
||||
env::current_exe,
|
||||
fs::File,
|
||||
io::{BufRead, BufReader},
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
net::{Ipv4Addr, UdpSocket},
|
||||
process::Stdio,
|
||||
sync::{LazyLock, mpsc},
|
||||
time::Duration,
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
/// Cache of server processes to be cleared at process exit
|
||||
static SERVER_PROCESSES: LazyLock<Mutex<Vec<std::process::Child>>> = LazyLock::new(Mutex::default);
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(GameState::Connecting), attempt_connection);
|
||||
app.add_plugins((RepliconPlugins, RepliconRenetPlugins));
|
||||
|
||||
app.add_systems(
|
||||
OnEnter(GameState::Connecting),
|
||||
connect_to_server.run_if(|config: Res<ClientConfig>| config.server.is_some()),
|
||||
);
|
||||
app.add_systems(
|
||||
Update,
|
||||
parse_local_server_stdout.run_if(resource_exists::<LocalServerStdout>),
|
||||
@@ -53,15 +55,31 @@ pub fn plugin(app: &mut App) {
|
||||
PreUpdate,
|
||||
(migrate_remote_entities, ApplyDeferred)
|
||||
.chain()
|
||||
.after(ReplicationSystems::Receive),
|
||||
.after(ClientSystems::Receive),
|
||||
);
|
||||
|
||||
// TODO: migrate this to connect_on_local_server_started
|
||||
app.add_systems(OnEnter(ClientState::Connected), on_connected_state);
|
||||
app.add_systems(OnExit(ClientState::Connected), on_disconnect);
|
||||
app.add_systems(
|
||||
OnEnter(GameState::Connecting),
|
||||
host_local_server.run_if(|config: Res<ClientConfig>| config.server.is_none()),
|
||||
);
|
||||
|
||||
global_observer!(app, on_connecting);
|
||||
global_observer!(app, on_connection_failed);
|
||||
global_observer!(app, on_connection_succeeded);
|
||||
global_observer!(app, temp_give_player_marker);
|
||||
global_observer!(app, connect_on_local_server_started);
|
||||
global_observer!(app, add_visual_interpolation_components);
|
||||
}
|
||||
|
||||
//
|
||||
// Client logic
|
||||
//
|
||||
|
||||
fn on_connected_state(mut commands: Commands, mut game_state: ResMut<NextState<GameState>>) {
|
||||
commands.client_trigger(ClientEnteredPlaying::default());
|
||||
game_state.set(GameState::Playing);
|
||||
}
|
||||
|
||||
fn on_disconnect() {
|
||||
info!("disconnected from the server");
|
||||
}
|
||||
|
||||
fn close_server_processes(mut app_exit: MessageReader<AppExit>) {
|
||||
@@ -75,142 +93,88 @@ fn close_server_processes(mut app_exit: MessageReader<AppExit>) {
|
||||
}
|
||||
}
|
||||
|
||||
fn attempt_connection(mut commands: Commands) -> Result {
|
||||
let mut args = std::env::args();
|
||||
let client_port = loop {
|
||||
match args.next().as_deref() {
|
||||
Some("--port") => {
|
||||
break args.next().unwrap().parse::<u16>().unwrap();
|
||||
}
|
||||
Some(_) => (),
|
||||
None => break 25564,
|
||||
}
|
||||
};
|
||||
//
|
||||
// Renet
|
||||
//
|
||||
|
||||
let client_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), client_port);
|
||||
let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 25565);
|
||||
let auth = Authentication::Manual {
|
||||
server_addr,
|
||||
client_id: client_port as u64,
|
||||
private_key: Key::default(),
|
||||
protocol_id: 0,
|
||||
};
|
||||
let sync_config = SyncConfig {
|
||||
jitter_multiple: 5,
|
||||
jitter_margin: Duration::from_millis(15),
|
||||
..default()
|
||||
};
|
||||
let conditioner = LinkConditioner::new(LinkConditionerConfig {
|
||||
incoming_latency: Duration::from_millis(10),
|
||||
incoming_jitter: Duration::from_millis(0),
|
||||
incoming_loss: 0.0,
|
||||
fn connect_to_server(
|
||||
mut commands: Commands,
|
||||
config: Res<ClientConfig>,
|
||||
channels: Res<RepliconChannels>,
|
||||
) -> Result {
|
||||
let server_channels_config = channels.server_configs();
|
||||
let client_channels_config = channels.client_configs();
|
||||
|
||||
let client = RenetClient::new(ConnectionConfig {
|
||||
server_channels_config,
|
||||
client_channels_config,
|
||||
..Default::default()
|
||||
});
|
||||
commands
|
||||
.spawn((
|
||||
Name::from("Client"),
|
||||
Client::default(),
|
||||
Link::new(Some(conditioner)),
|
||||
LocalAddr(client_addr),
|
||||
PeerAddr(server_addr),
|
||||
ReplicationReceiver::default(),
|
||||
client::NetcodeClient::new(
|
||||
auth,
|
||||
NetcodeConfig {
|
||||
client_timeout_secs: 3,
|
||||
..default()
|
||||
},
|
||||
)?,
|
||||
UdpIo::default(),
|
||||
PredictionManager::default(),
|
||||
InputTimeline(Timeline::from(Input::new(
|
||||
sync_config,
|
||||
InputDelayConfig::balanced(),
|
||||
))),
|
||||
))
|
||||
.trigger(|entity| Connect { entity });
|
||||
|
||||
commands.insert_resource(client);
|
||||
commands.insert_resource(client_transport(&config)?);
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_connection_succeeded(
|
||||
_trigger: On<Add, Connected>,
|
||||
state: Res<State<GameState>>,
|
||||
mut change_state: ResMut<NextState<GameState>>,
|
||||
mut sender: Single<&mut EventSender<ClientEnteredPlaying>>,
|
||||
) {
|
||||
if *state == GameState::Connecting {
|
||||
change_state.set(GameState::Playing);
|
||||
sender.trigger::<UnorderedReliableChannel>(ClientEnteredPlaying);
|
||||
}
|
||||
}
|
||||
fn client_transport(config: &ClientConfig) -> Result<NetcodeClientTransport, NetcodeError> {
|
||||
let current_time = SystemTime::now()
|
||||
.duration_since(SystemTime::UNIX_EPOCH)
|
||||
.unwrap();
|
||||
let client_id = current_time.as_millis() as u64;
|
||||
let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, config.port))?;
|
||||
let server_addr = config
|
||||
.server
|
||||
.flatten()
|
||||
.unwrap_or_else(|| "127.0.0.1:31111".parse().unwrap());
|
||||
let authentication = ClientAuthentication::Unsecure {
|
||||
client_id,
|
||||
protocol_id: 0,
|
||||
server_addr,
|
||||
user_data: None,
|
||||
};
|
||||
|
||||
/// A client starts `Disconnected`, so in order to tell if it *actually* failed to connect/disconnected
|
||||
/// vs. simply having been created, we need some extra state.
|
||||
#[derive(Component)]
|
||||
struct ClientActive;
|
||||
|
||||
fn on_connecting(trigger: On<Add, Connecting>, mut commands: Commands) {
|
||||
commands.entity(trigger.event().entity).insert(ClientActive);
|
||||
info!("attempting connection to {server_addr}");
|
||||
NetcodeClientTransport::new(current_time, authentication, socket)
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
struct LocalServerStdout(SyncCell<mpsc::Receiver<String>>);
|
||||
|
||||
fn on_connection_failed(
|
||||
trigger: On<Add, Disconnected>,
|
||||
disconnected: Query<&Disconnected>,
|
||||
mut commands: Commands,
|
||||
client_active: Query<&ClientActive>,
|
||||
mut opened_server: Local<bool>,
|
||||
) -> Result {
|
||||
let disconnected = disconnected.get(trigger.event().entity).unwrap();
|
||||
if *opened_server {
|
||||
panic!(
|
||||
"failed to connect to local server: {:?}",
|
||||
disconnected.reason
|
||||
);
|
||||
}
|
||||
fn host_local_server(mut commands: Commands) -> Result {
|
||||
// the server executable is assumed to be adjacent to the client executable
|
||||
let mut exe_path = current_exe().expect("failed to get path of client executable");
|
||||
exe_path.set_file_name("server");
|
||||
let server_log_file = File::create("server.log")?;
|
||||
let mut server_process = std::process::Command::new(exe_path)
|
||||
.args(["--timeout", "60", "--close-on-client-disconnect"])
|
||||
.env("NO_COLOR", "1")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(server_log_file)
|
||||
.spawn()
|
||||
.expect("failed to start server");
|
||||
let server_stdout = server_process.stdout.take().unwrap();
|
||||
SERVER_PROCESSES.lock().push(server_process);
|
||||
|
||||
let client = trigger.event().entity;
|
||||
if client_active.contains(client) {
|
||||
commands.entity(client).remove::<ClientActive>();
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
||||
// the server executable is assumed to be adjacent to the client executable
|
||||
let mut exe_path = current_exe().expect("failed to get path of client executable");
|
||||
exe_path.set_file_name("server");
|
||||
let server_log_file = File::create("server.log")?;
|
||||
let mut server_process = std::process::Command::new(exe_path)
|
||||
.args(["--timeout", "60", "--close-on-client-disconnect"])
|
||||
.env("NO_COLOR", "1")
|
||||
.stdin(Stdio::null())
|
||||
.stdout(Stdio::piped())
|
||||
.stderr(server_log_file)
|
||||
.spawn()
|
||||
.expect("failed to start server");
|
||||
let server_stdout = server_process.stdout.take().unwrap();
|
||||
SERVER_PROCESSES.lock().push(server_process);
|
||||
let stdout = BufReader::new(server_stdout).lines();
|
||||
|
||||
let (tx, rx) = std::sync::mpsc::channel();
|
||||
|
||||
let stdout = BufReader::new(server_stdout).lines();
|
||||
|
||||
std::thread::spawn(move || {
|
||||
for line in stdout {
|
||||
match line {
|
||||
Ok(line) => {
|
||||
tx.send(line).unwrap();
|
||||
}
|
||||
Err(error) => {
|
||||
error!("error reading local server stdout: `{error}`");
|
||||
}
|
||||
std::thread::spawn(move || {
|
||||
for line in stdout {
|
||||
match line {
|
||||
Ok(line) => {
|
||||
tx.send(line).unwrap();
|
||||
}
|
||||
Err(error) => {
|
||||
error!("error reading local server stdout: `{error}`");
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
commands.insert_resource(LocalServerStdout(SyncCell::new(rx)));
|
||||
|
||||
*opened_server = true;
|
||||
}
|
||||
commands.insert_resource(LocalServerStdout(SyncCell::new(rx)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
@@ -231,27 +195,22 @@ fn parse_local_server_stdout(mut commands: Commands, mut stdout: ResMut<LocalSer
|
||||
}
|
||||
|
||||
fn connect_on_local_server_started(
|
||||
_trigger: On<LocalServerStarted>,
|
||||
_: On<LocalServerStarted>,
|
||||
commands: Commands,
|
||||
state: Res<State<GameState>>,
|
||||
mut commands: Commands,
|
||||
client: Single<Entity, With<Client>>,
|
||||
) {
|
||||
channels: Res<RepliconChannels>,
|
||||
config: Res<ClientConfig>,
|
||||
) -> Result<()> {
|
||||
if *state == GameState::Connecting {
|
||||
commands
|
||||
.entity(*client)
|
||||
.trigger(|entity| Connect { entity });
|
||||
connect_to_server(commands, config, channels)?;
|
||||
}
|
||||
}
|
||||
|
||||
fn temp_give_player_marker(trigger: On<Add, Player>, mut commands: Commands) {
|
||||
commands
|
||||
.entity(trigger.event().entity)
|
||||
.insert(InputMarker::<ControlState>::default());
|
||||
Ok(())
|
||||
}
|
||||
|
||||
#[allow(clippy::type_complexity)]
|
||||
fn migrate_remote_entities(
|
||||
query: Query<(Entity, &TbMapEntityId), (Added<TbMapEntityId>, With<Replicated>)>,
|
||||
query: Query<(Entity, &TbMapEntityId), (Added<TbMapEntityId>, With<ConfirmHistory>)>,
|
||||
children: Query<&Children>,
|
||||
mut commands: Commands,
|
||||
mut mapping: ResMut<TbMapEntityMapping>,
|
||||
@@ -286,6 +245,7 @@ fn received_remote_map_entity(
|
||||
move_component::<ColliderOf>(commands, clientside, serverside);
|
||||
move_component::<ColliderTransform>(commands, clientside, serverside);
|
||||
move_component::<CollisionEventsEnabled>(commands, clientside, serverside);
|
||||
move_component::<Movable>(commands, clientside, serverside);
|
||||
move_component::<Platform>(commands, clientside, serverside);
|
||||
move_component::<PlatformTarget>(commands, clientside, serverside);
|
||||
move_component::<SceneInstance>(commands, clientside, serverside);
|
||||
@@ -316,37 +276,16 @@ fn move_component<B: Bundle + BundleFromComponents>(
|
||||
|
||||
fn despawn_absent_map_entities(
|
||||
mut commands: Commands,
|
||||
mut messages: Query<&mut MessageReceiver<DespawnTbMapEntity>>,
|
||||
mut messages: MessageReader<DespawnTbMapEntity>,
|
||||
mut map: ResMut<TbMapEntityMapping>,
|
||||
) {
|
||||
for mut recv in messages.iter_mut() {
|
||||
for msg in recv.receive() {
|
||||
// the server may double-send DespawnTbMapEntity for a given ID, so ignore it if the entity
|
||||
// was already despawned.
|
||||
let Some(entity) = map.0.remove(&msg.0) else {
|
||||
continue;
|
||||
};
|
||||
for msg in messages.read() {
|
||||
// the server may double-send DespawnTbMapEntity for a given ID, so ignore it if the entity
|
||||
// was already despawned.
|
||||
let Some(entity) = map.0.remove(&msg.0) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
commands.entity(entity).despawn();
|
||||
}
|
||||
}
|
||||
|
||||
fn add_visual_interpolation_components(trigger: On<Add, Predicted>, mut commands: Commands) {
|
||||
commands.entity(trigger.entity).insert((
|
||||
FrameInterpolate::<Position> {
|
||||
// We must trigger change detection on visual interpolation
|
||||
// to make sure that child entities (sprites, meshes, text)
|
||||
// are also interpolated
|
||||
trigger_change_detection: true,
|
||||
..default()
|
||||
},
|
||||
FrameInterpolate::<Rotation> {
|
||||
// We must trigger change detection on visual interpolation
|
||||
// to make sure that child entities (sprites, meshes, text)
|
||||
// are also interpolated
|
||||
trigger_change_detection: true,
|
||||
..default()
|
||||
},
|
||||
));
|
||||
}
|
||||
|
||||
23
crates/client/src/config.rs
Normal file
23
crates/client/src/config.rs
Normal file
@@ -0,0 +1,23 @@
|
||||
use bevy::prelude::*;
|
||||
use clap::Parser;
|
||||
use std::net::SocketAddr;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
let config = ClientConfig::parse();
|
||||
|
||||
app.insert_resource(config);
|
||||
}
|
||||
|
||||
#[derive(Resource, Parser, Debug)]
|
||||
#[command(version, about, long_about = None)]
|
||||
pub struct ClientConfig {
|
||||
/// The port to use when connecting.
|
||||
#[arg(long, default_value_t = 0)]
|
||||
pub port: u16,
|
||||
/// The IP/port to connect to.
|
||||
/// If `None`, host a local server.
|
||||
/// If Some(None), connect to the default server (`127.0.0.1:31111`)
|
||||
/// Otherwise, connect to the given server
|
||||
#[arg(long)]
|
||||
pub server: Option<Option<SocketAddr>>,
|
||||
}
|
||||
@@ -1,5 +1,4 @@
|
||||
use bevy::prelude::*;
|
||||
use lightyear::prelude::input::native::ActionState;
|
||||
use shared::{
|
||||
GameState,
|
||||
control::{ControlState, ControllerSet, LookDirMovement},
|
||||
@@ -18,13 +17,11 @@ pub fn plugin(app: &mut App) {
|
||||
}
|
||||
|
||||
fn rotate_rig(
|
||||
actions: Query<&ActionState<ControlState>>,
|
||||
controls: Res<ControlState>,
|
||||
look_dir: Res<LookDirMovement>,
|
||||
mut player: Query<(&mut Transform, &ChildOf), With<PlayerBodyMesh>>,
|
||||
mut player: Query<&mut Transform, With<PlayerBodyMesh>>,
|
||||
) {
|
||||
for (mut rig_transform, child_of) in player.iter_mut() {
|
||||
let controls = actions.get(child_of.parent()).unwrap();
|
||||
|
||||
for mut rig_transform in player.iter_mut() {
|
||||
if controls.view_mode {
|
||||
continue;
|
||||
}
|
||||
|
||||
@@ -8,10 +8,6 @@ use bevy::{
|
||||
},
|
||||
prelude::*,
|
||||
};
|
||||
use lightyear::{
|
||||
input::client::InputSystems,
|
||||
prelude::input::native::{ActionState, InputMarker},
|
||||
};
|
||||
use shared::{
|
||||
control::{ControllerSet, LookDirMovement},
|
||||
player::PlayerBodyMesh,
|
||||
@@ -27,7 +23,7 @@ pub fn plugin(app: &mut App) {
|
||||
app.add_systems(PreUpdate, (cache_keyboard_state, cache_gamepad_state));
|
||||
|
||||
app.add_systems(
|
||||
FixedPreUpdate,
|
||||
PreUpdate,
|
||||
(
|
||||
reset_lookdir,
|
||||
gamepad_controls,
|
||||
@@ -39,18 +35,14 @@ pub fn plugin(app: &mut App) {
|
||||
get_lookdir,
|
||||
clear_keyboard_just,
|
||||
clear_gamepad_just,
|
||||
send_inputs,
|
||||
)
|
||||
.chain()
|
||||
.in_set(ControllerSet::CollectInputs)
|
||||
.before(InputSystems::WriteClientInputs)
|
||||
.run_if(
|
||||
in_state(GameState::Playing)
|
||||
.and(resource_exists_and_equals(CharacterInputEnabled::On)),
|
||||
),
|
||||
)
|
||||
.add_systems(
|
||||
FixedPreUpdate,
|
||||
buffer_inputs.in_set(InputSystems::WriteClientInputs),
|
||||
);
|
||||
|
||||
app.add_systems(
|
||||
@@ -61,11 +53,8 @@ pub fn plugin(app: &mut App) {
|
||||
|
||||
/// 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;
|
||||
fn send_inputs(mut writer: MessageWriter<ControlState>, controls: Res<ControlState>) {
|
||||
writer.write(*controls);
|
||||
}
|
||||
|
||||
fn reset_lookdir(mut look_dir: ResMut<LookDirMovement>) {
|
||||
@@ -177,7 +166,7 @@ fn combine_controls(controls: Res<Controls>, mut combined_controls: ResMut<Contr
|
||||
let keyboard = controls.keyboard_state;
|
||||
let gamepad = controls.gamepad_state.unwrap_or_default();
|
||||
|
||||
combined_controls.look_dir = Dir3::NEG_Z;
|
||||
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;
|
||||
@@ -197,9 +186,9 @@ fn get_lookdir(
|
||||
rig_transform: Option<Single<&GlobalTransform, With<PlayerBodyMesh>>>,
|
||||
) {
|
||||
controls.look_dir = if let Some(ref rig_transform) = rig_transform {
|
||||
rig_transform.forward()
|
||||
rig_transform.forward().as_vec3()
|
||||
} else {
|
||||
Dir3::NEG_Z
|
||||
Vec3::NEG_Z
|
||||
};
|
||||
}
|
||||
|
||||
@@ -252,7 +241,7 @@ fn gamepad_controls(
|
||||
|
||||
let state = ControlState {
|
||||
move_dir,
|
||||
look_dir: Dir3::NEG_Z,
|
||||
look_dir: Vec3::NEG_Z,
|
||||
jump: gamepad.pressed(GamepadButton::South),
|
||||
view_mode: gamepad.pressed(GamepadButton::LeftTrigger2),
|
||||
trigger: gamepad.pressed(GamepadButton::RightTrigger2),
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
use crate::GameState;
|
||||
use bevy::prelude::*;
|
||||
use bevy_replicon::client::ClientSystems;
|
||||
use shared::control::{ControlState, ControllerSet};
|
||||
|
||||
mod controller_flying;
|
||||
@@ -23,7 +24,9 @@ pub fn plugin(app: &mut App) {
|
||||
app.add_plugins((controller_flying::plugin, controls::plugin));
|
||||
|
||||
app.configure_sets(
|
||||
FixedPreUpdate,
|
||||
ControllerSet::CollectInputs.run_if(in_state(GameState::Playing)),
|
||||
PreUpdate,
|
||||
ControllerSet::CollectInputs
|
||||
.before(ClientSystems::Receive)
|
||||
.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
mod backpack;
|
||||
mod client;
|
||||
mod config;
|
||||
mod control;
|
||||
mod debug;
|
||||
mod enemy;
|
||||
@@ -22,13 +23,8 @@ use bevy_trenchbroom::prelude::*;
|
||||
use bevy_trenchbroom_avian::AvianPhysicsBackend;
|
||||
use camera::MainCamera;
|
||||
use heads_database::HeadDatabaseAsset;
|
||||
use lightyear::{
|
||||
avian3d::plugin::LightyearAvianPlugin, frame_interpolation::FrameInterpolationPlugin,
|
||||
prelude::client::ClientPlugins,
|
||||
};
|
||||
use loading_assets::AudioAssets;
|
||||
use shared::*;
|
||||
use std::time::Duration;
|
||||
|
||||
fn main() {
|
||||
let mut app = App::new();
|
||||
@@ -68,25 +64,7 @@ fn main() {
|
||||
bevy_debug_log::LogViewerPlugin::default()
|
||||
.auto_open_threshold(bevy::log::tracing::level_filters::LevelFilter::OFF),
|
||||
);
|
||||
app.add_plugins(
|
||||
PhysicsPlugins::default()
|
||||
.build()
|
||||
// TODO: This plugin is *not* disabled on the server. This is to solve a bug related to collider transform inheritance. See the
|
||||
// LightyearAvianPlugin below.
|
||||
// Periwink is looking into it at the moment. This **must** be disabled on the client, or positions break for NPCs and some other things.
|
||||
.disable::<PhysicsTransformPlugin>()
|
||||
// FrameInterpolation handles interpolating Position and Rotation
|
||||
.disable::<PhysicsInterpolationPlugin>(),
|
||||
);
|
||||
// TODO: This plugin is *not* inserted on the server. This is to solve a bug related to collider transform inheritance. See the
|
||||
// `.disable::<PhysicsTransformPlugin>()` above.
|
||||
// Periwink is looking into it at the moment. This **must** be inserted on the client, or positions break for NPCs and some other things.
|
||||
app.add_plugins(LightyearAvianPlugin::default());
|
||||
app.add_plugins(FrameInterpolationPlugin::<Position>::default());
|
||||
app.add_plugins(FrameInterpolationPlugin::<Rotation>::default());
|
||||
app.add_plugins(ClientPlugins {
|
||||
tick_duration: Duration::from_secs_f64(1.0 / 60.0),
|
||||
});
|
||||
app.add_plugins(PhysicsPlugins::default());
|
||||
app.add_plugins(Sprite3dPlugin);
|
||||
app.add_plugins(TrenchBroomPlugins(
|
||||
TrenchBroomConfig::new("hedz")
|
||||
@@ -116,7 +94,6 @@ fn main() {
|
||||
app.add_plugins(shared::player::plugin);
|
||||
app.add_plugins(shared::gates::plugin);
|
||||
app.add_plugins(shared::platforms::plugin);
|
||||
app.add_plugins(shared::protocol::plugin);
|
||||
app.add_plugins(shared::movables::plugin);
|
||||
app.add_plugins(shared::utils::billboards::plugin);
|
||||
app.add_plugins(shared::aim::plugin);
|
||||
@@ -140,10 +117,16 @@ fn main() {
|
||||
app.add_plugins(shared::utils::trail::plugin);
|
||||
app.add_plugins(shared::utils::auto_rotate::plugin);
|
||||
app.add_plugins(shared::tb_entities::plugin);
|
||||
app.add_plugins(shared::tick::plugin);
|
||||
app.add_plugins(shared::utils::explosions::plugin);
|
||||
|
||||
app.add_plugins(backpack::plugin);
|
||||
// Networking
|
||||
// The client/server plugin must go before the protocol, or else `ProtocolHasher` will not be available.
|
||||
app.add_plugins(client::plugin);
|
||||
app.add_plugins(shared::protocol::plugin);
|
||||
|
||||
app.add_plugins(backpack::plugin);
|
||||
app.add_plugins(config::plugin);
|
||||
app.add_plugins(control::plugin);
|
||||
app.add_plugins(debug::plugin);
|
||||
app.add_plugins(enemy::plugin);
|
||||
|
||||
@@ -4,14 +4,13 @@ use crate::{
|
||||
loading_assets::AudioAssets,
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
use lightyear::prelude::MessageReceiver;
|
||||
use shared::{
|
||||
player::PlayerBodyMesh,
|
||||
player::{LocalPlayerId, PlayerBodyMesh},
|
||||
protocol::{PlaySound, PlayerId, events::ClientHeadChanged, messages::AssignClientPlayer},
|
||||
};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.register_type::<ClientPlayerId>();
|
||||
app.register_type::<LocalPlayerId>();
|
||||
app.register_type::<LocalPlayer>();
|
||||
|
||||
app.init_state::<PlayerAssignmentState>();
|
||||
@@ -28,35 +27,32 @@ pub fn plugin(app: &mut App) {
|
||||
global_observer!(app, on_update_head_mesh);
|
||||
}
|
||||
|
||||
#[derive(Resource, Reflect)]
|
||||
#[reflect(Resource)]
|
||||
pub struct ClientPlayerId {
|
||||
id: u8,
|
||||
}
|
||||
|
||||
fn receive_player_id(
|
||||
mut commands: Commands,
|
||||
mut recv: Single<&mut MessageReceiver<AssignClientPlayer>>,
|
||||
mut client_assignments: MessageReader<AssignClientPlayer>,
|
||||
mut next: ResMut<NextState<PlayerAssignmentState>>,
|
||||
) {
|
||||
for AssignClientPlayer(id) in recv.receive() {
|
||||
commands.insert_resource(ClientPlayerId { id });
|
||||
for &AssignClientPlayer(id) in client_assignments.read() {
|
||||
commands.insert_resource(LocalPlayerId { id });
|
||||
next.set(PlayerAssignmentState::IdReceived);
|
||||
info!("player id `{id}` received");
|
||||
info!("player id `{}` received", id.id);
|
||||
}
|
||||
}
|
||||
|
||||
fn match_player_id(
|
||||
mut commands: Commands,
|
||||
players: Query<(Entity, &PlayerId), Changed<PlayerId>>,
|
||||
client: Res<ClientPlayerId>,
|
||||
client: Res<LocalPlayerId>,
|
||||
mut next: ResMut<NextState<PlayerAssignmentState>>,
|
||||
) {
|
||||
for (entity, player) in players.iter() {
|
||||
if player.id == client.id {
|
||||
for (entity, player_id) in players.iter() {
|
||||
if *player_id == client.id {
|
||||
commands.entity(entity).insert(LocalPlayer);
|
||||
next.set(PlayerAssignmentState::Confirmed);
|
||||
info!("player entity {entity:?} confirmed with id `{}`", player.id);
|
||||
info!(
|
||||
"player entity {entity:?} confirmed with id `{}`",
|
||||
player_id.id
|
||||
);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,12 +12,13 @@ avian3d = { workspace = true }
|
||||
bevy = { workspace = true, default-features = false }
|
||||
bevy-steamworks = { workspace = true }
|
||||
bevy_common_assets = { workspace = true }
|
||||
bevy_replicon = { workspace = true, features = ["server"] }
|
||||
bevy_replicon_renet = { workspace = true, features = ["server"] }
|
||||
bevy_sprite3d = { workspace = true }
|
||||
bevy_trenchbroom = { workspace = true }
|
||||
bevy_trenchbroom_avian = { workspace = true }
|
||||
clap = { version = "=4.5.47", features = ["derive"] }
|
||||
clap = { workspace = true }
|
||||
happy_feet = { workspace = true }
|
||||
lightyear = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
ron = { workspace = true }
|
||||
serde = { workspace = true }
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
use bevy::prelude::*;
|
||||
use lightyear::prelude::input::native::ActionState;
|
||||
use bevy_replicon::prelude::{ClientState, FromClient, SendMode, ServerTriggerExt, ToClients};
|
||||
use shared::{
|
||||
GameState,
|
||||
backpack::{
|
||||
@@ -7,7 +7,7 @@ use shared::{
|
||||
backpack_ui::{BackpackUiState, HEAD_SLOTS},
|
||||
},
|
||||
control::ControlState,
|
||||
protocol::PlaySound,
|
||||
protocol::{ClientToController, PlaySound},
|
||||
};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
@@ -15,23 +15,34 @@ pub fn plugin(app: &mut App) {
|
||||
FixedUpdate,
|
||||
sync_on_change.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
app.add_systems(FixedUpdate, swap_head_inputs);
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
swap_head_inputs.run_if(in_state(ClientState::Disconnected)),
|
||||
);
|
||||
}
|
||||
|
||||
fn swap_head_inputs(
|
||||
player: Query<(&ActionState<ControlState>, Ref<Backpack>)>,
|
||||
backpacks: Query<Ref<Backpack>>,
|
||||
clients: ClientToController,
|
||||
mut inputs: MessageReader<FromClient<ControlState>>,
|
||||
mut commands: Commands,
|
||||
mut state: Single<&mut BackpackUiState>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
for (controls, backpack) in player.iter() {
|
||||
for controls in inputs.read() {
|
||||
let player = clients.get_controller(controls.client_id);
|
||||
let backpack = backpacks.get(player).unwrap();
|
||||
|
||||
if state.count == 0 {
|
||||
return;
|
||||
}
|
||||
|
||||
if controls.backpack_toggle {
|
||||
state.open = !state.open;
|
||||
commands.trigger(PlaySound::Backpack { open: state.open });
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: PlaySound::Backpack { open: state.open },
|
||||
});
|
||||
}
|
||||
|
||||
if !state.open {
|
||||
@@ -52,7 +63,10 @@ fn swap_head_inputs(
|
||||
}
|
||||
|
||||
if changed {
|
||||
commands.trigger(PlaySound::Selection);
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: PlaySound::Selection,
|
||||
});
|
||||
sync(&backpack, &mut state, time.elapsed_secs());
|
||||
}
|
||||
}
|
||||
|
||||
@@ -3,7 +3,7 @@ use bevy::{
|
||||
ecs::{relationship::RelatedSpawner, spawn::SpawnWith},
|
||||
prelude::*,
|
||||
};
|
||||
use lightyear::prelude::{NetworkTarget, Replicate};
|
||||
use bevy_replicon::prelude::{Replicated, SendMode, ServerTriggerExt, ToClients};
|
||||
use shared::{
|
||||
global_observer,
|
||||
head_drop::{HeadCollected, HeadDrop, HeadDropEnableTime, HeadDrops, SecretHeadMarker},
|
||||
@@ -39,7 +39,10 @@ fn on_head_drop(
|
||||
};
|
||||
|
||||
if drop.impulse {
|
||||
commands.trigger(PlaySound::HeadDrop);
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: PlaySound::HeadDrop,
|
||||
});
|
||||
}
|
||||
|
||||
let mesh_addr = format!("{:?}", heads_db.head_stats(drop.head_id).ability).to_lowercase();
|
||||
@@ -70,16 +73,18 @@ fn on_head_drop(
|
||||
CollisionEventsEnabled,
|
||||
HeadDrop { head_id },
|
||||
HeadDropEnableTime(now + 1.2),
|
||||
Replicated,
|
||||
))
|
||||
.observe(on_collect_head);
|
||||
}
|
||||
})),
|
||||
Replicate::to_clients(NetworkTarget::All),
|
||||
Replicated,
|
||||
))
|
||||
.with_child((
|
||||
Billboard::All,
|
||||
SquishAnimation(2.6),
|
||||
GltfSceneRoot::HeadDrop(mesh_addr),
|
||||
Replicated,
|
||||
));
|
||||
|
||||
Ok(())
|
||||
@@ -101,9 +106,15 @@ fn on_collect_head(
|
||||
let is_secret = query_secret.contains(collectable);
|
||||
|
||||
if is_secret {
|
||||
commands.trigger(PlaySound::SecretHeadCollect);
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: PlaySound::SecretHeadCollect,
|
||||
});
|
||||
} else {
|
||||
commands.trigger(PlaySound::HeadCollect);
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: PlaySound::HeadCollect,
|
||||
});
|
||||
}
|
||||
|
||||
commands.entity(collider).trigger(|entity| HeadCollected {
|
||||
|
||||
@@ -4,9 +4,7 @@ use bevy_common_assets::ron::RonAssetPlugin;
|
||||
use bevy_sprite3d::Sprite3dPlugin;
|
||||
use bevy_trenchbroom::prelude::*;
|
||||
use bevy_trenchbroom_avian::AvianPhysicsBackend;
|
||||
use lightyear::prelude::server::ServerPlugins;
|
||||
use shared::{DebugVisuals, GameState, heads_database::HeadDatabaseAsset};
|
||||
use std::time::Duration;
|
||||
|
||||
mod backpack;
|
||||
mod config;
|
||||
@@ -74,15 +72,7 @@ fn main() {
|
||||
..default()
|
||||
}));
|
||||
|
||||
app.add_plugins(
|
||||
PhysicsPlugins::default()
|
||||
.build()
|
||||
// FrameInterpolation handles interpolating Position and Rotation
|
||||
.disable::<PhysicsInterpolationPlugin>(),
|
||||
);
|
||||
app.add_plugins(ServerPlugins {
|
||||
tick_duration: Duration::from_secs_f32(1.0 / 60.0),
|
||||
});
|
||||
app.add_plugins(PhysicsPlugins::default());
|
||||
app.add_plugins(Sprite3dPlugin);
|
||||
app.add_plugins(TrenchBroomPlugins(
|
||||
TrenchBroomConfig::new("hedz").icon(None),
|
||||
@@ -112,9 +102,9 @@ fn main() {
|
||||
app.add_plugins(shared::npc::plugin);
|
||||
app.add_plugins(shared::platforms::plugin);
|
||||
app.add_plugins(shared::player::plugin);
|
||||
app.add_plugins(shared::protocol::plugin);
|
||||
app.add_plugins(shared::steam::plugin);
|
||||
app.add_plugins(shared::tb_entities::plugin);
|
||||
app.add_plugins(shared::tick::plugin);
|
||||
app.add_plugins(shared::utils::auto_rotate::plugin);
|
||||
app.add_plugins(shared::utils::billboards::plugin);
|
||||
app.add_plugins(shared::utils::explosions::plugin);
|
||||
@@ -124,12 +114,16 @@ fn main() {
|
||||
app.add_plugins(shared::utils::plugin);
|
||||
app.add_plugins(shared::water::plugin);
|
||||
|
||||
// Networking
|
||||
// The client/server plugin must go before the protocol, or else `ProtocolHasher` will not be available.
|
||||
app.add_plugins(server::plugin);
|
||||
app.add_plugins(shared::protocol::plugin);
|
||||
|
||||
app.add_plugins(backpack::plugin);
|
||||
app.add_plugins(config::plugin);
|
||||
app.add_plugins(head_drop::plugin);
|
||||
app.add_plugins(platforms::plugin);
|
||||
app.add_plugins(player::plugin);
|
||||
app.add_plugins(server::plugin);
|
||||
app.add_plugins(tb_entities::plugin);
|
||||
app.add_plugins(utils::plugin);
|
||||
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
use bevy::prelude::*;
|
||||
use bevy_trenchbroom::prelude::Target;
|
||||
use lightyear::prelude::{NetworkTarget, PredictionTarget};
|
||||
use shared::{
|
||||
GameState,
|
||||
platforms::ActivePlatform,
|
||||
@@ -34,8 +33,6 @@ fn init(
|
||||
target,
|
||||
};
|
||||
|
||||
commands
|
||||
.entity(e)
|
||||
.insert((platform, PredictionTarget::to_clients(NetworkTarget::All)));
|
||||
commands.entity(e).insert(platform);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
use bevy::prelude::*;
|
||||
use lightyear::prelude::{input::native::ActionState, *};
|
||||
use bevy_replicon::prelude::{Replicated, SendMode, ServerTriggerExt, ToClients};
|
||||
use shared::{
|
||||
backpack::{Backpack, backpack_ui::BackpackUiState},
|
||||
camera::{CameraArmRotation, CameraTarget},
|
||||
cash::CashResource,
|
||||
character::AnimatedCharacter,
|
||||
control::{ControlState, controller_common::PlayerCharacterController},
|
||||
control::{Inputs, controller_common::PlayerCharacterController},
|
||||
global_observer,
|
||||
head::ActiveHead,
|
||||
head_drop::HeadDrops,
|
||||
@@ -14,9 +14,7 @@ use shared::{
|
||||
hitpoints::{Hitpoints, Kill},
|
||||
npc::SpawnCharacter,
|
||||
player::{Player, PlayerBodyMesh},
|
||||
protocol::{
|
||||
PlaySound, PlayerId, channels::UnorderedReliableChannel, events::ClientHeadChanged,
|
||||
},
|
||||
protocol::{PlaySound, PlayerId, events::ClientHeadChanged},
|
||||
tb_entities::SpawnPoint,
|
||||
};
|
||||
|
||||
@@ -26,7 +24,7 @@ pub fn plugin(app: &mut App) {
|
||||
|
||||
pub fn spawn(
|
||||
mut commands: Commands,
|
||||
owner: Entity,
|
||||
_owner: Entity,
|
||||
query: Query<&Transform, With<SpawnPoint>>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
) -> Option<Entity> {
|
||||
@@ -34,48 +32,53 @@ pub fn spawn(
|
||||
|
||||
let transform = Transform::from_translation(spawn.translation + Vec3::new(0., 3., 0.));
|
||||
|
||||
let mut player = commands.spawn((
|
||||
(
|
||||
Name::from("player"),
|
||||
Player,
|
||||
ActiveHead(0),
|
||||
ActiveHeads::new([
|
||||
Some(HeadState::new(0, heads_db.as_ref())),
|
||||
Some(HeadState::new(3, heads_db.as_ref())),
|
||||
Some(HeadState::new(6, heads_db.as_ref())),
|
||||
Some(HeadState::new(10, heads_db.as_ref())),
|
||||
Some(HeadState::new(9, heads_db.as_ref())),
|
||||
]),
|
||||
Hitpoints::new(100),
|
||||
CashResource::default(),
|
||||
CameraTarget,
|
||||
transform,
|
||||
Visibility::default(),
|
||||
PlayerCharacterController,
|
||||
PlayerId { id: 0 },
|
||||
),
|
||||
ActionState::<ControlState>::default(),
|
||||
Backpack::default(),
|
||||
BackpackUiState::default(),
|
||||
UiActiveHeads::default(),
|
||||
Replicate::to_clients(NetworkTarget::All),
|
||||
PredictionTarget::to_clients(NetworkTarget::All),
|
||||
ControlledBy {
|
||||
owner,
|
||||
lifetime: Lifetime::SessionBased,
|
||||
},
|
||||
children![(
|
||||
Name::new("player-rig"),
|
||||
PlayerBodyMesh,
|
||||
CameraArmRotation,
|
||||
children![AnimatedCharacter::new(0)],
|
||||
)],
|
||||
));
|
||||
player.observe(on_kill);
|
||||
let id = commands
|
||||
.spawn((
|
||||
(
|
||||
Name::from("player"),
|
||||
Player,
|
||||
ActiveHead(0),
|
||||
ActiveHeads::new([
|
||||
Some(HeadState::new(0, heads_db.as_ref())),
|
||||
Some(HeadState::new(3, heads_db.as_ref())),
|
||||
Some(HeadState::new(6, heads_db.as_ref())),
|
||||
Some(HeadState::new(10, heads_db.as_ref())),
|
||||
Some(HeadState::new(9, heads_db.as_ref())),
|
||||
]),
|
||||
Hitpoints::new(100),
|
||||
CashResource::default(),
|
||||
CameraTarget,
|
||||
transform,
|
||||
Visibility::default(),
|
||||
PlayerCharacterController,
|
||||
PlayerId { id: 0 },
|
||||
),
|
||||
Backpack::default(),
|
||||
BackpackUiState::default(),
|
||||
UiActiveHeads::default(),
|
||||
Inputs::default(),
|
||||
Replicated,
|
||||
))
|
||||
.with_children(|c| {
|
||||
c.spawn((
|
||||
Name::new("player-rig"),
|
||||
PlayerBodyMesh,
|
||||
CameraArmRotation,
|
||||
Replicated,
|
||||
))
|
||||
.with_child((
|
||||
Name::new("player-animated-character"),
|
||||
AnimatedCharacter::new(0),
|
||||
Replicated,
|
||||
));
|
||||
})
|
||||
.observe(on_kill)
|
||||
.id();
|
||||
|
||||
let id = player.id();
|
||||
|
||||
commands.trigger(PlaySound::Head("angry demonstrator".to_string()));
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: PlaySound::Head("angry demonstrator".to_string()),
|
||||
});
|
||||
commands.trigger(SpawnCharacter(transform.translation));
|
||||
|
||||
Some(id)
|
||||
@@ -103,7 +106,6 @@ fn on_update_head_mesh(
|
||||
trigger: On<HeadChanged>,
|
||||
mut commands: Commands,
|
||||
mesh_children: Single<&Children, With<PlayerBodyMesh>>,
|
||||
mut sender: Single<&mut EventSender<ClientHeadChanged>>,
|
||||
animated_characters: Query<&AnimatedCharacter>,
|
||||
mut player: Single<&mut ActiveHead, With<Player>>,
|
||||
) -> Result {
|
||||
@@ -118,7 +120,10 @@ fn on_update_head_mesh(
|
||||
.entity(animated_char)
|
||||
.insert(AnimatedCharacter::new(trigger.0));
|
||||
|
||||
sender.trigger::<UnorderedReliableChannel>(ClientHeadChanged(trigger.0 as u64));
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: ClientHeadChanged(trigger.0 as u64),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -1,91 +1,58 @@
|
||||
use crate::config::ServerConfig;
|
||||
use bevy::{ecs::component::Components, prelude::*};
|
||||
use lightyear::{
|
||||
connection::client::PeerMetadata,
|
||||
link::LinkConditioner,
|
||||
use bevy::prelude::*;
|
||||
use bevy_replicon::{
|
||||
RepliconPlugins,
|
||||
prelude::{
|
||||
server::{ClientOf, NetcodeConfig, NetcodeServer, ServerUdpIo, Started},
|
||||
*,
|
||||
ClientId, ConnectedClient, FromClient, RepliconChannels, SendMode, ServerState,
|
||||
ServerTriggerExt, ToClients,
|
||||
},
|
||||
server::AuthorizedClient,
|
||||
};
|
||||
use bevy_replicon_renet::{
|
||||
RenetChannelsExt, RepliconRenetPlugins,
|
||||
netcode::{NetcodeServerTransport, ServerAuthentication},
|
||||
renet::{ConnectionConfig, RenetServer},
|
||||
};
|
||||
use shared::{
|
||||
GameState, global_observer,
|
||||
heads_database::HeadsDatabase,
|
||||
protocol::{
|
||||
ClientEnteredPlaying, channels::UnorderedReliableChannel, messages::AssignClientPlayer,
|
||||
},
|
||||
player::ClientPlayerId,
|
||||
protocol::{ClientEnteredPlaying, PlayerId, SetGameTick, messages::AssignClientPlayer},
|
||||
tb_entities::SpawnPoint,
|
||||
tick::GameTick,
|
||||
};
|
||||
use std::{
|
||||
net::{IpAddr, Ipv4Addr, SocketAddr},
|
||||
time::Duration,
|
||||
net::{Ipv4Addr, UdpSocket},
|
||||
time::SystemTime,
|
||||
};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(
|
||||
OnEnter(GameState::Playing),
|
||||
(start_server, setup_timeout_timer),
|
||||
);
|
||||
app.add_systems(
|
||||
Update,
|
||||
(notify_started, run_timeout).run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
app.add_plugins((RepliconPlugins, RepliconRenetPlugins));
|
||||
|
||||
global_observer!(app, handle_new_client);
|
||||
global_observer!(app, on_client_connected);
|
||||
global_observer!(app, on_client_playing);
|
||||
global_observer!(app, close_on_disconnect);
|
||||
app.add_systems(OnEnter(GameState::Playing), setup_timeout_timer);
|
||||
app.add_systems(OnEnter(ServerState::Running), notify_started);
|
||||
app.add_systems(Update, run_timeout.run_if(in_state(GameState::Playing)));
|
||||
|
||||
app.add_systems(OnEnter(GameState::Playing), open_renet_server);
|
||||
|
||||
// Replicon
|
||||
global_observer!(app, on_connected);
|
||||
global_observer!(app, on_disconnected);
|
||||
|
||||
// Server logic
|
||||
global_observer!(app, cancel_timeout);
|
||||
}
|
||||
|
||||
#[derive(Component)]
|
||||
struct ClientPlayerId(u8);
|
||||
|
||||
fn handle_new_client(
|
||||
trigger: On<Add, Linked>,
|
||||
mut commands: Commands,
|
||||
id: Query<&PeerAddr>,
|
||||
) -> Result {
|
||||
let Ok(id) = id.get(trigger.event().entity) else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
info!("Client connected on IP: {}", id.ip());
|
||||
|
||||
let conditioner = LinkConditioner::new(LinkConditionerConfig {
|
||||
incoming_latency: Duration::from_millis(10),
|
||||
incoming_jitter: Duration::from_millis(0),
|
||||
incoming_loss: 0.0,
|
||||
});
|
||||
|
||||
commands.entity(trigger.event().entity).insert((
|
||||
ReplicationSender::default(),
|
||||
Link::new(Some(conditioner)),
|
||||
ClientPlayerId(0),
|
||||
));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_client_connected(
|
||||
trigger: On<Add, ClientOf>,
|
||||
mut assign_player: Query<(&ClientPlayerId, &mut MessageSender<AssignClientPlayer>)>,
|
||||
) -> Result {
|
||||
// `Linked` happens before the `ClientOf` and `MessageSender` components are added, so the server can't
|
||||
// send the client player id until now.
|
||||
let (id, mut sender) = assign_player.get_mut(trigger.event().entity)?;
|
||||
sender.send::<UnorderedReliableChannel>(AssignClientPlayer(id.0));
|
||||
Ok(())
|
||||
global_observer!(app, on_client_playing);
|
||||
}
|
||||
|
||||
fn on_client_playing(
|
||||
trigger: On<RemoteEvent<ClientEnteredPlaying>>,
|
||||
trigger: On<FromClient<ClientEnteredPlaying>>,
|
||||
commands: Commands,
|
||||
query: Query<&Transform, With<SpawnPoint>>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
peers: Res<PeerMetadata>,
|
||||
) -> Result {
|
||||
let Some(&client) = peers.mapping.get(&trigger.from) else {
|
||||
info!("Client has entered playing gamestate");
|
||||
|
||||
let Some(client) = trigger.client_id.entity() else {
|
||||
return Ok(());
|
||||
};
|
||||
|
||||
@@ -94,51 +61,85 @@ fn on_client_playing(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn close_on_disconnect(
|
||||
_trigger: On<Remove, Connected>,
|
||||
//
|
||||
// Renet
|
||||
//
|
||||
|
||||
fn open_renet_server(
|
||||
mut commands: Commands,
|
||||
channels: Res<RepliconChannels>,
|
||||
) -> Result<(), BevyError> {
|
||||
let server_channels_config = channels.server_configs();
|
||||
let client_channels_config = channels.client_configs();
|
||||
|
||||
let server = RenetServer::new(ConnectionConfig {
|
||||
server_channels_config,
|
||||
client_channels_config,
|
||||
..Default::default()
|
||||
});
|
||||
|
||||
let current_time = SystemTime::now().duration_since(SystemTime::UNIX_EPOCH)?;
|
||||
let port = 31111;
|
||||
let socket = UdpSocket::bind((Ipv4Addr::UNSPECIFIED, port))?;
|
||||
let server_config = bevy_replicon_renet::netcode::ServerConfig {
|
||||
current_time,
|
||||
max_clients: 1,
|
||||
protocol_id: 0,
|
||||
authentication: ServerAuthentication::Unsecure,
|
||||
public_addresses: Default::default(),
|
||||
};
|
||||
let transport = NetcodeServerTransport::new(server_config, socket)?;
|
||||
|
||||
commands.insert_resource(server);
|
||||
commands.insert_resource(transport);
|
||||
|
||||
info!("Hosting a server on port {port}");
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
//
|
||||
// server logic
|
||||
//
|
||||
|
||||
fn on_connected(
|
||||
trigger: On<Add, AuthorizedClient>,
|
||||
game_tick: Res<GameTick>,
|
||||
mut commands: Commands,
|
||||
mut assign_id: MessageWriter<ToClients<AssignClientPlayer>>,
|
||||
) {
|
||||
let client = trigger.event_target();
|
||||
info!("{client} connected to server!");
|
||||
|
||||
let id = ClientPlayerId(PlayerId { id: 0 });
|
||||
commands.entity(client).insert(id);
|
||||
|
||||
assign_id.write(ToClients {
|
||||
mode: SendMode::Direct(ClientId::Client(trigger.entity)),
|
||||
message: AssignClientPlayer(id.0),
|
||||
});
|
||||
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Direct(ClientId::Client(trigger.entity)),
|
||||
message: SetGameTick(game_tick.0),
|
||||
});
|
||||
}
|
||||
|
||||
fn on_disconnected(
|
||||
on: On<Remove, ConnectedClient>,
|
||||
config: Res<ServerConfig>,
|
||||
mut writer: MessageWriter<AppExit>,
|
||||
) {
|
||||
info!("client {} disconnected", on.entity);
|
||||
|
||||
if config.close_on_client_disconnect {
|
||||
info!("client disconnected, exiting");
|
||||
writer.write(AppExit::Success);
|
||||
}
|
||||
}
|
||||
|
||||
fn start_server(mut commands: Commands) -> Result {
|
||||
let server_addr = SocketAddr::new(IpAddr::V4(Ipv4Addr::new(127, 0, 0, 1)), 25565);
|
||||
|
||||
let conditioner = LinkConditioner::new(LinkConditionerConfig {
|
||||
incoming_latency: Duration::from_millis(10),
|
||||
incoming_jitter: Duration::from_millis(0),
|
||||
incoming_loss: 0.0,
|
||||
});
|
||||
let mut commands = commands.spawn((
|
||||
Name::from("Server"),
|
||||
LocalAddr(server_addr),
|
||||
ServerUdpIo::default(),
|
||||
NetcodeServer::new(NetcodeConfig {
|
||||
client_timeout_secs: 3,
|
||||
..Default::default()
|
||||
}),
|
||||
Link::new(Some(conditioner)),
|
||||
));
|
||||
commands
|
||||
.observe(|on: On<Add>, components: &Components| {
|
||||
for &comp in on.trigger().components {
|
||||
info!("Added {} to server", components.get_name(comp).unwrap());
|
||||
}
|
||||
})
|
||||
.trigger(|entity| server::Start { entity });
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn notify_started(started: Query<&Started>, mut notified: Local<bool>) {
|
||||
if !*notified && !started.is_empty() {
|
||||
println!("hedz.server_started");
|
||||
*notified = true;
|
||||
}
|
||||
fn notify_started() {
|
||||
println!("hedz.server_started");
|
||||
}
|
||||
|
||||
#[derive(Resource)]
|
||||
@@ -161,7 +162,7 @@ fn run_timeout(
|
||||
}
|
||||
}
|
||||
|
||||
fn cancel_timeout(_trigger: On<Add, Connected>, mut timer: ResMut<TimeoutTimer>) {
|
||||
fn cancel_timeout(_trigger: On<Add, ConnectedClient>, mut timer: ResMut<TimeoutTimer>) {
|
||||
info!("client connected, cancelling timeout");
|
||||
timer.0 = f32::INFINITY;
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
use bevy::prelude::*;
|
||||
use lightyear::prelude::{Connected, MessageSender};
|
||||
use bevy_replicon::prelude::{ClientId, ConnectedClient, SendMode, ToClients};
|
||||
use shared::{
|
||||
GameState, global_observer,
|
||||
protocol::{TbMapEntityId, channels::UnorderedReliableChannel, messages::DespawnTbMapEntity},
|
||||
protocol::{TbMapEntityId, messages::DespawnTbMapEntity},
|
||||
};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
@@ -32,12 +32,14 @@ fn add_despawned_entities_to_cache(
|
||||
pub struct DespawnedTbEntityCache(pub Vec<u64>);
|
||||
|
||||
fn send_new_client_despawned_cache(
|
||||
trigger: On<Add, Connected>,
|
||||
on: On<Add, ConnectedClient>,
|
||||
cache: Res<DespawnedTbEntityCache>,
|
||||
mut send: Query<&mut MessageSender<DespawnTbMapEntity>>,
|
||||
mut send: MessageWriter<ToClients<DespawnTbMapEntity>>,
|
||||
) {
|
||||
let mut send = send.get_mut(trigger.event().entity).unwrap();
|
||||
for &id in cache.0.iter() {
|
||||
send.send::<UnorderedReliableChannel>(DespawnTbMapEntity(id));
|
||||
send.write(ToClients {
|
||||
mode: SendMode::Direct(ClientId::Client(on.entity)),
|
||||
message: DespawnTbMapEntity(id),
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -4,8 +4,8 @@ version = "0.1.0"
|
||||
edition = "2024"
|
||||
|
||||
[features]
|
||||
client = ["lightyear/client"]
|
||||
server = ["lightyear/server"]
|
||||
client = []
|
||||
server = []
|
||||
dbg = []
|
||||
|
||||
[dependencies]
|
||||
@@ -17,11 +17,10 @@ bevy_asset_loader = { workspace = true }
|
||||
bevy_ballistic = { workspace = true }
|
||||
bevy_common_assets = { workspace = true }
|
||||
bevy_debug_log = { workspace = true }
|
||||
bevy_replicon = { workspace = true }
|
||||
bevy_sprite3d = { workspace = true }
|
||||
bevy_trenchbroom = { workspace = true }
|
||||
happy_feet = { workspace = true }
|
||||
lightyear = { workspace = true }
|
||||
lightyear_serde = { workspace = true }
|
||||
nil = { workspace = true }
|
||||
rand = { workspace = true }
|
||||
ron = { workspace = true }
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::{
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::{light::NotShadowCaster, prelude::*};
|
||||
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients};
|
||||
use bevy_sprite3d::Sprite3d;
|
||||
|
||||
#[derive(Component)]
|
||||
@@ -44,7 +45,10 @@ fn on_trigger_arrow(
|
||||
) {
|
||||
let state = trigger.0;
|
||||
|
||||
commands.trigger(PlaySound::Crossbow);
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: PlaySound::Crossbow,
|
||||
});
|
||||
|
||||
let rotation = if let Some(target) = state.target {
|
||||
let t = query_transform
|
||||
|
||||
@@ -6,12 +6,11 @@ use crate::{
|
||||
physics_layers::GameLayer,
|
||||
protocol::GltfSceneRoot,
|
||||
tb_entities::EnemySpawn,
|
||||
utils::{auto_rotate::AutoRotation, commands::CommandExt, global_observer},
|
||||
utils::{auto_rotate::AutoRotation, global_observer},
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::prelude::*;
|
||||
#[cfg(feature = "server")]
|
||||
use lightyear::prelude::{NetworkTarget, Replicate};
|
||||
use bevy_replicon::prelude::{Replicated, SendMode, ServerTriggerExt, ToClients};
|
||||
use std::f32::consts::PI;
|
||||
|
||||
const MAX_SHOT_AGES: f32 = 15.;
|
||||
@@ -60,7 +59,7 @@ fn on_trigger_missile(
|
||||
let mut transform = Transform::from_translation(state.pos).with_rotation(rotation);
|
||||
transform.translation += transform.forward().as_vec3() * 2.0;
|
||||
|
||||
let mut _projectile = commands.spawn((
|
||||
commands.spawn((
|
||||
Name::new("projectile-missile"),
|
||||
CurverProjectile {
|
||||
time: time.elapsed_secs(),
|
||||
@@ -75,15 +74,13 @@ fn on_trigger_missile(
|
||||
CollisionEventsEnabled,
|
||||
Visibility::default(),
|
||||
transform,
|
||||
Replicated,
|
||||
children![(
|
||||
Transform::from_rotation(Quat::from_rotation_x(PI / 2.).inverse()),
|
||||
AutoRotation(Quat::from_rotation_x(0.4) * Quat::from_rotation_z(0.3)),
|
||||
GltfSceneRoot::Projectile(head.projectile.clone()),
|
||||
),],
|
||||
));
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
_projectile.insert(Replicate::to_clients(NetworkTarget::All));
|
||||
}
|
||||
|
||||
fn enemy_hit(
|
||||
@@ -169,10 +166,13 @@ fn shot_collision(
|
||||
continue;
|
||||
}
|
||||
|
||||
commands.trigger_server(BuildExplosionSprite {
|
||||
pos: shot_pos,
|
||||
pixels_per_meter: 128.,
|
||||
time: 0.01,
|
||||
commands.server_trigger(ToClients {
|
||||
message: BuildExplosionSprite {
|
||||
pos: shot_pos,
|
||||
pixels_per_meter: 128.,
|
||||
time: 0.01,
|
||||
},
|
||||
mode: SendMode::Broadcast,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
@@ -6,6 +6,7 @@ use crate::{
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::{light::NotShadowCaster, prelude::*};
|
||||
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients};
|
||||
use bevy_sprite3d::Sprite3d;
|
||||
|
||||
#[derive(Component)]
|
||||
@@ -91,7 +92,10 @@ fn on_trigger_gun(
|
||||
) {
|
||||
let state = trigger.0;
|
||||
|
||||
commands.trigger(PlaySound::Gun);
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: PlaySound::Gun,
|
||||
});
|
||||
|
||||
let rotation = if let Some(t) = state
|
||||
.target
|
||||
|
||||
@@ -9,8 +9,7 @@ use crate::{
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::prelude::*;
|
||||
#[cfg(feature = "server")]
|
||||
use lightyear::prelude::{NetworkTarget, Replicate};
|
||||
use bevy_replicon::prelude::Replicated;
|
||||
use std::f32::consts::PI;
|
||||
|
||||
const MAX_SHOT_AGES: f32 = 15.;
|
||||
@@ -61,7 +60,7 @@ fn on_trigger_missile(
|
||||
let mut transform = Transform::from_translation(state.pos).with_rotation(rotation);
|
||||
transform.translation += transform.forward().as_vec3() * 2.0;
|
||||
|
||||
let mut _projectile = commands.spawn((
|
||||
commands.spawn((
|
||||
Name::new("projectile-missile"),
|
||||
MissileProjectile {
|
||||
time: time.elapsed_secs(),
|
||||
@@ -73,13 +72,16 @@ fn on_trigger_missile(
|
||||
LayerMask(state.target_layer.to_bits() | GameLayer::Level.to_bits()),
|
||||
),
|
||||
Sensor,
|
||||
RigidBody::Kinematic,
|
||||
CollisionEventsEnabled,
|
||||
Visibility::default(),
|
||||
transform,
|
||||
Replicated,
|
||||
children![
|
||||
(
|
||||
Transform::from_rotation(Quat::from_rotation_x(PI / 2.).inverse()),
|
||||
GltfSceneRoot::Projectile("missile".to_string()),
|
||||
Replicated,
|
||||
),
|
||||
(
|
||||
Trail::new(
|
||||
@@ -99,8 +101,6 @@ fn on_trigger_missile(
|
||||
)
|
||||
],
|
||||
));
|
||||
#[cfg(feature = "server")]
|
||||
_projectile.insert(Replicate::to_clients(NetworkTarget::All));
|
||||
}
|
||||
|
||||
fn update(mut query: Query<&mut Transform, With<MissileProjectile>>) {
|
||||
|
||||
@@ -10,21 +10,25 @@ use crate::{
|
||||
loading_assets::GameAssets,
|
||||
physics_layers::GameLayer,
|
||||
protocol::PlaySound,
|
||||
utils::{billboards::Billboard, sprite_3d_animation::AnimationTimer},
|
||||
utils::{billboards::Billboard, explosions::Explosion, sprite_3d_animation::AnimationTimer},
|
||||
};
|
||||
#[cfg(feature = "server")]
|
||||
use crate::{
|
||||
aim::AimTarget, character::CharacterHierarchy, control::ControlState, head::ActiveHead,
|
||||
heads::ActiveHeads, heads_database::HeadsDatabase, player::Player,
|
||||
utils::explosions::Explosion,
|
||||
aim::AimTarget,
|
||||
character::CharacterHierarchy,
|
||||
control::{ControlState, Inputs},
|
||||
head::ActiveHead,
|
||||
heads::ActiveHeads,
|
||||
heads_database::HeadsDatabase,
|
||||
player::Player,
|
||||
protocol::ClientToController,
|
||||
};
|
||||
use bevy::{light::NotShadowCaster, prelude::*};
|
||||
#[cfg(feature = "server")]
|
||||
use bevy_replicon::prelude::FromClient;
|
||||
use bevy_replicon::prelude::{ClientState, SendMode, ServerTriggerExt, ToClients};
|
||||
use bevy_sprite3d::Sprite3d;
|
||||
pub use healing::Healing;
|
||||
#[cfg(feature = "server")]
|
||||
use healing::HealingStateChanged;
|
||||
#[cfg(feature = "server")]
|
||||
use lightyear::prelude::input::native::ActionState;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Debug, Copy, Clone, PartialEq, Reflect, Default, Serialize, Deserialize)]
|
||||
@@ -143,16 +147,16 @@ pub fn plugin(app: &mut App) {
|
||||
.chain()
|
||||
.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
#[cfg(feature = "server")]
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
explode_projectiles.in_set(ExplodingProjectileSet::Explode),
|
||||
explode_projectiles
|
||||
.run_if(in_state(ClientState::Disconnected))
|
||||
.in_set(ExplodingProjectileSet::Explode),
|
||||
);
|
||||
|
||||
global_observer!(app, build_explosion_sprite);
|
||||
}
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
fn explode_projectiles(mut commands: Commands, query: Query<(Entity, &ExplodingProjectile)>) {
|
||||
for (shot_entity, projectile) in query.iter() {
|
||||
if let Ok(mut entity) = commands.get_entity(shot_entity) {
|
||||
@@ -161,7 +165,10 @@ fn explode_projectiles(mut commands: Commands, query: Query<(Entity, &ExplodingP
|
||||
continue;
|
||||
}
|
||||
|
||||
commands.trigger(projectile.sound.clone());
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: projectile.sound.clone(),
|
||||
});
|
||||
|
||||
commands.trigger(Explosion {
|
||||
damage: projectile.damage,
|
||||
@@ -172,12 +179,13 @@ fn explode_projectiles(mut commands: Commands, query: Query<(Entity, &ExplodingP
|
||||
|
||||
//TODO: support different impact animations
|
||||
if projectile.animation {
|
||||
use crate::utils::commands::CommandExt;
|
||||
|
||||
commands.trigger_server(BuildExplosionSprite {
|
||||
pos: projectile.position,
|
||||
pixels_per_meter: projectile.anim_pixels_per_meter,
|
||||
time: projectile.anim_time,
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: BuildExplosionSprite {
|
||||
pos: projectile.position,
|
||||
pixels_per_meter: projectile.anim_pixels_per_meter,
|
||||
time: projectile.anim_time,
|
||||
},
|
||||
});
|
||||
}
|
||||
}
|
||||
@@ -186,11 +194,15 @@ fn explode_projectiles(mut commands: Commands, query: Query<(Entity, &ExplodingP
|
||||
#[cfg(feature = "server")]
|
||||
fn on_trigger_state(
|
||||
mut res: ResMut<TriggerStateRes>,
|
||||
player: Query<(&ActiveHead, &ActionState<ControlState>), With<Player>>,
|
||||
players: Query<&ActiveHead, With<Player>>,
|
||||
clients: ClientToController,
|
||||
mut controls: MessageReader<FromClient<ControlState>>,
|
||||
headdb: Res<HeadsDatabase>,
|
||||
time: Res<Time>,
|
||||
) {
|
||||
for (player_head, controls) in player.iter() {
|
||||
for controls in controls.read() {
|
||||
let player = clients.get_controller(controls.client_id);
|
||||
let player_head = players.get(player).unwrap();
|
||||
res.active = controls.trigger;
|
||||
if controls.just_triggered {
|
||||
let head_stats = headdb.head_stats(player_head.0);
|
||||
@@ -203,7 +215,7 @@ fn on_trigger_state(
|
||||
fn update(
|
||||
mut res: ResMut<TriggerStateRes>,
|
||||
mut commands: Commands,
|
||||
player_query: Query<(Entity, &AimTarget, &ActionState<ControlState>), With<Player>>,
|
||||
player_query: Query<(Entity, &AimTarget, &Inputs), With<Player>>,
|
||||
mut active_heads: Single<&mut ActiveHeads, With<Player>>,
|
||||
heads_db: Res<HeadsDatabase>,
|
||||
time: Res<Time>,
|
||||
@@ -215,11 +227,14 @@ fn update(
|
||||
};
|
||||
|
||||
if !state.has_ammo() {
|
||||
commands.trigger(PlaySound::Invalid);
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: PlaySound::Invalid,
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
let Some((player, target, actions)) = player_query.iter().next() else {
|
||||
let Some((player, target, inputs)) = player_query.iter().next() else {
|
||||
return;
|
||||
};
|
||||
|
||||
@@ -241,7 +256,7 @@ fn update(
|
||||
res.next_trigger_timestamp = time.elapsed_secs() + (1. / head.aps);
|
||||
|
||||
let trigger_state = TriggerData {
|
||||
dir: actions.look_dir,
|
||||
dir: Dir3::try_from(inputs.look_dir).unwrap_or(Dir3::NEG_Z),
|
||||
pos: projectile_origin,
|
||||
target: target.0,
|
||||
target_layer: GameLayer::Npc,
|
||||
@@ -281,11 +296,15 @@ fn update_heal_ability(
|
||||
|
||||
use crate::abilities::healing::HealingState;
|
||||
if res.active {
|
||||
use crate::abilities::healing::HealingStateChanged;
|
||||
|
||||
commands.trigger(HealingStateChanged {
|
||||
state: HealingState::Started,
|
||||
entity: player,
|
||||
});
|
||||
} else {
|
||||
use crate::abilities::healing::HealingStateChanged;
|
||||
|
||||
commands.trigger(HealingStateChanged {
|
||||
state: HealingState::Stopped,
|
||||
entity: player,
|
||||
|
||||
@@ -9,8 +9,7 @@ use crate::{
|
||||
use avian3d::prelude::*;
|
||||
use bevy::prelude::*;
|
||||
use bevy_ballistic::launch_velocity;
|
||||
#[cfg(feature = "server")]
|
||||
use lightyear::prelude::{NetworkTarget, Replicate};
|
||||
use bevy_replicon::prelude::{Replicated, SendMode, ServerTriggerExt, ToClients};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Component, Serialize, Deserialize, PartialEq)]
|
||||
@@ -36,7 +35,10 @@ fn on_trigger_thrown(
|
||||
) {
|
||||
let state = trigger.event().0;
|
||||
|
||||
commands.trigger(PlaySound::Throw);
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: PlaySound::Throw,
|
||||
});
|
||||
|
||||
const SPEED: f32 = 35.;
|
||||
|
||||
@@ -57,32 +59,32 @@ fn on_trigger_thrown(
|
||||
//TODO: projectile db?
|
||||
let explosion_animation = !matches!(state.head, 8 | 16);
|
||||
|
||||
let mut projectile = commands.spawn((
|
||||
Transform::from_translation(pos),
|
||||
Name::new("projectile-thrown"),
|
||||
ThrownProjectile {
|
||||
impact_animation: explosion_animation,
|
||||
damage: head.damage,
|
||||
},
|
||||
Collider::sphere(0.4),
|
||||
CollisionLayers::new(
|
||||
LayerMask(GameLayer::Projectile.to_bits()),
|
||||
LayerMask(state.target_layer.to_bits() | GameLayer::Level.to_bits()),
|
||||
),
|
||||
RigidBody::Dynamic,
|
||||
CollisionEventsEnabled,
|
||||
Mass(0.01),
|
||||
LinearVelocity(vel),
|
||||
Visibility::default(),
|
||||
Sensor,
|
||||
));
|
||||
projectile.with_child((
|
||||
AutoRotation(Quat::from_rotation_x(0.4) * Quat::from_rotation_z(0.3)),
|
||||
GltfSceneRoot::Projectile(head.projectile.clone()),
|
||||
));
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
projectile.insert(Replicate::to_clients(NetworkTarget::All));
|
||||
commands
|
||||
.spawn((
|
||||
Transform::from_translation(pos),
|
||||
Name::new("projectile-thrown"),
|
||||
ThrownProjectile {
|
||||
impact_animation: explosion_animation,
|
||||
damage: head.damage,
|
||||
},
|
||||
Collider::sphere(0.4),
|
||||
CollisionLayers::new(
|
||||
LayerMask(GameLayer::Projectile.to_bits()),
|
||||
LayerMask(state.target_layer.to_bits() | GameLayer::Level.to_bits()),
|
||||
),
|
||||
RigidBody::Dynamic,
|
||||
CollisionEventsEnabled,
|
||||
Mass(0.01),
|
||||
LinearVelocity(vel),
|
||||
Visibility::default(),
|
||||
Sensor,
|
||||
Replicated,
|
||||
))
|
||||
.with_child((
|
||||
AutoRotation(Quat::from_rotation_x(0.4) * Quat::from_rotation_z(0.3)),
|
||||
GltfSceneRoot::Projectile(head.projectile.clone()),
|
||||
Replicated,
|
||||
));
|
||||
}
|
||||
|
||||
fn shot_collision(
|
||||
|
||||
@@ -2,12 +2,11 @@ mod marker;
|
||||
mod target_ui;
|
||||
|
||||
use crate::{
|
||||
GameState, control::ControlState, head::ActiveHead, heads_database::HeadsDatabase,
|
||||
GameState, control::Inputs, head::ActiveHead, heads_database::HeadsDatabase,
|
||||
hitpoints::Hitpoints, physics_layers::GameLayer, player::Player, tb_entities::EnemySpawn,
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::prelude::*;
|
||||
use lightyear::prelude::input::native::ActionState;
|
||||
use marker::MarkerEvent;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::f32::consts::PI;
|
||||
@@ -71,19 +70,13 @@ fn update_player_aim(
|
||||
mut commands: Commands,
|
||||
potential_targets: Query<(Entity, &Transform), With<Hitpoints>>,
|
||||
mut player_aim: Query<
|
||||
(
|
||||
Entity,
|
||||
&AimState,
|
||||
&mut AimTarget,
|
||||
&GlobalTransform,
|
||||
&ActionState<ControlState>,
|
||||
),
|
||||
(Entity, &AimState, &mut AimTarget, &GlobalTransform, &Inputs),
|
||||
With<Player>,
|
||||
>,
|
||||
spatial_query: SpatialQuery,
|
||||
) {
|
||||
for (player, state, mut aim_target, global_tf, actions) in player_aim.iter_mut() {
|
||||
let (player_pos, player_forward) = (global_tf.translation(), actions.look_dir);
|
||||
for (player, state, mut aim_target, global_tf, inputs) in player_aim.iter_mut() {
|
||||
let (player_pos, player_forward) = (global_tf.translation(), inputs.look_dir);
|
||||
|
||||
let mut new_target = None;
|
||||
let mut target_distance = f32::MAX;
|
||||
|
||||
@@ -1,6 +1,3 @@
|
||||
pub mod backpack_ui;
|
||||
pub mod ui_head_state;
|
||||
|
||||
use crate::{
|
||||
cash::CashCollectEvent, global_observer, head_drop::HeadCollected, heads::HeadState,
|
||||
heads_database::HeadsDatabase,
|
||||
@@ -9,6 +6,9 @@ use bevy::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
pub use ui_head_state::UiHeadState;
|
||||
|
||||
pub mod backpack_ui;
|
||||
pub mod ui_head_state;
|
||||
|
||||
#[derive(Component, Default, Reflect, Serialize, Deserialize, PartialEq)]
|
||||
#[reflect(Component)]
|
||||
pub struct Backpack {
|
||||
|
||||
@@ -39,6 +39,7 @@ struct CameraUi;
|
||||
#[derive(Component, Reflect, Debug)]
|
||||
#[reflect(Component)]
|
||||
pub struct MainCamera {
|
||||
pub enabled: bool,
|
||||
dir: Dir3,
|
||||
distance: f32,
|
||||
target_offset: Vec3,
|
||||
@@ -48,6 +49,7 @@ impl MainCamera {
|
||||
fn new(arm: Vec3) -> Self {
|
||||
let (dir, distance) = Dir3::new_and_length(arm).expect("invalid arm length");
|
||||
Self {
|
||||
enabled: true,
|
||||
dir,
|
||||
distance,
|
||||
target_offset: Vec3::new(0., 2., 0.),
|
||||
@@ -64,10 +66,8 @@ pub fn plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(GameState::Playing), startup);
|
||||
#[cfg(feature = "client")]
|
||||
app.add_systems(
|
||||
RunFixedMainLoop,
|
||||
(update, update_ui, update_look_around, rotate_view)
|
||||
.after(RunFixedMainLoopSystems::AfterFixedMainLoop)
|
||||
.run_if(in_state(GameState::Playing)),
|
||||
PostUpdate,
|
||||
(update, update_ui, update_look_around, rotate_view).run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -146,6 +146,10 @@ fn update(
|
||||
return;
|
||||
};
|
||||
|
||||
if !camera.enabled {
|
||||
return;
|
||||
}
|
||||
|
||||
let target = target_q.translation + camera.target_offset;
|
||||
|
||||
let direction = arm_tf.rotation * Quat::from_rotation_y(cam_rotation_input.x) * camera.dir;
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
use crate::{GameState, HEDZ_GREEN, loading_assets::UIAssets};
|
||||
#[cfg(feature = "server")]
|
||||
use crate::{global_observer, protocol::PlaySound};
|
||||
use avian3d::prelude::Rotation;
|
||||
use bevy::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
@@ -38,14 +39,21 @@ fn on_cash_collect(
|
||||
mut commands: Commands,
|
||||
mut cash: Single<&mut CashResource>,
|
||||
) {
|
||||
commands.trigger(PlaySound::CashCollect);
|
||||
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients};
|
||||
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: PlaySound::CashCollect,
|
||||
});
|
||||
|
||||
cash.cash += 100;
|
||||
}
|
||||
|
||||
fn rotate(time: Res<Time>, mut query: Query<&mut Transform, With<Cash>>) {
|
||||
for mut transform in query.iter_mut() {
|
||||
transform.rotate(Quat::from_rotation_y(time.delta_secs()));
|
||||
fn rotate(time: Res<Time>, mut query: Query<&mut Rotation, With<Cash>>) {
|
||||
for mut rotation in query.iter_mut() {
|
||||
rotation.0 = rotation
|
||||
.0
|
||||
.mul_quat(Quat::from_rotation_y(time.delta_secs()));
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
protocol::PlaySound,
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
use lightyear::prelude::input::native::ActionState;
|
||||
use bevy_replicon::prelude::{FromClient, SendMode, ServerTriggerExt, ToClients};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(FixedUpdate, on_heal_trigger);
|
||||
@@ -16,27 +16,33 @@ struct HealAction {
|
||||
}
|
||||
|
||||
fn on_heal_trigger(
|
||||
mut cmds: Commands,
|
||||
mut commands: Commands,
|
||||
mut cash: Single<&mut CashResource>,
|
||||
mut query: Query<(&mut Hitpoints, &ActionState<ControlState>), With<Player>>,
|
||||
mut query: Query<&mut Hitpoints, With<Player>>,
|
||||
mut controls: MessageReader<FromClient<ControlState>>,
|
||||
) {
|
||||
for (mut hp, controls) in query.iter_mut() {
|
||||
if !controls.cash_heal {
|
||||
continue;
|
||||
for controls in controls.read() {
|
||||
for mut hp in query.iter_mut() {
|
||||
if !controls.cash_heal {
|
||||
continue;
|
||||
}
|
||||
|
||||
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
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: PlaySound::CashHeal,
|
||||
});
|
||||
}
|
||||
|
||||
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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -9,7 +9,6 @@ use bevy::{
|
||||
animation::RepeatAnimation, ecs::system::SystemParam, platform::collections::HashMap,
|
||||
prelude::*, scene::SceneInstanceReady,
|
||||
};
|
||||
use lightyear::prelude::DisableReplicateHierarchy;
|
||||
use serde::{Deserialize, Serialize};
|
||||
use std::{f32::consts::PI, time::Duration};
|
||||
|
||||
@@ -112,11 +111,7 @@ fn spawn(
|
||||
commands.entity(entity).despawn_related::<Children>();
|
||||
|
||||
commands
|
||||
.spawn((
|
||||
SceneRoot(asset.scenes[0].clone()),
|
||||
DisableReplicateHierarchy,
|
||||
ChildOf(entity),
|
||||
))
|
||||
.spawn((SceneRoot(asset.scenes[0].clone()), ChildOf(entity)))
|
||||
.observe(find_marker_bones);
|
||||
|
||||
commands
|
||||
|
||||
@@ -3,7 +3,7 @@ use crate::{
|
||||
GameState,
|
||||
abilities::TriggerStateRes,
|
||||
animation::AnimationFlags,
|
||||
control::{ControlState, ControllerSettings, SelectedController},
|
||||
control::{ControllerSettings, Inputs, SelectedController},
|
||||
head::ActiveHead,
|
||||
heads_database::{HeadControls, HeadsDatabase},
|
||||
physics_layers::GameLayer,
|
||||
@@ -11,12 +11,12 @@ use crate::{
|
||||
};
|
||||
use avian3d::{math::*, prelude::*};
|
||||
use bevy::prelude::*;
|
||||
use bevy_replicon::client::confirm_history::ConfirmHistory;
|
||||
use happy_feet::prelude::{
|
||||
Character, CharacterDrag, CharacterGravity, CharacterMovement, CharacterPlugins,
|
||||
GroundFriction, Grounding, GroundingConfig, KinematicVelocity, MoveInput, SteppingBehaviour,
|
||||
SteppingConfig,
|
||||
};
|
||||
use lightyear::prelude::{Replicated, input::native::ActionState};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
@@ -47,12 +47,12 @@ pub fn plugin(app: &mut App) {
|
||||
fn set_animation_flags(
|
||||
trigger: Res<TriggerStateRes>,
|
||||
mut player: Query<
|
||||
(&Grounding, &mut AnimationFlags, &ActionState<ControlState>),
|
||||
(With<Player>, Without<Replicated>),
|
||||
(&Grounding, &mut AnimationFlags, &Inputs),
|
||||
(With<Player>, Without<ConfirmHistory>),
|
||||
>,
|
||||
) {
|
||||
for (grounding, mut flags, controls) in player.iter_mut() {
|
||||
let direction = controls.move_dir;
|
||||
for (grounding, mut flags, inputs) in player.iter_mut() {
|
||||
let direction = inputs.move_dir;
|
||||
let deadzone = 0.2;
|
||||
|
||||
if flags.any_direction {
|
||||
@@ -79,17 +79,10 @@ pub fn reset_upon_switch(
|
||||
mut event_controller_switch: MessageReader<ControllerSwitchEvent>,
|
||||
selected_controller: Res<SelectedController>,
|
||||
mut rig_transforms: Query<&mut Transform, With<PlayerBodyMesh>>,
|
||||
mut controllers: Query<
|
||||
(
|
||||
&mut KinematicVelocity,
|
||||
&ActionState<ControlState>,
|
||||
&Children,
|
||||
),
|
||||
With<Character>,
|
||||
>,
|
||||
mut controllers: Query<(&mut KinematicVelocity, &Children, &Inputs), With<Character>>,
|
||||
) {
|
||||
for &ControllerSwitchEvent { controller } in event_controller_switch.read() {
|
||||
let (mut velocity, actions, children) = controllers.get_mut(controller).unwrap();
|
||||
let (mut velocity, children, inputs) = controllers.get_mut(controller).unwrap();
|
||||
|
||||
velocity.0 = Vec3::ZERO;
|
||||
|
||||
@@ -100,7 +93,7 @@ pub fn reset_upon_switch(
|
||||
let mut rig_transform = rig_transforms.get_mut(rig_transform).unwrap();
|
||||
|
||||
// Reset pitch but keep yaw the same
|
||||
let flat_look_dir = actions.look_dir.with_y(0.0).normalize();
|
||||
let flat_look_dir = inputs.look_dir.with_y(0.0).normalize();
|
||||
rig_transform.look_to(flat_look_dir, Dir3::Y);
|
||||
|
||||
match selected_controller.0 {
|
||||
@@ -190,6 +183,7 @@ pub struct CharacterControllerBundle {
|
||||
collision_messages: CollisionEventsEnabled,
|
||||
movement_config: MovementConfig,
|
||||
layers: CollisionLayers,
|
||||
interpolation: TransformInterpolation,
|
||||
}
|
||||
|
||||
impl CharacterControllerBundle {
|
||||
@@ -216,6 +210,7 @@ impl CharacterControllerBundle {
|
||||
LayerMask(GameLayer::Player.to_bits()),
|
||||
LayerMask::ALL & !GameLayer::CollectiblePhysics.to_bits(),
|
||||
),
|
||||
interpolation: TransformInterpolation,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
use super::{ControlState, ControllerSet};
|
||||
use crate::{GameState, control::controller_common::MovementSpeedFactor};
|
||||
use super::ControllerSet;
|
||||
use crate::{
|
||||
GameState,
|
||||
control::{Inputs, controller_common::MovementSpeedFactor},
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
use happy_feet::prelude::MoveInput;
|
||||
use lightyear::prelude::input::native::ActionState;
|
||||
|
||||
pub struct CharacterControllerPlugin;
|
||||
|
||||
@@ -17,14 +19,8 @@ impl Plugin for CharacterControllerPlugin {
|
||||
}
|
||||
}
|
||||
|
||||
pub fn apply_controls(
|
||||
character: Single<(
|
||||
&mut MoveInput,
|
||||
&ActionState<ControlState>,
|
||||
&MovementSpeedFactor,
|
||||
)>,
|
||||
) {
|
||||
let (mut char_input, actions, factor) = character.into_inner();
|
||||
pub fn apply_controls(character: Single<(&mut MoveInput, &MovementSpeedFactor, &Inputs)>) {
|
||||
let (mut char_input, factor, inputs) = character.into_inner();
|
||||
|
||||
char_input.set(actions.look_dir * factor.0);
|
||||
char_input.set(inputs.look_dir * factor.0);
|
||||
}
|
||||
|
||||
@@ -1,42 +1,46 @@
|
||||
use super::{ControlState, ControllerSet};
|
||||
use crate::{
|
||||
GameState,
|
||||
animation::AnimationFlags,
|
||||
control::{ControllerSettings, controller_common::MovementSpeedFactor},
|
||||
control::{ControllerSet, ControllerSettings, Inputs, controller_common::MovementSpeedFactor},
|
||||
};
|
||||
#[cfg(feature = "client")]
|
||||
use crate::{control::LookDirMovement, player::PlayerBodyMesh};
|
||||
use crate::{
|
||||
control::{ControlState, LookDirMovement},
|
||||
player::PlayerBodyMesh,
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
use bevy_replicon::prelude::ClientState;
|
||||
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) {
|
||||
#[cfg(feature = "client")]
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
(
|
||||
#[cfg(feature = "client")]
|
||||
rotate_view,
|
||||
apply_controls,
|
||||
)
|
||||
.chain()
|
||||
Update,
|
||||
rotate_view
|
||||
.in_set(ControllerSet::ApplyControlsRun)
|
||||
.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
|
||||
app.add_systems(
|
||||
FixedUpdate,
|
||||
apply_controls
|
||||
.in_set(ControllerSet::ApplyControlsRun)
|
||||
.run_if(in_state(GameState::Playing))
|
||||
.run_if(in_state(ClientState::Disconnected)),
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
fn rotate_view(
|
||||
actions: Query<&ActionState<ControlState>>,
|
||||
controls: Res<ControlState>,
|
||||
look_dir: Res<LookDirMovement>,
|
||||
mut player: Query<(&mut Transform, &ChildOf), With<PlayerBodyMesh>>,
|
||||
mut player: Query<&mut Transform, With<PlayerBodyMesh>>,
|
||||
) {
|
||||
for (mut tr, child_of) in player.iter_mut() {
|
||||
let controls = actions.get(child_of.parent()).unwrap();
|
||||
|
||||
for mut tr in player.iter_mut() {
|
||||
if controls.view_mode {
|
||||
continue;
|
||||
}
|
||||
@@ -53,24 +57,24 @@ fn apply_controls(
|
||||
&mut AnimationFlags,
|
||||
&ControllerSettings,
|
||||
&MovementSpeedFactor,
|
||||
&ActionState<ControlState>,
|
||||
&Inputs,
|
||||
)>,
|
||||
) {
|
||||
let (mut move_input, mut grounding, mut velocity, mut flags, settings, move_factor, controls) =
|
||||
let (mut move_input, mut grounding, mut velocity, mut flags, settings, move_factor, inputs) =
|
||||
character.into_inner();
|
||||
|
||||
let ground_normal = *grounding.normal().unwrap_or(Dir3::Y);
|
||||
|
||||
let mut direction = controls.move_dir.extend(0.0).xzy();
|
||||
let look_dir_right = controls.look_dir.cross(Vec3::Y);
|
||||
direction = (controls.look_dir * direction.z) + (look_dir_right * direction.x);
|
||||
let mut direction = inputs.move_dir.extend(0.0).xzy();
|
||||
let look_dir_right = inputs.look_dir.cross(Vec3::Y);
|
||||
direction = (inputs.look_dir * direction.z) + (look_dir_right * direction.x);
|
||||
let y_projection = direction.project_onto(ground_normal);
|
||||
direction -= y_projection;
|
||||
direction = direction.normalize_or_zero();
|
||||
|
||||
move_input.set(direction * move_factor.0);
|
||||
|
||||
if controls.jump && grounding.is_grounded() {
|
||||
if inputs.jump && grounding.is_grounded() {
|
||||
if cfg!(feature = "server") {
|
||||
flags.jumping = true;
|
||||
flags.jump_count += 1;
|
||||
|
||||
@@ -2,9 +2,14 @@ use crate::{
|
||||
GameState,
|
||||
head::ActiveHead,
|
||||
heads_database::{HeadControls, HeadsDatabase},
|
||||
player::Player,
|
||||
player::{LocalPlayerId, Player},
|
||||
protocol::{ClientToController, PlayerIdMap},
|
||||
};
|
||||
use bevy::{ecs::entity::MapEntities, prelude::*};
|
||||
use bevy_replicon::{
|
||||
client::ClientSystems,
|
||||
prelude::{ClientState, FromClient},
|
||||
};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod controller_common;
|
||||
@@ -25,6 +30,7 @@ pub struct SelectedController(pub ControllerSet);
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.register_type::<ControllerSettings>();
|
||||
app.register_type::<LookDirMovement>();
|
||||
app.register_type::<Inputs>();
|
||||
|
||||
app.init_resource::<ControlState>();
|
||||
app.init_resource::<LookDirMovement>();
|
||||
@@ -50,16 +56,29 @@ pub fn plugin(app: &mut App) {
|
||||
.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
|
||||
app.add_systems(
|
||||
PreUpdate,
|
||||
(
|
||||
collect_player_inputs.run_if(in_state(ClientState::Disconnected)),
|
||||
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),
|
||||
);
|
||||
app.add_systems(Update, head_change.run_if(in_state(GameState::Playing)));
|
||||
}
|
||||
|
||||
#[derive(Resource, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, Reflect)]
|
||||
// TODO: Split this into an enum of individual input commands, e.g. `JustJumped`
|
||||
#[derive(Resource, Debug, Clone, Copy, Message, PartialEq, Serialize, Deserialize, Reflect)]
|
||||
pub struct ControlState {
|
||||
/// Movement direction with a maximum length of 1.0
|
||||
pub move_dir: Vec2,
|
||||
/// The current direction that the character is facing
|
||||
/// (i.e. the direction that holding the forward movement key moves)
|
||||
pub look_dir: Dir3,
|
||||
pub look_dir: Vec3,
|
||||
pub jump: bool,
|
||||
/// Determines if the camera can rotate freely around the player
|
||||
pub view_mode: bool,
|
||||
@@ -74,15 +93,11 @@ pub struct ControlState {
|
||||
pub cash_heal: bool,
|
||||
}
|
||||
|
||||
#[derive(Resource, Default, Reflect)]
|
||||
#[reflect(Resource)]
|
||||
pub struct LookDirMovement(pub Vec2);
|
||||
|
||||
impl Default for ControlState {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
move_dir: Default::default(),
|
||||
look_dir: Dir3::NEG_Z,
|
||||
look_dir: Vec3::NEG_Z,
|
||||
jump: Default::default(),
|
||||
view_mode: Default::default(),
|
||||
trigger: Default::default(),
|
||||
@@ -102,6 +117,14 @@ impl MapEntities for ControlState {
|
||||
fn map_entities<E: EntityMapper>(&mut self, _entity_mapper: &mut E) {}
|
||||
}
|
||||
|
||||
#[derive(Component, Default, Deref, DerefMut, Reflect, Serialize, Deserialize)]
|
||||
#[reflect(Component, Default)]
|
||||
pub struct Inputs(pub ControlState);
|
||||
|
||||
#[derive(Resource, Default, Reflect)]
|
||||
#[reflect(Resource)]
|
||||
pub struct LookDirMovement(pub Vec2);
|
||||
|
||||
#[derive(Resource, Debug, PartialEq, Eq)]
|
||||
pub enum CharacterInputEnabled {
|
||||
On,
|
||||
@@ -120,6 +143,33 @@ pub struct ControllerSwitchEvent {
|
||||
controller: Entity,
|
||||
}
|
||||
|
||||
/// Take incoming client input messages and cache them on the corresponding player controller
|
||||
fn collect_player_inputs(
|
||||
mut players: Query<&mut Inputs>,
|
||||
clients: ClientToController,
|
||||
mut input_messages: MessageReader<FromClient<ControlState>>,
|
||||
) {
|
||||
for msg in input_messages.read() {
|
||||
let player = clients.get_controller(msg.client_id);
|
||||
let mut inputs = players.get_mut(player).unwrap();
|
||||
|
||||
inputs.0 = msg.message;
|
||||
}
|
||||
}
|
||||
|
||||
/// 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(
|
||||
//TODO: needs a 'LocalPlayer' at some point for multiplayer
|
||||
query: Query<(Entity, &ActiveHead), (Changed<ActiveHead>, With<Player>)>,
|
||||
|
||||
@@ -3,6 +3,7 @@ use crate::{
|
||||
protocol::PlaySound,
|
||||
};
|
||||
use bevy::{platform::collections::HashSet, prelude::*};
|
||||
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
global_observer!(app, on_key_collected);
|
||||
@@ -11,22 +12,34 @@ pub fn plugin(app: &mut App) {
|
||||
fn on_key_collected(trigger: On<KeyCollected>, mut commands: Commands) {
|
||||
match trigger.event().0.as_str() {
|
||||
"fence_gate" => {
|
||||
commands.trigger(StartCutscene("fence_01".to_string()));
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: StartCutscene("fence_01".to_string()),
|
||||
});
|
||||
|
||||
let entities: HashSet<_> = vec!["fence_01", "fence_02"]
|
||||
.into_iter()
|
||||
.map(String::from)
|
||||
.collect();
|
||||
|
||||
commands.trigger(PlaySound::Gate);
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: PlaySound::Gate,
|
||||
});
|
||||
commands.trigger(TriggerMovableEvent(entities));
|
||||
}
|
||||
"fence_shaft" => {
|
||||
commands.trigger(StartCutscene("cutscene_02".to_string()));
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: StartCutscene("cutscene_02".to_string()),
|
||||
});
|
||||
|
||||
let entities: HashSet<_> = vec!["fence_shaft"].into_iter().map(String::from).collect();
|
||||
|
||||
commands.trigger(PlaySound::Gate);
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: PlaySound::Gate,
|
||||
});
|
||||
commands.trigger(TriggerMovableEvent(entities));
|
||||
}
|
||||
_ => {
|
||||
|
||||
@@ -115,19 +115,32 @@ fn spawn_head_ui(
|
||||
..default()
|
||||
},
|
||||
BorderRadius::all(Val::Px(9999.)),
|
||||
BackgroundGradient::from(ConicGradient {
|
||||
start: 0.,
|
||||
stops: vec![
|
||||
AngularColorStop::new(Color::linear_rgba(0., 0., 0., 0.9), 0.),
|
||||
AngularColorStop::new(Color::linear_rgba(0., 0., 0., 0.9), PI * 1.5),
|
||||
AngularColorStop::new(Color::linear_rgba(0., 0., 0., 0.0), PI * 1.5),
|
||||
],
|
||||
position: UiPosition::CENTER,
|
||||
color_space: InterpolationColorSpace::Srgba,
|
||||
}),
|
||||
ImageNode::default(),
|
||||
Visibility::Hidden,
|
||||
HeadImage(head_slot),
|
||||
children![(
|
||||
Node {
|
||||
width: Val::Percent(100.0),
|
||||
height: Val::Percent(100.0),
|
||||
..default()
|
||||
},
|
||||
BorderRadius::all(Val::Px(9999.)),
|
||||
HeadImage(0),
|
||||
ImageNode {
|
||||
color: Color::linear_rgba(0.0, 0.0, 0.0, 0.0),
|
||||
..default()
|
||||
},
|
||||
BackgroundGradient::from(ConicGradient {
|
||||
start: 0.,
|
||||
stops: vec![
|
||||
AngularColorStop::new(Color::linear_rgba(0., 0., 0., 0.9), 0.),
|
||||
AngularColorStop::new(Color::linear_rgba(0., 0., 0., 0.9), PI * 1.5),
|
||||
AngularColorStop::new(Color::linear_rgba(0., 0., 0., 0.0), PI * 1.5),
|
||||
],
|
||||
position: UiPosition::CENTER,
|
||||
color_space: InterpolationColorSpace::Srgba,
|
||||
}),
|
||||
)]
|
||||
),
|
||||
(
|
||||
Node {
|
||||
@@ -163,7 +176,7 @@ fn spawn_head_ui(
|
||||
|
||||
#[cfg(feature = "client")]
|
||||
fn update(
|
||||
res: Single<&UiActiveHeads, Changed<UiActiveHeads>>,
|
||||
res: Single<&UiActiveHeads>,
|
||||
heads_images: Res<HeadsImages>,
|
||||
mut head_image: Query<(&HeadImage, &mut Visibility, &mut ImageNode), Without<HeadSelector>>,
|
||||
mut head_selector: Query<(&HeadSelector, &mut Visibility), Without<HeadImage>>,
|
||||
@@ -188,9 +201,14 @@ fn update(
|
||||
#[cfg(feature = "client")]
|
||||
fn update_ammo(
|
||||
res: Single<&UiActiveHeads, Changed<UiActiveHeads>>,
|
||||
mut gradients: Query<(&mut BackgroundGradient, &HeadImage)>,
|
||||
heads: Query<&HeadImage>,
|
||||
mut gradients: Query<(&mut BackgroundGradient, &ChildOf)>,
|
||||
) {
|
||||
for (mut gradient, HeadImage(head)) in gradients.iter_mut() {
|
||||
for (mut gradient, child_of) in gradients.iter_mut() {
|
||||
let Ok(HeadImage(head)) = heads.get(child_of.parent()) else {
|
||||
continue;
|
||||
};
|
||||
|
||||
if let Some(head) = res.heads[*head] {
|
||||
let Gradient::Conic(gradient) = &mut gradient.0[0] else {
|
||||
continue;
|
||||
@@ -202,7 +220,7 @@ fn update_ammo(
|
||||
head.ammo_used()
|
||||
};
|
||||
|
||||
let angle = progress * PI * 2.;
|
||||
let angle = progress * PI * 2.0;
|
||||
|
||||
gradient.stops[1].angle = Some(angle);
|
||||
gradient.stops[2].angle = Some(angle);
|
||||
|
||||
@@ -1,5 +1,3 @@
|
||||
pub mod heads_ui;
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
use crate::animation::AnimationFlags;
|
||||
use crate::{
|
||||
@@ -14,9 +12,11 @@ use crate::{
|
||||
use crate::{control::ControlState, protocol::PlaySound};
|
||||
use bevy::prelude::*;
|
||||
#[cfg(feature = "server")]
|
||||
use lightyear::prelude::input::native::ActionState;
|
||||
use bevy_replicon::prelude::FromClient;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod heads_ui;
|
||||
|
||||
pub static HEAD_COUNT: usize = 18;
|
||||
pub static HEAD_SLOTS: usize = 5;
|
||||
|
||||
@@ -232,7 +232,12 @@ fn reload(
|
||||
|
||||
if !head.has_ammo() && (head.last_use + head.reload_duration <= time.elapsed_secs()) {
|
||||
// only for player?
|
||||
commands.trigger(PlaySound::Reloaded);
|
||||
|
||||
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients};
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: PlaySound::Reloaded,
|
||||
});
|
||||
flags.restart_shooting = true;
|
||||
head.ammo = head.ammo_max;
|
||||
}
|
||||
@@ -243,31 +248,39 @@ fn reload(
|
||||
#[cfg(feature = "server")]
|
||||
fn on_select_active_head(
|
||||
mut commands: Commands,
|
||||
mut query: Query<(&mut ActiveHeads, &mut Hitpoints, &ActionState<ControlState>), With<Player>>,
|
||||
mut query: Query<(&mut ActiveHeads, &mut Hitpoints), With<Player>>,
|
||||
mut controls: MessageReader<FromClient<ControlState>>,
|
||||
) {
|
||||
for (mut active_heads, mut hp, controls) in query.iter_mut() {
|
||||
if !controls.select_right && !controls.select_left {
|
||||
continue;
|
||||
}
|
||||
for controls in controls.read() {
|
||||
for (mut active_heads, mut hp) in query.iter_mut() {
|
||||
use bevy_replicon::prelude::{SendMode, ServerTriggerExt, ToClients};
|
||||
|
||||
if controls.select_right {
|
||||
active_heads.selected_slot = (active_heads.selected_slot + 1) % HEAD_SLOTS;
|
||||
}
|
||||
if !controls.select_right && !controls.select_left {
|
||||
continue;
|
||||
}
|
||||
|
||||
if controls.select_left {
|
||||
active_heads.selected_slot =
|
||||
(active_heads.selected_slot + (HEAD_SLOTS - 1)) % HEAD_SLOTS;
|
||||
}
|
||||
if controls.select_right {
|
||||
active_heads.selected_slot = (active_heads.selected_slot + 1) % HEAD_SLOTS;
|
||||
}
|
||||
|
||||
commands.trigger(PlaySound::Selection);
|
||||
if controls.select_left {
|
||||
active_heads.selected_slot =
|
||||
(active_heads.selected_slot + (HEAD_SLOTS - 1)) % HEAD_SLOTS;
|
||||
}
|
||||
|
||||
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.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: PlaySound::Selection,
|
||||
});
|
||||
|
||||
commands.trigger(HeadChanged(
|
||||
active_heads.heads[active_heads.current_slot].unwrap().head,
|
||||
));
|
||||
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,
|
||||
));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -5,6 +5,7 @@ use crate::{
|
||||
protocol::PlaySound,
|
||||
};
|
||||
use bevy::prelude::*;
|
||||
use bevy_replicon::prelude::{ClientState, SendMode, ServerTriggerExt, ToClients};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(EntityEvent, Reflect)]
|
||||
@@ -65,9 +66,14 @@ impl Hitpoints {
|
||||
}
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(Update, on_hp_added).add_systems(
|
||||
app.add_systems(
|
||||
Update,
|
||||
on_hp_added.run_if(in_state(ClientState::Disconnected)),
|
||||
)
|
||||
.add_systems(
|
||||
PreUpdate,
|
||||
reset_hit_animation_flag.run_if(in_state(GameState::Playing)),
|
||||
reset_hit_animation_flag
|
||||
.run_if(in_state(ClientState::Disconnected).and(in_state(GameState::Playing))),
|
||||
);
|
||||
}
|
||||
|
||||
@@ -89,7 +95,10 @@ fn on_hit(
|
||||
return;
|
||||
};
|
||||
|
||||
commands.trigger(PlaySound::Hit);
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: PlaySound::Hit,
|
||||
});
|
||||
|
||||
if let Some(mut flags) = flags {
|
||||
flags.hit = true;
|
||||
|
||||
@@ -8,13 +8,8 @@ use crate::{
|
||||
utils::one_shot_force::OneShotForce,
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::{
|
||||
ecs::{relationship::RelatedSpawner, spawn::SpawnWith},
|
||||
prelude::*,
|
||||
};
|
||||
use lightyear::prelude::DisableReplicateHierarchy;
|
||||
#[cfg(feature = "server")]
|
||||
use lightyear::prelude::{NetworkTarget, Replicate};
|
||||
use bevy::prelude::*;
|
||||
use bevy_replicon::prelude::{Replicated, SendMode, ServerTriggerExt, ToClients};
|
||||
use std::f32::consts::PI;
|
||||
|
||||
#[derive(Event, Reflect)]
|
||||
@@ -40,35 +35,37 @@ fn on_spawn_key(trigger: On<KeySpawn>, mut commands: Commands, time: Res<Time<Fi
|
||||
let spawn_dir = Quat::from_rotation_y(angle) * Vec3::new(0.5, 0.6, 0.).normalize();
|
||||
let spawn_force = spawn_dir * 180.0 / time.delta_secs();
|
||||
|
||||
commands.spawn((
|
||||
Name::new("key"),
|
||||
Transform::from_translation(*position),
|
||||
Position::new(*position),
|
||||
Visibility::default(),
|
||||
Collider::sphere(1.5),
|
||||
LockedAxes::ROTATION_LOCKED,
|
||||
RigidBody::Dynamic,
|
||||
OneShotForce(spawn_force),
|
||||
CollisionLayers::new(GameLayer::CollectiblePhysics, GameLayer::Level),
|
||||
Restitution::new(0.6),
|
||||
Children::spawn((
|
||||
Spawn((Billboard::All, SquishAnimation(2.6), GltfSceneRoot::Key)),
|
||||
SpawnWith(|parent: &mut RelatedSpawner<ChildOf>| {
|
||||
parent
|
||||
.spawn((
|
||||
Collider::sphere(1.5),
|
||||
CollisionLayers::new(GameLayer::CollectibleSensors, GameLayer::Player),
|
||||
Sensor,
|
||||
CollisionEventsEnabled,
|
||||
Key(id),
|
||||
DisableReplicateHierarchy,
|
||||
))
|
||||
.observe(on_collect_key);
|
||||
}),
|
||||
)),
|
||||
#[cfg(feature = "server")]
|
||||
Replicate::to_clients(NetworkTarget::All),
|
||||
));
|
||||
commands
|
||||
.spawn((
|
||||
Name::new("key"),
|
||||
Transform::from_translation(*position),
|
||||
Position::new(*position),
|
||||
Visibility::default(),
|
||||
Collider::sphere(1.5),
|
||||
LockedAxes::ROTATION_LOCKED,
|
||||
RigidBody::Dynamic,
|
||||
OneShotForce(spawn_force),
|
||||
CollisionLayers::new(GameLayer::CollectiblePhysics, GameLayer::Level),
|
||||
Restitution::new(0.6),
|
||||
Replicated,
|
||||
))
|
||||
.with_children(|c| {
|
||||
c.spawn((
|
||||
Billboard::All,
|
||||
SquishAnimation(2.6),
|
||||
GltfSceneRoot::Key,
|
||||
Replicated,
|
||||
));
|
||||
c.spawn((
|
||||
Collider::sphere(1.5),
|
||||
CollisionLayers::new(GameLayer::CollectibleSensors, GameLayer::Player),
|
||||
Sensor,
|
||||
CollisionEventsEnabled,
|
||||
Key(id),
|
||||
Replicated,
|
||||
))
|
||||
.observe(on_collect_key);
|
||||
});
|
||||
}
|
||||
|
||||
fn on_collect_key(
|
||||
@@ -83,7 +80,10 @@ fn on_collect_key(
|
||||
if query_player.contains(collider) {
|
||||
let (key, child_of) = query_collectable.get(key).unwrap();
|
||||
|
||||
commands.trigger(PlaySound::KeyCollect);
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: PlaySound::KeyCollect,
|
||||
});
|
||||
commands.trigger(KeyCollected(key.0.clone()));
|
||||
commands.entity(child_of.parent()).despawn();
|
||||
}
|
||||
|
||||
@@ -26,6 +26,7 @@ pub mod player;
|
||||
pub mod protocol;
|
||||
pub mod steam;
|
||||
pub mod tb_entities;
|
||||
pub mod tick;
|
||||
pub mod utils;
|
||||
pub mod water;
|
||||
|
||||
|
||||
@@ -1,12 +1,8 @@
|
||||
#[cfg(feature = "server")]
|
||||
use crate::protocol::TbMapEntityId;
|
||||
use crate::{GameState, physics_layers::GameLayer};
|
||||
use crate::{GameState, physics_layers::GameLayer, protocol::TbMapEntityId};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::prelude::*;
|
||||
use bevy_replicon::prelude::Replicated;
|
||||
use bevy_trenchbroom::physics::SceneCollidersReady;
|
||||
use lightyear::prelude::ReplicationGroup;
|
||||
#[cfg(feature = "server")]
|
||||
use lightyear::prelude::{DisableReplicateHierarchy, NetworkTarget, Replicate};
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_systems(OnEnter(GameState::MapLoading), setup_scene);
|
||||
@@ -22,33 +18,20 @@ fn setup_scene(mut commands: Commands, asset_server: Res<AssetServer>) {
|
||||
.observe(
|
||||
|t: On<SceneCollidersReady>,
|
||||
children: Query<&Children>,
|
||||
#[cfg(feature = "server")] map_entities: Query<&TbMapEntityId>,
|
||||
map_entities: Query<&TbMapEntityId>,
|
||||
mut commands: Commands,
|
||||
#[cfg(any(feature = "client", feature = "server"))] mut next_game_state: ResMut<
|
||||
NextState<GameState>,
|
||||
>| {
|
||||
mut next_game_state: ResMut<NextState<GameState>>| {
|
||||
info!("map loaded");
|
||||
|
||||
for child in children.get(t.event().scene_root_entity).unwrap() {
|
||||
commands.entity(*child).remove::<ChildOf>();
|
||||
|
||||
#[cfg(feature = "server")]
|
||||
if map_entities.contains(*child) {
|
||||
commands.entity(*child).insert((
|
||||
Replicate::to_clients(NetworkTarget::All),
|
||||
ReplicationGroup::new_id(t.scene_root_entity.to_bits()),
|
||||
DisableReplicateHierarchy,
|
||||
));
|
||||
commands.entity(*child).insert(Replicated);
|
||||
}
|
||||
}
|
||||
|
||||
commands.entity(t.scene_root_entity).insert((
|
||||
#[cfg(feature = "server")]
|
||||
Replicate::to_clients(NetworkTarget::All),
|
||||
#[cfg(feature = "server")]
|
||||
DisableReplicateHierarchy,
|
||||
ReplicationGroup::new_from_entity(),
|
||||
));
|
||||
commands.entity(t.scene_root_entity).insert(Replicated);
|
||||
|
||||
const { assert!(cfg!(feature = "client") ^ cfg!(feature = "server")) };
|
||||
|
||||
|
||||
@@ -80,6 +80,8 @@ fn on_spawn_check(
|
||||
}
|
||||
|
||||
for (e, spawn, transform) in query.iter() {
|
||||
use bevy_replicon::prelude::{Replicated, SendMode, ServerTriggerExt, ToClients};
|
||||
|
||||
if let Some(order) = spawn.spawn_order
|
||||
&& order > spawning.spawn_index
|
||||
{
|
||||
@@ -100,13 +102,21 @@ fn on_spawn_check(
|
||||
None,
|
||||
None,
|
||||
]),
|
||||
Replicated,
|
||||
))
|
||||
.insert_if(Ai, || !spawn.disable_ai)
|
||||
.with_child((Name::from("body-rig"), AnimatedCharacter::new(id)))
|
||||
.with_child((
|
||||
Name::from("body-rig"),
|
||||
AnimatedCharacter::new(id),
|
||||
Replicated,
|
||||
))
|
||||
.observe(on_kill);
|
||||
|
||||
commands.trigger(SpawnCharacter(transform.translation));
|
||||
commands.trigger(PlaySound::Beaming);
|
||||
commands.server_trigger(ToClients {
|
||||
mode: SendMode::Broadcast,
|
||||
message: PlaySound::Beaming,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
use crate::GameState;
|
||||
use crate::{GameState, tick::GameTick};
|
||||
use avian3d::prelude::{LinearVelocity, Position};
|
||||
use bevy::{math::ops::sin, prelude::*};
|
||||
use lightyear::prelude::LocalTimeline;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Component, Reflect, Default, Debug, Serialize, Deserialize, PartialEq)]
|
||||
@@ -20,18 +19,17 @@ pub fn plugin(app: &mut App) {
|
||||
}
|
||||
|
||||
fn move_active(
|
||||
#[cfg(not(feature = "server"))] time: Single<&LocalTimeline>,
|
||||
#[cfg(feature = "server")] time: Single<&LocalTimeline, With<lightyear::prelude::Server>>,
|
||||
bevy_time: Res<Time<Fixed>>,
|
||||
tick: Res<GameTick>,
|
||||
fixed_time: Res<Time<Fixed>>,
|
||||
mut platforms: Query<(&Position, &ActivePlatform, &mut LinearVelocity)>,
|
||||
) {
|
||||
for (position, active, mut velocity) in platforms.iter_mut() {
|
||||
let now = time.now.as_duration(bevy_time.delta()).as_secs_f32();
|
||||
let now = tick.0 as f32 * fixed_time.timestep().as_secs_f32();
|
||||
let t = (sin(now * 0.4) + 1.) / 2.;
|
||||
|
||||
let target = active.start.lerp(active.target, t);
|
||||
let prev = position.0;
|
||||
|
||||
velocity.0 = (target - prev) / bevy_time.delta_secs();
|
||||
velocity.0 = (target - prev) / fixed_time.timestep().as_secs_f32();
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ use crate::{
|
||||
GameState,
|
||||
cash::{Cash, CashCollectEvent},
|
||||
character::HedzCharacter,
|
||||
protocol::PlayerId,
|
||||
};
|
||||
use avian3d::prelude::*;
|
||||
use bevy::{
|
||||
@@ -20,8 +21,22 @@ pub struct Player;
|
||||
#[require(Transform, Visibility)]
|
||||
pub struct PlayerBodyMesh;
|
||||
|
||||
/// Server-side only; inserted on each `client` (not the controller) to track player ids.
|
||||
#[derive(Component, Clone, Copy)]
|
||||
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) {
|
||||
app.add_systems(Startup, (toggle_cursor_system, cursor_recenter));
|
||||
app.add_systems(
|
||||
OnEnter(GameState::Playing),
|
||||
(toggle_cursor_system, cursor_recenter),
|
||||
);
|
||||
app.add_systems(
|
||||
Update,
|
||||
(
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
pub struct UnorderedReliableChannel;
|
||||
@@ -1,21 +1,120 @@
|
||||
use crate::{
|
||||
loading_assets::{GameAssets, HeadDropAssets},
|
||||
player::{ClientPlayerId, LocalPlayerId},
|
||||
protocol::TbMapEntityMapping,
|
||||
};
|
||||
use avian3d::prelude::Collider;
|
||||
use bevy::{
|
||||
ecs::{lifecycle::HookContext, world::DeferredWorld},
|
||||
platform::collections::hash_map,
|
||||
ecs::{lifecycle::HookContext, system::SystemParam, world::DeferredWorld},
|
||||
platform::collections::{HashMap, hash_map},
|
||||
prelude::*,
|
||||
};
|
||||
use lightyear::prelude::Replicated;
|
||||
use bevy_replicon::{client::confirm_history::ConfirmHistory, prelude::ClientId};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Copy, Component, Reflect, Serialize, Deserialize, PartialEq)]
|
||||
#[derive(Component)]
|
||||
pub struct SkipReplicateColliders;
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
pub enum NetworkedCollider {
|
||||
Sphere {
|
||||
radius: f32,
|
||||
},
|
||||
Cuboid {
|
||||
half_extents: Vec3,
|
||||
},
|
||||
Capsule {
|
||||
radius: f32,
|
||||
length: f32,
|
||||
},
|
||||
/// If a collider value wasn't set up to be replicated, it is replicated as unknown
|
||||
/// and a warning is logged, and unwraps to `sphere(0.1)` on the other side. Likely
|
||||
/// very incorrect, but good enough to mitigate some bugs before it's fixed.
|
||||
Unknown,
|
||||
}
|
||||
|
||||
impl From<Collider> for NetworkedCollider {
|
||||
fn from(value: Collider) -> Self {
|
||||
if let Some(value) = value.shape().as_ball() {
|
||||
NetworkedCollider::Sphere {
|
||||
radius: value.radius,
|
||||
}
|
||||
} else if let Some(value) = value.shape().as_cuboid() {
|
||||
NetworkedCollider::Cuboid {
|
||||
half_extents: value.half_extents.into(),
|
||||
}
|
||||
} else if let Some(value) = value.shape().as_capsule() {
|
||||
NetworkedCollider::Capsule {
|
||||
radius: value.radius,
|
||||
length: value.height(),
|
||||
}
|
||||
} else {
|
||||
warn!(
|
||||
"unable to serialize collider type {value:?}; must be accounted for in `NetworkedCollider`"
|
||||
);
|
||||
NetworkedCollider::Unknown
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<NetworkedCollider> for Collider {
|
||||
fn from(value: NetworkedCollider) -> Self {
|
||||
match value {
|
||||
NetworkedCollider::Sphere { radius } => Collider::sphere(radius),
|
||||
NetworkedCollider::Cuboid { half_extents } => {
|
||||
Collider::cuboid(half_extents.x, half_extents.y, half_extents.z)
|
||||
}
|
||||
NetworkedCollider::Capsule { radius, length } => Collider::capsule(radius, length),
|
||||
NetworkedCollider::Unknown => Collider::sphere(0.1),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// An ID, unique per player, inserted on the character controller. The `PlayerIdMap` maintains a mapping of ID -> controller entity
|
||||
/// on the server
|
||||
#[derive(Clone, Copy, Component, Hash, Reflect, Serialize, Deserialize, PartialEq, Eq)]
|
||||
#[reflect(Component)]
|
||||
#[component(on_insert = PlayerId::on_insert, on_remove = PlayerId::on_remove)]
|
||||
pub struct PlayerId {
|
||||
pub id: u8,
|
||||
}
|
||||
|
||||
impl PlayerId {
|
||||
fn on_insert(mut world: DeferredWorld, ctx: HookContext) {
|
||||
let id = *world.get::<PlayerId>(ctx.entity).unwrap();
|
||||
world.resource_mut::<PlayerIdMap>().insert(id, ctx.entity);
|
||||
}
|
||||
|
||||
fn on_remove(mut world: DeferredWorld, ctx: HookContext) {
|
||||
let id = *world.get::<PlayerId>(ctx.entity).unwrap();
|
||||
world.resource_mut::<PlayerIdMap>().insert(id, ctx.entity);
|
||||
}
|
||||
}
|
||||
|
||||
/// A (serverside only) mapping of ID -> controller entity
|
||||
#[derive(Resource, Default, Deref, DerefMut)]
|
||||
pub struct PlayerIdMap {
|
||||
pub map: HashMap<PlayerId, Entity>,
|
||||
}
|
||||
|
||||
#[derive(SystemParam)]
|
||||
pub struct ClientToController<'w, 's> {
|
||||
clients: Query<'w, 's, &'static ClientPlayerId>,
|
||||
players: Res<'w, PlayerIdMap>,
|
||||
local_id: Option<Res<'w, LocalPlayerId>>,
|
||||
}
|
||||
|
||||
impl ClientToController<'_, '_> {
|
||||
/// Looks up the character controller owned by the given client
|
||||
pub fn get_controller(&self, client: ClientId) -> Entity {
|
||||
let player_id = match client.entity() {
|
||||
Some(client) => self.clients.get(client).unwrap().0,
|
||||
None => self.local_id.as_ref().unwrap().id,
|
||||
};
|
||||
*self.players.get(&player_id).unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
/// A unique ID assigned to every entity spawned by the trenchbroom map loader. This allows synchronizing
|
||||
/// them across the network even when they are spawned initially by both sides.
|
||||
#[derive(Clone, Copy, Component, Debug, Reflect, Serialize, Deserialize, PartialEq)]
|
||||
@@ -28,7 +127,9 @@ pub struct TbMapEntityId {
|
||||
impl TbMapEntityId {
|
||||
fn insert_id(mut world: DeferredWorld, ctx: HookContext) {
|
||||
let id = world.get::<TbMapEntityId>(ctx.entity).unwrap().id;
|
||||
let entity_is_replicated = world.entity(ctx.entity).contains::<Replicated>();
|
||||
// Under lightyear, this was querying `Replicated`. But in replicon `Replicated` is on both sides, and
|
||||
// `ConfirmHistory` is only client-side.
|
||||
let entity_is_replicated = world.entity(ctx.entity).contains::<ConfirmHistory>();
|
||||
let mut mapping = world.resource_mut::<TbMapEntityMapping>();
|
||||
if let hash_map::Entry::Vacant(e) = mapping.entry(id) {
|
||||
if entity_is_replicated {
|
||||
|
||||
@@ -27,5 +27,8 @@ pub enum PlaySound {
|
||||
Head(String),
|
||||
}
|
||||
|
||||
#[derive(Clone, Event, Serialize, Deserialize)]
|
||||
#[derive(Clone, Default, Event, Serialize, Deserialize)]
|
||||
pub struct ClientEnteredPlaying;
|
||||
|
||||
#[derive(Clone, Event, Serialize, Deserialize)]
|
||||
pub struct SetGameTick(pub u64);
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
use crate::protocol::PlayerId;
|
||||
use bevy::ecs::message::Message;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
/// Trenchbroom map entities (spawned during map loading) must be despawned manually if the server
|
||||
/// has already despawned it but the client has just loaded the map and connected
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
#[derive(Clone, Copy, Message, Serialize, Deserialize)]
|
||||
pub struct DespawnTbMapEntity(pub u64);
|
||||
|
||||
#[derive(Clone, Copy, Serialize, Deserialize)]
|
||||
pub struct AssignClientPlayer(pub u8);
|
||||
#[derive(Clone, Copy, Message, Serialize, Deserialize)]
|
||||
pub struct AssignClientPlayer(pub PlayerId);
|
||||
|
||||
@@ -1,8 +1,3 @@
|
||||
pub mod channels;
|
||||
pub mod components;
|
||||
pub mod events;
|
||||
pub mod messages;
|
||||
|
||||
use crate::{
|
||||
GameState,
|
||||
abilities::{BuildExplosionSprite, healing::Healing},
|
||||
@@ -12,7 +7,7 @@ use crate::{
|
||||
cash::CashResource,
|
||||
character::{AnimatedCharacter, HedzCharacter},
|
||||
control::{
|
||||
ControlState, ControllerSettings,
|
||||
ControlState, ControllerSettings, Inputs,
|
||||
controller_common::{MovementSpeedFactor, PlayerCharacterController},
|
||||
},
|
||||
cutscene::StartCutscene,
|
||||
@@ -22,14 +17,18 @@ use crate::{
|
||||
hitpoints::Hitpoints,
|
||||
platforms::ActivePlatform,
|
||||
player::{Player, PlayerBodyMesh},
|
||||
protocol::channels::UnorderedReliableChannel,
|
||||
utils::{
|
||||
auto_rotate::AutoRotation, billboards::Billboard, squish_animation::SquishAnimation,
|
||||
triggers::TriggerAppExt,
|
||||
},
|
||||
tick::GameTick,
|
||||
utils::{auto_rotate::AutoRotation, billboards::Billboard, squish_animation::SquishAnimation},
|
||||
};
|
||||
use avian3d::prelude::{
|
||||
AngularInertia, AngularVelocity, CenterOfMass, Collider, ColliderDensity, CollisionLayers,
|
||||
LinearVelocity, LockedAxes, Mass, Position, RigidBody, Rotation,
|
||||
};
|
||||
use avian3d::prelude::{AngularVelocity, CollisionLayers, LinearVelocity, Position, Rotation};
|
||||
use bevy::{platform::collections::HashMap, prelude::*};
|
||||
use bevy_replicon::prelude::{
|
||||
AppRuleExt, Channel, ClientEventAppExt, ClientMessageAppExt, ServerEventAppExt,
|
||||
ServerMessageAppExt, SyncRelatedAppExt,
|
||||
};
|
||||
pub use components::*;
|
||||
pub use events::*;
|
||||
use happy_feet::{
|
||||
@@ -39,146 +38,134 @@ use happy_feet::{
|
||||
GroundingConfig, KinematicVelocity, MoveInput, SteppingConfig,
|
||||
},
|
||||
};
|
||||
use lightyear::prelude::{
|
||||
AppChannelExt, AppComponentExt, AppMessageExt, AppTriggerExt, ChannelMode, ChannelSettings,
|
||||
ComponentReplicationConfig, Confirmed, ConfirmedTick, InterpolationRegistrationExt,
|
||||
NetworkDirection, PredictionHistory, PredictionRegistrationExt, ReliableSettings,
|
||||
input::native::InputPlugin,
|
||||
};
|
||||
use lightyear_serde::{
|
||||
SerializationError, reader::ReadInteger, registry::SerializeFns, writer::WriteInteger,
|
||||
};
|
||||
use std::time::Duration;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
pub mod components;
|
||||
pub mod events;
|
||||
pub mod messages;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.add_plugins(InputPlugin::<ControlState>::default());
|
||||
app.add_client_message::<ControlState>(Channel::Unreliable);
|
||||
|
||||
app.register_type::<ConfirmedTick>();
|
||||
app.register_type::<Confirmed<Position>>();
|
||||
app.register_type::<Confirmed<Rotation>>();
|
||||
app.register_type::<PredictionHistory<Position>>();
|
||||
app.add_client_event::<ClientEnteredPlaying>(Channel::Ordered);
|
||||
|
||||
app.add_server_message::<messages::DespawnTbMapEntity>(Channel::Unordered)
|
||||
.add_server_message::<messages::AssignClientPlayer>(Channel::Unordered);
|
||||
|
||||
app.add_server_event::<ClientHeadChanged>(Channel::Unordered)
|
||||
.add_server_event::<BuildExplosionSprite>(Channel::Unreliable)
|
||||
.add_server_event::<StartCutscene>(Channel::Ordered)
|
||||
.add_server_event::<events::PlaySound>(Channel::Unreliable)
|
||||
.add_server_event::<events::SetGameTick>(Channel::Ordered);
|
||||
|
||||
app.register_type::<PlayerId>();
|
||||
app.register_type::<TbMapEntityId>();
|
||||
app.register_type::<TbMapIdCounter>();
|
||||
app.register_type::<TbMapEntityMapping>();
|
||||
|
||||
app.init_resource::<PlayerIdMap>();
|
||||
app.init_resource::<TbMapIdCounter>();
|
||||
app.init_resource::<TbMapEntityMapping>();
|
||||
|
||||
app.add_channel::<UnorderedReliableChannel>(ChannelSettings {
|
||||
mode: ChannelMode::UnorderedReliable(ReliableSettings::default()),
|
||||
send_frequency: Duration::from_millis(100),
|
||||
priority: 1.0,
|
||||
})
|
||||
.add_direction(NetworkDirection::Bidirectional);
|
||||
app.replicate::<ChildOf>();
|
||||
app.sync_related_entities::<ChildOf>();
|
||||
|
||||
app.register_message::<messages::DespawnTbMapEntity>()
|
||||
.add_direction(NetworkDirection::ServerToClient);
|
||||
app.register_message::<messages::AssignClientPlayer>()
|
||||
.add_direction(NetworkDirection::ServerToClient);
|
||||
app.replicate::<components::GltfSceneRoot>()
|
||||
.replicate_once::<components::PlayerId>()
|
||||
.replicate::<components::TbMapEntityId>()
|
||||
.replicate::<ActiveHead>()
|
||||
.replicate::<ActiveHeads>()
|
||||
.replicate::<ActivePlatform>()
|
||||
.replicate::<AnimatedCharacter>()
|
||||
.replicate::<AnimationFlags>()
|
||||
.replicate_once::<AutoRotation>()
|
||||
.replicate::<Backpack>()
|
||||
.replicate::<BackpackUiState>()
|
||||
.replicate::<Billboard>()
|
||||
.replicate_once::<CameraArmRotation>()
|
||||
.replicate_once::<CameraTarget>()
|
||||
.replicate::<CashResource>()
|
||||
.replicate_once::<HedzCharacter>()
|
||||
.replicate_once::<Healing>()
|
||||
.replicate::<Hitpoints>()
|
||||
.replicate::<Inputs>()
|
||||
.replicate::<Name>()
|
||||
.replicate_once::<Player>()
|
||||
.replicate_once::<PlayerBodyMesh>()
|
||||
.replicate::<SquishAnimation>()
|
||||
.replicate_once::<Transform>()
|
||||
.replicate::<UiActiveHeads>()
|
||||
.replicate_as::<Visibility, SerVisibility>();
|
||||
|
||||
app.register_component::<components::GltfSceneRoot>();
|
||||
app.register_component::<components::PlayerId>();
|
||||
app.register_component::<components::TbMapEntityId>();
|
||||
app.register_component::<ActiveHead>();
|
||||
app.register_component::<ActiveHeads>();
|
||||
app.register_component::<ActivePlatform>();
|
||||
app.register_component::<AnimatedCharacter>();
|
||||
app.register_component::<AnimationFlags>();
|
||||
app.register_component::<AutoRotation>()
|
||||
.with_replication_config(ComponentReplicationConfig {
|
||||
replicate_once: true,
|
||||
..default()
|
||||
});
|
||||
app.register_component::<Backpack>();
|
||||
app.register_component::<BackpackUiState>();
|
||||
app.register_component::<Billboard>();
|
||||
app.register_component::<CameraArmRotation>();
|
||||
app.register_component::<CameraTarget>();
|
||||
app.register_component::<CashResource>();
|
||||
app.register_component::<HedzCharacter>();
|
||||
app.register_component::<Healing>();
|
||||
app.register_component::<Hitpoints>();
|
||||
app.register_component::<Name>();
|
||||
app.register_component::<Player>();
|
||||
app.register_component::<PlayerBodyMesh>()
|
||||
.with_replication_config(ComponentReplicationConfig {
|
||||
replicate_once: true,
|
||||
..default()
|
||||
});
|
||||
app.register_component::<SquishAnimation>();
|
||||
// Physics components
|
||||
app.replicate::<AngularInertia>()
|
||||
.replicate::<AngularVelocity>()
|
||||
.replicate::<CenterOfMass>()
|
||||
.replicate_filtered_as::<Collider, NetworkedCollider, Without<SkipReplicateColliders>>()
|
||||
.replicate::<ColliderDensity>()
|
||||
.replicate::<CollisionLayers>()
|
||||
.replicate::<LinearVelocity>()
|
||||
.replicate::<LockedAxes>()
|
||||
.replicate::<Mass>()
|
||||
.replicate::<Position>()
|
||||
.replicate::<RigidBody>()
|
||||
.replicate::<Rotation>();
|
||||
|
||||
// Physics
|
||||
app.register_component::<AngularVelocity>()
|
||||
.add_prediction()
|
||||
.add_should_rollback(|this, that| this.0.distance_squared(that.0) >= 0.01f32.powf(2.0));
|
||||
app.register_component::<CollisionLayers>();
|
||||
app.register_component::<LinearVelocity>()
|
||||
.add_prediction()
|
||||
.add_should_rollback(|this, that| this.0.distance_squared(that.0) >= 0.01f32.powf(2.0));
|
||||
app.register_component::<Position>()
|
||||
.add_prediction()
|
||||
.add_should_rollback(|this, that| this.0.distance_squared(that.0) >= 0.01f32.powf(2.0))
|
||||
.add_linear_correction_fn()
|
||||
.add_linear_interpolation();
|
||||
app.register_component::<Rotation>()
|
||||
.add_prediction()
|
||||
.add_should_rollback(|this, that| this.angle_between(*that) >= 0.01)
|
||||
.add_linear_correction_fn()
|
||||
.add_linear_interpolation();
|
||||
|
||||
// Controller
|
||||
app.register_component::<CharacterDrag>();
|
||||
app.register_component::<CharacterGravity>();
|
||||
app.register_component::<CharacterMovement>();
|
||||
app.register_component::<ControllerSettings>();
|
||||
app.register_component::<GroundFriction>();
|
||||
app.register_component::<Grounding>();
|
||||
app.register_component::<GroundingConfig>();
|
||||
app.register_component::<GroundingState>();
|
||||
app.register_component::<KinematicVelocity>();
|
||||
app.register_component::<MoveInput>();
|
||||
app.register_component::<MovementSpeedFactor>();
|
||||
app.register_component::<PlayerCharacterController>()
|
||||
.with_replication_config(ComponentReplicationConfig {
|
||||
replicate_once: true,
|
||||
..default()
|
||||
});
|
||||
app.register_component::<SteppingConfig>();
|
||||
|
||||
app.register_component::<UiActiveHeads>();
|
||||
// `Visibility` isn't `(De)Serialize`, so we have to provide custom serde for it.
|
||||
app.register_component_custom_serde::<Visibility>(SerializeFns {
|
||||
serialize: |comp, writer| writer.write_u8(*comp as u8).map_err(SerializationError::Io),
|
||||
deserialize: |reader| {
|
||||
let byte = reader.read_u8().map_err(SerializationError::Io)?;
|
||||
Ok(match byte {
|
||||
0 => Visibility::Inherited,
|
||||
1 => Visibility::Hidden,
|
||||
2 => Visibility::Visible,
|
||||
_ => return Err(SerializationError::InvalidValue),
|
||||
})
|
||||
},
|
||||
});
|
||||
|
||||
app.replicate_event::<BuildExplosionSprite, UnorderedReliableChannel>();
|
||||
app.replicate_event::<StartCutscene, UnorderedReliableChannel>();
|
||||
|
||||
app.replicate_event::<events::ClientHeadChanged, UnorderedReliableChannel>();
|
||||
app.replicate_event::<events::PlaySound, UnorderedReliableChannel>();
|
||||
|
||||
app.register_event::<events::ClientEnteredPlaying>()
|
||||
.add_direction(NetworkDirection::ClientToServer);
|
||||
// Character controller components
|
||||
app.replicate::<CharacterDrag>()
|
||||
.replicate::<CharacterGravity>()
|
||||
.replicate::<CharacterMovement>()
|
||||
.replicate::<ControllerSettings>()
|
||||
.replicate::<GroundFriction>()
|
||||
.replicate::<Grounding>()
|
||||
.replicate::<GroundingConfig>()
|
||||
.replicate::<GroundingState>()
|
||||
.replicate::<KinematicVelocity>()
|
||||
.replicate::<MoveInput>()
|
||||
.replicate::<MovementSpeedFactor>()
|
||||
.replicate_once::<PlayerCharacterController>()
|
||||
.replicate::<SteppingConfig>();
|
||||
|
||||
app.add_systems(
|
||||
OnEnter(GameState::MapLoading),
|
||||
|mut counter: ResMut<TbMapIdCounter>| counter.reset(),
|
||||
);
|
||||
|
||||
global_observer!(app, set_game_tick);
|
||||
global_observer!(app, components::spawn_gltf_scene_roots);
|
||||
}
|
||||
|
||||
fn set_game_tick(on: On<SetGameTick>, mut tick: ResMut<GameTick>) {
|
||||
tick.0 = on.event().0;
|
||||
}
|
||||
|
||||
#[derive(Serialize, Deserialize)]
|
||||
enum SerVisibility {
|
||||
Inherited,
|
||||
Hidden,
|
||||
Visible,
|
||||
}
|
||||
|
||||
impl From<Visibility> for SerVisibility {
|
||||
fn from(value: Visibility) -> Self {
|
||||
match value {
|
||||
Visibility::Inherited => Self::Inherited,
|
||||
Visibility::Hidden => Self::Hidden,
|
||||
Visibility::Visible => Self::Visible,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<SerVisibility> for Visibility {
|
||||
fn from(value: SerVisibility) -> Self {
|
||||
match value {
|
||||
SerVisibility::Inherited => Self::Inherited,
|
||||
SerVisibility::Hidden => Self::Hidden,
|
||||
SerVisibility::Visible => Self::Visible,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// A global allocator for `TbMapEntityId` values. Should be reset when a map begins loading.
|
||||
#[derive(Resource, Reflect, Default)]
|
||||
#[reflect(Resource)]
|
||||
|
||||
@@ -1,6 +1,10 @@
|
||||
use crate::{
|
||||
GameState, cash::Cash, loading_assets::GameAssets, physics_layers::GameLayer,
|
||||
protocol::TbMapIdCounter, utils::global_observer,
|
||||
GameState,
|
||||
cash::Cash,
|
||||
loading_assets::GameAssets,
|
||||
physics_layers::GameLayer,
|
||||
protocol::{SkipReplicateColliders, TbMapIdCounter},
|
||||
utils::global_observer,
|
||||
};
|
||||
use avian3d::{
|
||||
parry::{na::SVector, shape::SharedShape},
|
||||
@@ -124,18 +128,21 @@ impl EnemySpawn {
|
||||
let mut this_transform = *this_transform;
|
||||
this_transform.translation += Vec3::new(0., 1.5, 0.);
|
||||
|
||||
let position = Position::new(this_transform.translation);
|
||||
let rotation = Rotation(this_transform.rotation);
|
||||
|
||||
let head = this.head.clone();
|
||||
|
||||
world.commands().entity(entity).insert((
|
||||
this_transform,
|
||||
position,
|
||||
rotation,
|
||||
Name::from(format!("enemy [{head}]")),
|
||||
Visibility::default(),
|
||||
RigidBody::Dynamic,
|
||||
RigidBody::Kinematic,
|
||||
Collider::capsule(0.6, 2.),
|
||||
CollisionLayers::new(LayerMask(GameLayer::Npc.to_bits()), LayerMask::ALL),
|
||||
LockedAxes::new().lock_rotation_z().lock_rotation_x(),
|
||||
#[cfg(feature = "server")]
|
||||
lightyear::prelude::Replicate::to_clients(lightyear::prelude::NetworkTarget::All),
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -159,6 +166,7 @@ impl CashSpawn {
|
||||
Cash,
|
||||
Collider::cuboid(2., 3.0, 2.),
|
||||
CollisionLayers::new(GameLayer::CollectibleSensors, LayerMask::ALL),
|
||||
RigidBody::Static,
|
||||
CollisionEventsEnabled,
|
||||
Sensor,
|
||||
));
|
||||
@@ -222,5 +230,8 @@ fn tb_component_setup<C: Component>(
|
||||
) {
|
||||
let id = world.resource_mut::<TbMapIdCounter>().alloc();
|
||||
|
||||
commands.entity(trigger.event().entity).insert_if_new(id);
|
||||
commands
|
||||
.entity(trigger.event().entity)
|
||||
.insert_if_new(id)
|
||||
.insert(SkipReplicateColliders);
|
||||
}
|
||||
|
||||
17
crates/shared/src/tick.rs
Normal file
17
crates/shared/src/tick.rs
Normal file
@@ -0,0 +1,17 @@
|
||||
use crate::GameState;
|
||||
use bevy::prelude::*;
|
||||
|
||||
pub fn plugin(app: &mut App) {
|
||||
app.init_resource::<GameTick>();
|
||||
|
||||
app.add_systems(
|
||||
FixedLast,
|
||||
(|mut tick: ResMut<GameTick>| {
|
||||
tick.0 += 1;
|
||||
})
|
||||
.run_if(in_state(GameState::Playing)),
|
||||
);
|
||||
}
|
||||
|
||||
#[derive(Default, Resource)]
|
||||
pub struct GameTick(pub u64);
|
||||
@@ -1,44 +0,0 @@
|
||||
use bevy::ecs::{
|
||||
entity::Entity,
|
||||
event::{EntityEvent, Event},
|
||||
system::{Commands, EntityCommands},
|
||||
world::{EntityWorldMut, World},
|
||||
};
|
||||
use lightyear::prelude::Disconnected;
|
||||
|
||||
pub trait CommandExt {
|
||||
fn trigger_server<'a, E: Event<Trigger<'a>: Default>>(&mut self, event: E) -> &mut Self;
|
||||
}
|
||||
|
||||
impl<'w, 's> CommandExt for Commands<'w, 's> {
|
||||
fn trigger_server<'a, E: Event<Trigger<'a>: Default>>(&mut self, event: E) -> &mut Self {
|
||||
self.queue(|world: &mut World| {
|
||||
let mut query_state = world.query::<&Disconnected>();
|
||||
if cfg!(feature = "server") || !query_state.query(world).is_empty() {
|
||||
world.trigger(event);
|
||||
}
|
||||
});
|
||||
self
|
||||
}
|
||||
}
|
||||
|
||||
pub trait EntityCommandExt {
|
||||
fn trigger_server<'a, E: EntityEvent<Trigger<'a>: Default>>(
|
||||
&mut self,
|
||||
event: impl FnOnce(Entity) -> E + Send + Sync + 'static,
|
||||
) -> &mut Self;
|
||||
}
|
||||
|
||||
impl<'w> EntityCommandExt for EntityCommands<'w> {
|
||||
fn trigger_server<'a, E: EntityEvent<Trigger<'a>: Default>>(
|
||||
&mut self,
|
||||
event: impl FnOnce(Entity) -> E + Send + Sync + 'static,
|
||||
) -> &mut Self {
|
||||
self.queue(|mut entity: EntityWorldMut| {
|
||||
let mut query_state = entity.world_scope(|world| world.query::<&Disconnected>());
|
||||
if cfg!(feature = "server") || !query_state.query(entity.world()).is_empty() {
|
||||
entity.trigger(event);
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -1,6 +1,5 @@
|
||||
pub mod auto_rotate;
|
||||
pub mod billboards;
|
||||
pub mod commands;
|
||||
pub mod explosions;
|
||||
pub mod observers;
|
||||
pub mod one_shot_force;
|
||||
@@ -8,7 +7,6 @@ pub mod run_conditions;
|
||||
pub mod sprite_3d_animation;
|
||||
pub mod squish_animation;
|
||||
pub mod trail;
|
||||
pub mod triggers;
|
||||
|
||||
use bevy::prelude::*;
|
||||
pub(crate) use observers::global_observer;
|
||||
|
||||
@@ -1,60 +0,0 @@
|
||||
use crate::utils::global_observer;
|
||||
use bevy::{ecs::system::SystemParam, prelude::*};
|
||||
use lightyear::prelude::{AppTriggerExt, Channel, EventSender, NetworkDirection, RemoteEvent};
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(SystemParam)]
|
||||
pub struct ServerMultiTriggerSender<'w, 's, M: Event + Clone> {
|
||||
senders: Query<'w, 's, &'static mut EventSender<M>>,
|
||||
}
|
||||
|
||||
impl<'w, 's, M: Event + Clone> ServerMultiTriggerSender<'w, 's, M> {
|
||||
pub fn server_trigger_targets<C: Channel>(&mut self, event: M) {
|
||||
if cfg!(not(feature = "server")) {
|
||||
return;
|
||||
}
|
||||
|
||||
for mut sender in self.senders.iter_mut() {
|
||||
sender.trigger::<C>(event.clone());
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub trait TriggerAppExt {
|
||||
fn replicate_event<
|
||||
'a,
|
||||
M: Event<Trigger<'a>: Default> + Clone + Serialize + for<'de> Deserialize<'de>,
|
||||
C: Channel,
|
||||
>(
|
||||
&mut self,
|
||||
);
|
||||
}
|
||||
|
||||
impl TriggerAppExt for App {
|
||||
fn replicate_event<
|
||||
'a,
|
||||
M: Event<Trigger<'a>: Default> + Clone + Serialize + for<'de> Deserialize<'de>,
|
||||
C: Channel,
|
||||
>(
|
||||
&mut self,
|
||||
) {
|
||||
self.register_event::<M>()
|
||||
.add_direction(NetworkDirection::ServerToClient);
|
||||
global_observer!(self, replicate_trigger_to_clients::<M, C>);
|
||||
global_observer!(self, remote_to_local_event::<M>);
|
||||
}
|
||||
}
|
||||
|
||||
fn replicate_trigger_to_clients<M: Event + Clone, C: Channel>(
|
||||
on: On<M>,
|
||||
mut sender: ServerMultiTriggerSender<M>,
|
||||
) {
|
||||
sender.server_trigger_targets::<C>(on.event().clone());
|
||||
}
|
||||
|
||||
fn remote_to_local_event<'a, M: Event<Trigger<'a>: Default> + Clone>(
|
||||
trigger: On<RemoteEvent<M>>,
|
||||
mut c: Commands,
|
||||
) {
|
||||
c.trigger(trigger.event().trigger.clone());
|
||||
}
|
||||
Reference in New Issue
Block a user