mirror of
https://gitlab.com/veloren/veloren
synced 2026-08-16 16:26:07 -04:00
Redesigned clock logic to track real and game time independently
This commit is contained in:
parent
8ff8641f6a
commit
3990d336a1
11 changed files with 71 additions and 32 deletions
|
|
@ -89,7 +89,7 @@ fn main() {
|
|||
client.send_chat(msg)
|
||||
}
|
||||
|
||||
let events = match client.tick(comp::ControllerInputs::default(), clock.dt()) {
|
||||
let events = match client.tick(comp::ControllerInputs::default(), clock.game_dt()) {
|
||||
Ok(events) => events,
|
||||
Err(err) => {
|
||||
error!("Error: {:?}", err);
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ impl BotClient {
|
|||
for (username, client) in self.bot_clients.iter_mut() {
|
||||
trace!(?username, "tick");
|
||||
let _msgs: Result<Vec<veloren_client::Event>, veloren_client::Error> =
|
||||
client.tick(comp::ControllerInputs::default(), self.clock.dt());
|
||||
client.tick(comp::ControllerInputs::default(), self.clock.game_dt());
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -135,7 +135,7 @@ fn run_client(
|
|||
|
||||
let mut tick = |client: &mut Client| -> Result<(), veloren_client::Error> {
|
||||
clock.tick();
|
||||
client.tick_network(clock.dt())?;
|
||||
client.tick_network(clock.real_dt())?;
|
||||
Ok(())
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -3596,7 +3596,7 @@ mod tests {
|
|||
|
||||
//tick
|
||||
let events_result: Result<Vec<Event>, Error> =
|
||||
client.tick(ControllerInputs::default(), clock.dt());
|
||||
client.tick(ControllerInputs::default(), clock.game_dt());
|
||||
|
||||
//chat functionality
|
||||
client.send_chat("foobar".to_string());
|
||||
|
|
|
|||
|
|
@ -9,6 +9,10 @@ pub struct Clock {
|
|||
target_dt: Duration,
|
||||
|
||||
// Working state
|
||||
/// The amount of real time that has passed on the clock
|
||||
real_time: Duration,
|
||||
/// The amount of game time that has passed on the clock
|
||||
game_time: Duration,
|
||||
/// The last time the clock was ticked
|
||||
last_tick: Instant,
|
||||
/// The last time we started performing work
|
||||
|
|
@ -16,7 +20,6 @@ pub struct Clock {
|
|||
/// The number of ticks that have elapsed so far
|
||||
tick: u64,
|
||||
|
||||
// Outputs
|
||||
/// The average time between ticks, seconds
|
||||
average_dt: f64,
|
||||
/// The average amount of time within each tick in which we're busy (i.e:
|
||||
|
|
@ -25,7 +28,9 @@ pub struct Clock {
|
|||
/// The average amount of variance between ticks
|
||||
average_variance: f64,
|
||||
/// The time that passed between the last tick, and the tick before it
|
||||
last_dt: f64,
|
||||
last_real_dt: f64,
|
||||
/// The dt to be used for the next game tick, in game time.
|
||||
last_game_dt: f64,
|
||||
}
|
||||
|
||||
pub struct ClockStats {
|
||||
|
|
@ -40,13 +45,20 @@ pub struct ClockStats {
|
|||
}
|
||||
|
||||
/// The weighting used to calculate averages. Must be > 0.0. 1.0 = no averaging.
|
||||
const SMOOTH_WEIGHT: f64 = 0.025;
|
||||
const SMOOTH_WEIGHT: f64 = 0.05;
|
||||
/// The proportion of the difference between real and game time that gets
|
||||
/// applied each tick to keep the two aligned.
|
||||
const NUDGE_RATE: f64 = 0.05;
|
||||
/// The maximum dt that the game should ever run at.
|
||||
const MAX_GAME_DT: f64 = 1.0 / 5.0;
|
||||
|
||||
impl Clock {
|
||||
pub fn new(target_dt: Duration) -> Self {
|
||||
Self {
|
||||
target_dt,
|
||||
|
||||
real_time: Duration::ZERO,
|
||||
game_time: Duration::ZERO,
|
||||
last_tick: Instant::now(),
|
||||
last_work: Instant::now(),
|
||||
tick: 0,
|
||||
|
|
@ -54,14 +66,16 @@ impl Clock {
|
|||
average_dt: target_dt.as_secs_f64(),
|
||||
average_busy: target_dt.as_secs_f64(),
|
||||
average_variance: 0.0,
|
||||
last_dt: target_dt.as_secs_f64(),
|
||||
last_real_dt: target_dt.as_secs_f64(),
|
||||
last_game_dt: target_dt.as_secs_f64(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn set_target_dt(&mut self, target_dt: Duration) {
|
||||
if target_dt != self.target_dt {
|
||||
self.target_dt = target_dt;
|
||||
// The target dt has changed, throw out the existing state to avoid problems
|
||||
|
||||
// The target dt has changed, throw out the existing stats to avoid problems
|
||||
self.average_dt = target_dt.as_secs_f64();
|
||||
self.average_busy = target_dt.as_secs_f64();
|
||||
self.average_variance = 0.0;
|
||||
|
|
@ -76,9 +90,9 @@ impl Clock {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn dt(&self) -> Duration { Duration::from_secs_f64(self.last_dt) }
|
||||
pub fn real_dt(&self) -> Duration { Duration::from_secs_f64(self.last_real_dt) }
|
||||
|
||||
pub fn get_stable_dt(&self) -> Duration { Duration::from_secs_f64(self.average_dt) }
|
||||
pub fn game_dt(&self) -> Duration { Duration::from_secs_f64(self.last_game_dt) }
|
||||
|
||||
pub fn tick(&mut self) {
|
||||
span!(_guard, "tick", "Clock::tick");
|
||||
|
|
@ -86,15 +100,19 @@ impl Clock {
|
|||
|
||||
// Give the tick thread realtime priority to minimise stuttering. Don't do this
|
||||
// all the time to avoid upsetting the scheduler.
|
||||
if self.tick % 30 == 0 {
|
||||
if self.tick == 0
|
||||
/* .is_multiple_of(30) */
|
||||
{
|
||||
use thread_priority::*;
|
||||
// Try to target a tick period that's consistent with our current FPS (a low but
|
||||
// consistent framerate is a better outcome than one that's faster on paper but
|
||||
// is bouncing around all over the place).
|
||||
/*
|
||||
let stable_dt = self.average_busy
|
||||
// Don't try to schedule for a tick rate that's higher than our target, even if we
|
||||
// could achieve it.
|
||||
.max(self.target_dt.as_secs_f64());
|
||||
*/
|
||||
_ = std::thread::current().set_priority_and_policy(
|
||||
ThreadSchedulePolicy::Realtime(RealtimeThreadSchedulePolicy::Fifo),
|
||||
// ThreadSchedulePolicy::Realtime(RealtimeThreadSchedulePolicy::Deadline),
|
||||
|
|
@ -112,10 +130,13 @@ impl Clock {
|
|||
let this_tick = Instant::now();
|
||||
|
||||
// Calculate average metrics
|
||||
|
||||
let busy_time = self.last_work.elapsed().as_secs_f64();
|
||||
self.average_busy = Lerp::lerp(self.average_busy, busy_time, SMOOTH_WEIGHT);
|
||||
|
||||
let tick_time = self.last_tick.elapsed().as_secs_f64();
|
||||
self.average_dt = Lerp::lerp(self.average_dt, tick_time, SMOOTH_WEIGHT);
|
||||
|
||||
let variance = (tick_time - self.average_dt).abs();
|
||||
self.average_variance = Lerp::lerp(self.average_variance, variance, SMOOTH_WEIGHT);
|
||||
|
||||
|
|
@ -129,9 +150,28 @@ impl Clock {
|
|||
spin_sleep::sleep(sleep_dur);
|
||||
}
|
||||
|
||||
// Update clock state
|
||||
|
||||
self.last_tick = this_tick;
|
||||
self.last_work = Instant::now();
|
||||
self.last_dt = tick_time;
|
||||
|
||||
// Progress real and game time
|
||||
self.real_time += Duration::from_secs_f64(self.last_real_dt);
|
||||
self.game_time += Duration::from_secs_f64(self.last_game_dt);
|
||||
|
||||
// Calculate the deltas for both real and game clocks. The real clock is
|
||||
// absolute: we can't alter the progression of time. However, we can
|
||||
// alter the game clock and nudge is toward real time. The reason we
|
||||
// don't want to keep the two *exactly* in time is that a lag spike on a
|
||||
// single tick would cause a corresponding jump in dt on the next tick, which
|
||||
// might produce strange results for any dt-dependent gameplay systems.
|
||||
// Instead, we gradually nudge the game time back toward real time over
|
||||
// several ticks.
|
||||
self.last_real_dt = tick_time;
|
||||
self.last_game_dt = (self.average_dt
|
||||
+ (self.real_time.as_secs_f64() - self.game_time.as_secs_f64()) * NUDGE_RATE)
|
||||
.min(MAX_GAME_DT);
|
||||
|
||||
self.tick += 1;
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -321,7 +321,7 @@ fn server_loop(
|
|||
}
|
||||
|
||||
let events = server
|
||||
.tick(Input::default(), clock.dt())
|
||||
.tick(Input::default(), clock.game_dt())
|
||||
.expect("Failed to tick server");
|
||||
|
||||
for event in events {
|
||||
|
|
|
|||
|
|
@ -253,10 +253,10 @@ impl PlayState for CharSelectionState {
|
|||
// Tick the client (currently only to keep the connection alive).
|
||||
let localized_strings = &global_state.i18n.read();
|
||||
|
||||
let res = self
|
||||
.client
|
||||
.borrow_mut()
|
||||
.tick(comp::ControllerInputs::default(), global_state.clock.dt());
|
||||
let res = self.client.borrow_mut().tick(
|
||||
comp::ControllerInputs::default(),
|
||||
global_state.clock.game_dt(),
|
||||
);
|
||||
match res {
|
||||
Ok(events) => {
|
||||
let mut join_metadata = None;
|
||||
|
|
|
|||
|
|
@ -274,7 +274,10 @@ impl PlayState for MainMenuState {
|
|||
|
||||
// Tick the client to keep the connection alive if we are waiting on pipelines
|
||||
if let InitState::Pipeline(client, _) = &mut self.init {
|
||||
match client.tick(comp::ControllerInputs::default(), global_state.clock.dt()) {
|
||||
match client.tick(
|
||||
comp::ControllerInputs::default(),
|
||||
global_state.clock.game_dt(),
|
||||
) {
|
||||
Ok(events) => {
|
||||
for event in events {
|
||||
match event {
|
||||
|
|
@ -399,7 +402,7 @@ impl PlayState for MainMenuState {
|
|||
// Maintain the UI.
|
||||
for event in self
|
||||
.main_menu_ui
|
||||
.maintain(global_state, global_state.clock.dt())
|
||||
.maintain(global_state, global_state.clock.real_dt())
|
||||
{
|
||||
match event {
|
||||
MainMenuEvent::LoginAttempt {
|
||||
|
|
|
|||
|
|
@ -179,10 +179,10 @@ impl PlayState for ServerInfoState {
|
|||
}
|
||||
|
||||
if let Some(char_select) = &mut self.char_select
|
||||
&& let Err(err) = char_select
|
||||
.client()
|
||||
.borrow_mut()
|
||||
.tick(comp::ControllerInputs::default(), global_state.clock.dt())
|
||||
&& let Err(err) = char_select.client().borrow_mut().tick(
|
||||
comp::ControllerInputs::default(),
|
||||
global_state.clock.game_dt(),
|
||||
)
|
||||
{
|
||||
error!(?err, "[server_info] Failed to tick the client");
|
||||
global_state.info_message =
|
||||
|
|
|
|||
|
|
@ -608,7 +608,7 @@ impl PlayState for SessionState {
|
|||
let client = self.client.borrow();
|
||||
let player_entity = client.entity();
|
||||
|
||||
let dt = global_state.clock.get_stable_dt().as_secs_f32();
|
||||
let dt = global_state.clock.real_dt().as_secs_f32();
|
||||
|
||||
#[cfg(feature = "discord")]
|
||||
if global_state.discord.is_active()
|
||||
|
|
@ -1646,11 +1646,7 @@ impl PlayState for SessionState {
|
|||
// Runs if either in a multiplayer server or the singleplayer server is unpaused
|
||||
if !global_state.paused() {
|
||||
// Perform an in-game tick.
|
||||
match self.tick(
|
||||
global_state.clock.get_stable_dt(),
|
||||
global_state,
|
||||
&mut outcomes,
|
||||
) {
|
||||
match self.tick(global_state.clock.game_dt(), global_state, &mut outcomes) {
|
||||
Ok(TickAction::Continue) => {}, // Do nothing
|
||||
Ok(TickAction::Disconnect) => return PlayStateResult::Pop, // Go to main menu
|
||||
Err(Error::ClientError(error)) => {
|
||||
|
|
@ -1753,7 +1749,7 @@ impl PlayState for SessionState {
|
|||
global_state,
|
||||
&debug_info,
|
||||
self.scene.camera(),
|
||||
global_state.clock.get_stable_dt(),
|
||||
global_state.clock.real_dt(),
|
||||
HudInfo {
|
||||
is_aiming,
|
||||
active_mine_tool,
|
||||
|
|
|
|||
|
|
@ -217,7 +217,7 @@ fn run_server(mut server: Server, stop_server_r: Receiver<()>, paused: Arc<Atomi
|
|||
}
|
||||
|
||||
let events = server
|
||||
.tick(Input::default(), clock.dt())
|
||||
.tick(Input::default(), clock.game_dt())
|
||||
.expect("Failed to tick server!");
|
||||
|
||||
for event in events {
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue