Skip to content

Commit

Permalink
feat: Client-side prediction multiplayer
Browse files Browse the repository at this point in the history
  • Loading branch information
AudranTourneur committed Jan 26, 2025
1 parent 53589b6 commit c714b4b
Show file tree
Hide file tree
Showing 19 changed files with 317 additions and 275 deletions.
3 changes: 2 additions & 1 deletion client/src/game.rs
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,7 @@ use crate::network::{
establish_authenticated_connection_to_server, init_server_connection,
launch_local_server_system, network_failure_handler, poll_network_messages,
terminate_server_connection, upload_player_inputs_system, CurrentPlayerProfile, TargetServer,
TargetServerState,
TargetServerState, UnacknowledgedInputs,
};

use crate::GameState;
Expand Down Expand Up @@ -93,6 +93,7 @@ pub fn game_plugin(app: &mut App) {
.init_resource::<PlayerTickInputsBuffer>()
.init_resource::<CurrentFrameInputs>()
.init_resource::<SyncTime>()
.init_resource::<UnacknowledgedInputs>()
.insert_resource(Time::<Fixed>::from_hz(TICKS_PER_SECOND as f64))
.add_event::<WorldRenderRequestUpdateEvent>()
.add_event::<PlayerSpawnEvent>()
Expand Down
20 changes: 0 additions & 20 deletions client/src/mob/fox.rs
Original file line number Diff line number Diff line change
Expand Up @@ -279,26 +279,6 @@ impl Default for FoxFeetTargets {
}
}

// pub fn move_fox_towards_player(
// mut fox_transforms: Query<&mut Transform, With<MobRoot>>,
// player_transform: Query<&Transform, (With<CurrentPlayerMarker>, Without<MobRoot>)>,
// ) {
// let player_transform = player_transform.get_single();
// if let Ok(player_transform) = player_transform {
// for mut fox_transform in &mut fox_transforms.iter_mut() {
// let direction = player_transform.translation - fox_transform.translation;
// let direction = direction.normalize();
// let speed = 0.04;
// fox_transform.translation += direction * speed;

// let y_angle_to_player = (player_transform.translation.x - fox_transform.translation.x)
// .atan2(player_transform.translation.z - fox_transform.translation.z);

// fox_transform.rotation = Quat::from_rotation_y(y_angle_to_player);
// }
// }
// }

// TODO: only update the color of the targeted mob, not all mobs sharing the same material
pub fn update_targetted_mob_color(
mut query: Query<(&mut MeshMaterial3d<StandardMaterial>, &MobMarker)>,
Expand Down
17 changes: 14 additions & 3 deletions client/src/network/buffered_client.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,15 +10,17 @@ pub struct PlayerTickInputsBuffer {
pub struct CurrentFrameInputs(pub PlayerFrameInput);

pub trait CurrentFrameInputsExt {
fn reset(&mut self, time: u64);
fn reset(&mut self, time: u64, delta: u64);
}

impl CurrentFrameInputsExt for CurrentFrameInputs {
fn reset(&mut self, time: u64) {
fn reset(&mut self, new_time: u64, new_delta: u64) {
self.0 = PlayerFrameInput {
time_ms: time,
time_ms: new_time,
delta_ms: new_delta,
inputs: HashSet::default(),
camera: Quat::default(),
position: Vec3::default(),
};
}
}
Expand Down Expand Up @@ -48,10 +50,19 @@ impl Default for SyncTime {

pub trait SyncTimeExt {
fn delta(&self) -> u64;
fn advance(&mut self);
}

impl SyncTimeExt for SyncTime {
fn delta(&self) -> u64 {
self.curr_time_ms - self.last_time_ms
}

fn advance(&mut self) {
self.last_time_ms = self.curr_time_ms;
self.curr_time_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;
}
}
14 changes: 13 additions & 1 deletion client/src/network/inputs.rs
Original file line number Diff line number Diff line change
@@ -1,17 +1,29 @@
use bevy::prelude::*;
use bevy_renet::renet::RenetClient;
use shared::messages::ClientToServerMessage;
use shared::messages::{ClientToServerMessage, PlayerFrameInput};

use super::buffered_client::PlayerTickInputsBuffer;
use super::SendGameMessageExtension;

// vector of time_ms values for inputs that have not been acknowledged by the server
#[derive(Debug, Default, Resource)]
pub struct UnacknowledgedInputs(pub Vec<PlayerFrameInput>);

pub fn upload_player_inputs_system(
mut client: ResMut<RenetClient>,
mut inputs: ResMut<PlayerTickInputsBuffer>,
mut unacknowledged_inputs: ResMut<UnacknowledgedInputs>,
) {
let mut frames = vec![];
for input in inputs.buffer.iter() {
frames.push(input.clone());
unacknowledged_inputs.0.push(input.clone());
}
for frame in frames.iter() {
debug!(
"Sending input: {:?} | {:?} | {:?}",
frame.time_ms, frame.inputs, frame.position
);
}
client.send_game_message(ClientToServerMessage::PlayerInputs(frames));
inputs.buffer.clear();
Expand Down
12 changes: 1 addition & 11 deletions client/src/network/setup.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5,14 +5,11 @@ use bevy_renet::netcode::{
use bevy_renet::{renet::RenetClient, RenetClientPlugin};
use rand::Rng;
use shared::messages::mob::MobUpdateEvent;
use shared::players::Player;
use shared::{get_shared_renet_config, GameServerConfig};

use crate::menus::solo::SelectedWorld;
use crate::network::world::update_world_from_network;
use crate::network::CachedChatConversation;
use crate::player::CurrentPlayerMarker;
use crate::world::render_distance::RenderDistance;
use crate::world::time::ClientTime;
use crate::world::WorldRenderRequestUpdateEvent;
use crate::PlayerNameSupplied;
Expand Down Expand Up @@ -78,7 +75,7 @@ impl FromWorld for CurrentPlayerProfile {
pub struct TargetServer {
pub address: Option<SocketAddr>,
pub username: Option<String>,
pub session_token: Option<u128>,
pub session_token: Option<u64>,
pub state: TargetServerState,
}

Expand Down Expand Up @@ -144,9 +141,6 @@ pub fn poll_network_messages(
// client_time: ResMut<ClientTime>,
mut world: ResMut<ClientWorldMap>,
mut ev_render: EventWriter<WorldRenderRequestUpdateEvent>,
mut players: Query<(&mut Transform, &Player), With<Player>>,
current_player_entity: Query<Entity, With<CurrentPlayerMarker>>,
render_distance: Res<RenderDistance>,
mut ev_player_spawn: EventWriter<PlayerSpawnEvent>,
mut ev_mob_update: EventWriter<MobUpdateEvent>,
mut ev_item_stacks_update: EventWriter<ItemStackUpdateEvent>,
Expand All @@ -156,11 +150,7 @@ pub fn poll_network_messages(
update_world_from_network(
&mut client,
&mut world,
// client_time,
&mut ev_render,
&mut players,
current_player_entity,
render_distance,
&mut ev_player_spawn,
&mut ev_mob_update,
&mut ev_item_stacks_update,
Expand Down
47 changes: 4 additions & 43 deletions client/src/network/world.rs
Original file line number Diff line number Diff line change
@@ -1,45 +1,26 @@
use crate::{player::CurrentPlayerMarker, world::ClientChunk};
use crate::world::ClientChunk;
use bevy::prelude::*;
use bevy_renet::renet::RenetClient;
use shared::{
messages::{
mob::MobUpdateEvent, ItemStackUpdateEvent, PlayerSpawnEvent, PlayerUpdateEvent,
ServerToClientMessage,
},
players::Player,
world::{block_to_chunk_coord, chunk_in_radius},
use shared::messages::{
mob::MobUpdateEvent, ItemStackUpdateEvent, PlayerSpawnEvent, PlayerUpdateEvent,
ServerToClientMessage,
};

use crate::world::ClientWorldMap;

use crate::world::RenderDistance;
use crate::world::WorldRenderRequestUpdateEvent;

use super::SendGameMessageExtension;

pub fn update_world_from_network(
client: &mut ResMut<RenetClient>,
world: &mut ResMut<ClientWorldMap>,
// mut client_time: ResMut<ClientTime>,
ev_render: &mut EventWriter<WorldRenderRequestUpdateEvent>,
players: &mut Query<(&mut Transform, &Player), With<Player>>,
current_player_entity: Query<Entity, With<CurrentPlayerMarker>>,
render_distance: Res<RenderDistance>,
ev_player_spawn: &mut EventWriter<PlayerSpawnEvent>,
ev_mob_update: &mut EventWriter<MobUpdateEvent>,
ev_item_stacks_update: &mut EventWriter<ItemStackUpdateEvent>,
ev_player_update: &mut EventWriter<PlayerUpdateEvent>,
) {
let (player_pos, current_player) = players.get(current_player_entity.single()).unwrap();
let current_player_id = current_player.id;

let player_pos = IVec3::new(
block_to_chunk_coord(player_pos.translation.x as i32),
0,
block_to_chunk_coord(player_pos.translation.z as i32),
);
let r = render_distance.distance as i32;

while let Ok(msg) = client.receive_game_message() {
// truncate the message to 1000 characters
// let debug_msg = format!("{:?}", msg).chars().take(1000).collect::<String>();
Expand All @@ -52,11 +33,6 @@ pub fn update_world_from_network(
);

for (pos, chunk) in world_update.new_map {
// If the chunk is not in render distance range or is empty, do not consider it
if !chunk_in_radius(&player_pos, &pos, r) || chunk.map.is_empty() {
continue;
}

let chunk = ClientChunk {
map: chunk.map,
entity: {
Expand All @@ -72,21 +48,6 @@ pub fn update_world_from_network(
ev_render.send(WorldRenderRequestUpdateEvent::ChunkToReload(pos));
}

debug!("Player pos {:?}", world_update.player_positions);

for (mut transform, player) in players.iter_mut() {
debug!("Player found: {} at {:?}", player.name, transform);
if player.id == current_player_id {
continue;
}
let vec3 = world_update.player_positions.get(&player.id);
if let Some(vec3) = vec3 {
let new_transform = Transform::from_translation(*vec3);
*transform = new_transform;
debug!("Set transform {} => {:?}", player.id, new_transform);
}
}

for mob in world_update.mobs {
debug!("ServerMob received: {:?}", mob);
ev_mob_update.send(MobUpdateEvent { mob });
Expand Down
28 changes: 15 additions & 13 deletions client/src/player/controller.rs
Original file line number Diff line number Diff line change
Expand Up @@ -25,14 +25,11 @@ pub fn pre_input_update_system(
mut tick_buffer: ResMut<PlayerTickInputsBuffer>,
mut sync_time: ResMut<SyncTime>,
) {
sync_time.curr_time_ms = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap()
.as_millis() as u64;

let inputs = frame_inputs.0.clone();
tick_buffer.buffer.push(inputs);
frame_inputs.reset(sync_time.curr_time_ms);
sync_time.advance();

let inputs_of_last_frame = frame_inputs.0.clone();
tick_buffer.buffer.push(inputs_of_last_frame);
frame_inputs.reset(sync_time.curr_time_ms, sync_time.delta());
}

pub fn player_movement_system(
Expand All @@ -44,13 +41,12 @@ pub fn player_movement_system(
Res<KeyMap>,
ResMut<CurrentFrameInputs>,
),
time: Res<SyncTime>,
world_map: Res<ClientWorldMap>,
) {
let mut player_query = queries;
let (keyboard_input, ui_mode, key_map, mut frame_inputs) = resources;

if time.delta() == 0 {
if frame_inputs.0.delta_ms == 0 {
return;
}

Expand All @@ -70,7 +66,7 @@ pub fn player_movement_system(
if *ui_mode == UIMode::Closed
&& is_action_just_pressed(GameAction::ToggleFlyMode, &keyboard_input, &key_map)
{
player.toggle_fly_mode();
frame_inputs.0.inputs.insert(NetworkAction::ToggleFlyMode);
}

if is_action_pressed(GameAction::MoveBackward, &keyboard_input, &key_map) {
Expand All @@ -93,11 +89,17 @@ pub fn player_movement_system(
}

let world_clone = world_map.clone();
let frame_inputs = frame_inputs.0.clone();

simulate_player_movement(&mut player, &world_clone, &frame_inputs, 17);
simulate_player_movement(&mut player, &world_clone, &frame_inputs.0);

frame_inputs.0.position = player.position;

player_transform.translation = player.position;

// debug!(
// "At t={}, player position: {:?}",
// frame_inputs.0.time_ms, player.position
// );
}

pub fn first_and_third_person_view_system(
Expand Down
4 changes: 2 additions & 2 deletions client/src/player/mod.rs
Original file line number Diff line number Diff line change
@@ -1,9 +1,9 @@
mod controller;
mod interactions;
mod labels;
mod spawn;
mod update;

pub use controller::*;
pub use interactions::*;
pub use labels::*;
pub use spawn::*;
pub use update::*;
Loading

0 comments on commit c714b4b

Please sign in to comment.