From 68e37469e3a410d522c7597770211b585fb9edbc Mon Sep 17 00:00:00 2001 From: Nils Hasenbanck Date: Sun, 29 Dec 2024 18:25:14 +0100 Subject: [PATCH] Implement a PID regulator to smoothly adjust the ClientTick We use a gaussian filter to filter out network jitter. This change also makes the client tick continuous without any discontinuations. --- korangar/src/main.rs | 4 +- korangar/src/system/timer.rs | 91 ++++++++++++++++++++++++++++++-- korangar_networking/src/event.rs | 7 ++- korangar_networking/src/lib.rs | 12 +++-- 4 files changed, 104 insertions(+), 10 deletions(-) diff --git a/korangar/src/main.rs b/korangar/src/main.rs index 37d16b77..fc5511d9 100644 --- a/korangar/src/main.rs +++ b/korangar/src/main.rs @@ -1094,8 +1094,8 @@ impl Client { self.point_light_manager.clear(); let _ = self.networking_system.map_loaded(); } - NetworkEvent::UpdateClientTick(client_tick) => { - self.game_timer.set_client_tick(client_tick); + NetworkEvent::UpdateClientTick { client_tick, received_at } => { + self.game_timer.set_client_tick(client_tick, received_at); } NetworkEvent::ChatMessage { text, color } => { self.chat_messages.push(ChatMessage { text, color }); diff --git a/korangar/src/system/timer.rs b/korangar/src/system/timer.rs index 614d0ca5..8feffc3c 100644 --- a/korangar/src/system/timer.rs +++ b/korangar/src/system/timer.rs @@ -1,3 +1,4 @@ +use std::collections::VecDeque; use std::time::Instant; use chrono::prelude::*; @@ -12,10 +13,24 @@ pub struct GameTimer { animation_timer: f32, day_timer: f32, last_client_tick: Instant, + first_tick_received: bool, base_client_tick: u32, + frequency: f64, + last_update: Instant, + last_error: f64, + integral_error: f64, + error_history: VecDeque<(Instant, f64)>, } const TIME_FACTOR: f32 = 1000.0; +// PID constants +const KP: f64 = 0.0005; +const KI: f64 = 0.00005; +const KD: f64 = 0.00005; +// Gaussian filter constants +const GAUSSIAN_SIGMA: f64 = 4.0; +const GAUSSIAN_DENOMINATOR: f64 = 2.0 * GAUSSIAN_SIGMA * GAUSSIAN_SIGMA; +const GAUSSIAN_WINDOW_SIZE: usize = 15; impl GameTimer { pub fn new() -> Self { @@ -31,18 +46,86 @@ impl GameTimer { animation_timer: Default::default(), day_timer, last_client_tick: Instant::now(), + first_tick_received: false, base_client_tick: 0, + frequency: 0.0, + last_update: Instant::now(), + last_error: 0.0, + integral_error: 0.0, + error_history: VecDeque::with_capacity(GAUSSIAN_WINDOW_SIZE), } } - pub fn set_client_tick(&mut self, client_tick: ClientTick) { - self.last_client_tick = Instant::now(); - self.base_client_tick = client_tick.0; + fn gaussian_filter(&self, packet_time: Instant) -> f64 { + if self.error_history.is_empty() { + return 0.0; + } + + let mut weighted_sum = 0.0; + let mut weight_sum = 0.0; + + for (time, error) in &self.error_history { + let dt = packet_time.duration_since(*time).as_secs_f64(); + let weight = (-dt.powi(2) / GAUSSIAN_DENOMINATOR).exp(); + + weighted_sum += error * weight; + weight_sum += weight; + } + + if weight_sum > 0.0 { + weighted_sum / weight_sum + } else { + 0.0 + } + } + + /// Uses a simple PID regulator that uses a gaussian filter to be a bit more + /// resistant against network jitter to synchronize the client side tick and + /// the server tick. + pub fn set_client_tick(&mut self, server_tick: ClientTick, packet_receive_time: Instant) { + if !self.first_tick_received { + self.first_tick_received = true; + self.base_client_tick = server_tick.0; + self.last_client_tick = packet_receive_time; + self.last_update = packet_receive_time; + self.last_error = 0.0; + self.integral_error = 0.0; + return; + } + + let elapsed = packet_receive_time.duration_since(self.last_client_tick).as_secs_f64(); + let adjustment = self.frequency * elapsed; + let tick_at_receive = self.base_client_tick as f64 + (elapsed * 1000.0) + adjustment; + + let error = server_tick.0 as f64 - tick_at_receive; + + self.error_history.push_back((packet_receive_time, error)); + while self.error_history.len() > GAUSSIAN_WINDOW_SIZE { + self.error_history.pop_front(); + } + + let filtered_error = self.gaussian_filter(packet_receive_time); + + let dt = packet_receive_time.duration_since(self.last_update).as_secs_f64(); + + self.integral_error = (self.integral_error + filtered_error * dt).clamp(-10.0, 10.0); + + let derivative = (filtered_error - self.last_error) / dt; + + self.frequency = (KP * filtered_error + KI * self.integral_error + KD * derivative).clamp(-0.1, 0.1); + + self.last_error = filtered_error; + self.base_client_tick = server_tick.0; + self.last_client_tick = packet_receive_time; + self.last_update = packet_receive_time; } #[cfg_attr(feature = "debug", korangar_debug::profile)] pub fn get_client_tick(&self) -> ClientTick { - ClientTick(self.last_client_tick.elapsed().as_millis() as u32 + self.base_client_tick) + let elapsed = self.last_client_tick.elapsed().as_secs_f64(); + let adjustment = self.frequency * elapsed; + let tick = self.base_client_tick as f64 + (elapsed * 1000.0) + adjustment; + ClientTick(tick as u32) } #[cfg(feature = "debug")] diff --git a/korangar_networking/src/event.rs b/korangar_networking/src/event.rs index c5e9a48d..6f0e31b6 100644 --- a/korangar_networking/src/event.rs +++ b/korangar_networking/src/event.rs @@ -1,3 +1,5 @@ +use std::time::Instant; + use ragnarok_packets::*; use crate::hotkey::HotkeyState; @@ -82,7 +84,10 @@ pub enum NetworkEvent { /// Update the client side [`tick /// counter`](crate::system::GameTimer::base_client_tick) to keep server and /// client synchronized. - UpdateClientTick(ClientTick), + UpdateClientTick { + client_tick: ClientTick, + received_at: Instant, + }, /// New chat message for the client. ChatMessage { text: String, diff --git a/korangar_networking/src/lib.rs b/korangar_networking/src/lib.rs index 206c47b8..b8ab10da 100644 --- a/korangar_networking/src/lib.rs +++ b/korangar_networking/src/lib.rs @@ -8,7 +8,7 @@ mod server; use std::cell::RefCell; use std::net::{IpAddr, SocketAddr}; use std::rc::Rc; -use std::time::Duration; +use std::time::{Duration, Instant}; use event::{ CharacterServerDisconnectedEvent, DisconnectedEvent, LoginServerDisconnectedEvent, MapServerDisconnectedEvent, NetworkEventList, @@ -946,7 +946,10 @@ where index: packet.index, amount: packet.amount, })?; - packet_handler.register(|packet: ServerTickPacket| NetworkEvent::UpdateClientTick(packet.client_tick))?; + packet_handler.register(|packet: ServerTickPacket| NetworkEvent::UpdateClientTick { + client_tick: packet.client_tick, + received_at: Instant::now(), + })?; packet_handler.register(|packet: RequestPlayerDetailsSuccessPacket| { NetworkEvent::UpdateEntityDetails(EntityId(packet.character_id.0), packet.name) })?; @@ -997,7 +1000,10 @@ where })?; packet_handler.register_noop::()?; packet_handler.register_noop::()?; - packet_handler.register(|packet: MapServerLoginSuccessPacket| NetworkEvent::UpdateClientTick(packet.client_tick))?; + packet_handler.register(|packet: MapServerLoginSuccessPacket| NetworkEvent::UpdateClientTick { + client_tick: packet.client_tick, + received_at: Instant::now(), + })?; packet_handler.register(|packet: RestartResponsePacket| match packet.result { RestartResponseStatus::Ok => NetworkEvent::LoggedOut, RestartResponseStatus::Nothing => NetworkEvent::ChatMessage {