太快了!

控制恒定的更新速度是游戏中常见的需求,这一节我们将介绍一种简单的控制更新频率的方法。

玩过 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;
        }
    }
}

放进事件循环中也是同理,快去试试吧!