Skip to content

Commit

Permalink
Implement a PID regulator to smoothly adjust the ClientTick
Browse files Browse the repository at this point in the history
We use a gaussian filter to filter out network jitter.

This change also makes the client tick continuous without any discontinuations.
  • Loading branch information
hasenbanck committed Dec 29, 2024
1 parent 72fca1d commit 68e3746
Show file tree
Hide file tree
Showing 4 changed files with 104 additions and 10 deletions.
4 changes: 2 additions & 2 deletions korangar/src/main.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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 });
Expand Down
91 changes: 87 additions & 4 deletions korangar/src/system/timer.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use std::collections::VecDeque;
use std::time::Instant;

use chrono::prelude::*;
Expand All @@ -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 {
Expand All @@ -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")]
Expand Down
7 changes: 6 additions & 1 deletion korangar_networking/src/event.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,5 @@
use std::time::Instant;

use ragnarok_packets::*;

use crate::hotkey::HotkeyState;
Expand Down Expand Up @@ -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,
Expand Down
12 changes: 9 additions & 3 deletions korangar_networking/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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)
})?;
Expand Down Expand Up @@ -997,7 +1000,10 @@ where
})?;
packet_handler.register_noop::<Packet8302>()?;
packet_handler.register_noop::<Packet0b18>()?;
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 {
Expand Down

0 comments on commit 68e3746

Please sign in to comment.