太快了!
控制恒定的更新速度是游戏中常见的需求,这一节我们将介绍一种简单的控制更新频率的方法。
玩过 Minecraft™ 吗?Minecraft中的游戏逻辑循环是以20次/秒的频率进行的,称为 20 tps(ticks per second),而渲染循环却以不同的频率(常见的为60个画面(称为 帧)每秒,称为60 fps(frames per second))刷新。较低的逻辑更新频率可以减少计算的负担,而更高的渲染频率则可以使游戏显得更加流畅。如果更新的速度不均匀,则可能使游戏显示的画面有违和的跳动。那么,我们应该如何控制更新的频率呢?
考虑这样一个问题,我们希望一个循环以60次每秒的速度进行更新,那么每次循环的平均用时应当为 ,现在有两种情况:
- 这次循环的用时比少,那我们自然可以等到时间为,时再开始下一次循环。其中是上一次循环结束的时间戳
- 如果这次循环用时比多呢?不妨假设这一次循环所耗的时间为我们整整缺少了次循环!一种简单的解决方案是,当场补上这次循环的更新并且直接进入下一个循环,而补上的循环的更新间隔。这样可以一定程度上保证更新的正确性,并且如果后续更新没有再消耗大于的时间的话也能顺利地接上
现在,我们来实现一下
use std::time::{Duration, Instant};
const TPS: f64 = 60.0;
const TIMEOUT: f32 = 3.0;
fn update(dt: Duration) {
println!("delta tau: {:?}", dt);
}
fn main() {
let tau = Duration::from_secs_f64(1.0 / TPS);
let timeout = Duration::from_secs_f32(TIMEOUT);
let begin = Instant::now();
let mut now = Instant::now();
let mut lag = Duration::ZERO;
loop {
let dt = now.elapsed();
now = Instant::now();
lag += dt;
while lag > tau {
update(tau);
lag -= tau;
}
if begin.elapsed() > timeout {
break;
}
}
}
是不是非常简单呢?唯一值得注意的一点是我们并不用sleep来控制间隔,而不终止循环的运行。因此我们可以有不同的频率同时运行。而主循环内部就是运行的最快的部分了。
use std::time::{Duration, Instant};
const TPS_0: f64 = 60.0;
const TPS_1: f64 = 20.0;
const TIMEOUT: f32 = 3.0;
fn update(dt: Duration) {
println!("60 tps delta tau: {:?}", dt);
}
fn slow_update(dt: Duration) {
println!("20 tps delta tau: {:?}", dt);
}
fn fast_update(dt: Duration) {
// 以最快可能速度运行的逻辑,通常不会使用
}
fn main() {
let tau_0 = Duration::from_secs_f64(1.0 / TPS_0);
let tau_1 = Duration::from_secs_f64(1.0 / TPS_1);
let timeout = Duration::from_secs_f32(TIMEOUT);
let begin = Instant::now();
let mut now = Instant::now();
let mut lag_0 = Duration::ZERO;
let mut lag_1 = Duration::ZERO;
loop {
let dt = now.elapsed();
now = Instant::now();
lag_0 += dt;
lag_1 += dt;
while lag_0 > tau_0 {
update(tau_0);
lag_0 -= tau_0;
}
while lag_1 > tau_1 {
slow_update(tau_1);
lag_1 -= tau_1;
}
fast_update(dt);
// 为了使其能够在 playground 中运行,加上了超时限制。在实际应用中并不需要这几行
if begin.elapsed() > timeout {
break;
}
}
}
放进 EventLoop::run 中也是同理,快去试试吧!