【LWJGL官方教程】Game loops 游戏循环

来源:互联网 发布:淘宝价格走势app 编辑:程序博客网 时间:2024/06/04 19:20

原文:https://github.com/SilverTiger/lwjgl3-tutorial/wiki/Game-loops
译注:翻译仅供参考,请以原文为准。代码请看原文中的链接。括号里的内容一般也是译注,供理解参考用。
PS:Markdown还挺好用的,给CSDN点个赞。

How does a game work? 一个游戏如何工作?

从编程角度来讲,一个游戏可以用一个简单的方法来描述:

public void startGame() {    init();    gameLoop();    dispose();}

这就是游戏全部必须要做的事,init()包含初始化的全部内容,比如创建窗口、读取资源。dispose()包含释放游戏资源的全部代码。

A simple game loop 一个简单的游戏循环

游戏循环的关键部分是处理输入、更新游戏逻辑、渲染游戏。

public void gameLoop() {    while (running) {        input();        update();        render();    }}

这样的游戏循环会耗尽全部的CPU,因为它运行得太快了。通常你并不需要它这样,因此需要让循环适当停歇。

public void gameLoop() {    long sleepTime = 1000L / 60L;    while (running) {        input();        update();        render();        sleep(sleepTime);    }}

在这个例子里,加入了16毫秒的休眠时间,游戏每秒将只更新62.5次。
虽然这已经比毫无停歇要好一些了,但是其实你需要有一个可变的休眠时间以保证游戏有稳定的FPS。为此,我们应该看看时间运算教程。
根据时间计数器的选择,有两种形式的游戏循环:
- 可变时步游戏循环
- 固定时步游戏循环

Variable timestep 可变时步

用可变时步,你不需要关心你的游戏是不是跑得太快或太慢了,因为会使用时间增量(delta time)来更新游戏。(意思是,如果跑太慢了,时间增量超长,那么就一次更新许多内容,太快也是同理)
在这种循环,我们首先就得拿到时间增量

float delta = timer.getDelta();

时间增量用来更新游戏。所以给update()方法加上参数。

public void update(float delta) {    /* do updates */}

之后也不需要再加什么了,最后是这样的:

public void gameLoop() {    while (running) {        float delta = timer.getDelta();        input();        update(delta);        render();        window.update();    }}

你可能想问,为啥不休眠了。因为你可以让GLFW去激活垂直同步,这样你不需要再关心休眠的问题了。
但是如果我们在循环里加入休眠,也不需要改太多东西。

public void gameLoop() {    long targetTime = 1000L / targetFPS;    while (running) {        /* Note that you have to multiply by 1000 to get milliseconds */long startTime = timer.getTime() * 1000;        float delta = timer.getDelta();        input();        update(delta);        render();        window.update();        /* Same as above, multiply time in seconds by 1000 */long endTime = timer.getTime() * 1000;        sleep(startTime + targetTime - endTime);    }}

但是这个循环有一个瑕疵:你的更新受限于你的帧率,在简单的游戏里这还OK,但是如果是想做一个逼真的物理情况模拟,这就不是你想要的了。(意思是,假如FPS很低的话,那更新次数也很低,时间增量也超长,如果模拟铁球下落,会看到铁球在空中是断断续续的,一次还落超长距离……效果很糟糕。大部分情况下虽然不会这么明显,但是也会让玩家有一种违和感。)

Fixed timestep 固定时步

固定时步的话,你的游戏循环不能再受限于帧率了,而是基于时间,比起可变时步,它更具有确定性。
固定时步除了时间增量以外还需要有更多的变量,需要一个累加器(accumulator)记录经过的时间,一个表示更新之间的时间应该是多少的间隔量(interval),还需要有一个aplha值来充当插值。

float delta;float accumulator = 0f;float interval = 1f / 30f;float alpha;

在这个例子里,我们希望每秒有30次更新,所以间隔量大概是33.33毫秒。
变量有了,开始循环。跟可变循环一样,先要拿到时间增量,但在此循环里,还要把增量加在累加器上。

delta = timer.getDelta();accumulator += delta;

接着处理输入。再之后要检查经过的时间是否应该做更新操作了。(累加器时间超过设定的间隔量,按间隔量来做更新)

while (accumulator >= interval) {    update(interval);    accumulator -= interval;}

之后我们其实还剩下一些多出来的时间,举个例子。
比如现在游戏已经开始了48毫秒,但是我们下一次更新应该是在66.67毫秒的时候,因为我们说好是每33.33毫秒更新一次的(之前设定的间隔量)。这时累加器的值应该是48-33.33=14.67毫秒。
所以我们当前的游戏状态其实是14.67毫秒以前的游戏状态,现在我们可以再渲染一次相同的屏幕内容,但是那样的话我们的游戏看起来就像是以30FPS来渲染而不是我们预期的那样,帧在做重复无意义的废渲染。为了能表现出两次更新间的某种状态,我们引入了alpha这个插值。(意思是,我们虽然希望一秒更新30次,但是却期望能更精确地连续渲染画面,而不是离散地去重复渲染这30次更新时的画面,那样就算一秒渲染了100次,其实也是在反复重复地渲染这30个更新瞬间的画面而已。)

alpha = accumulator / interval;

在上面说的例子里,alpha值应该是14.67/33.33=0.44,所以我们距下次更新大概行进到了44%的阶段。为了渲染,我们需要保持上一次和本次的状态插值,具体在另一篇教程里再说。
最后,固定时步循环应该是这样的:

public void gameLoop() {    float delta;    float accumulator = 0f;    float interval = 1f / targetUPS;    float alpha;    while (running) {        delta = timer.getDelta();        accumulator += delta;        input();        while (accumulator >= interval) {            update();            accumulator -= interval;        }        alpha = accumulator / interval;        render(alpha);        window.update();    }}

关于udpate方法,有一点:如果间隔量不打算改的话,其实没必要再把间隔量放在update方法里了。(因为它是固定的值)
本教程里,update()写作update(delta),render()写作render(alpha),所以你用哪种时步都可以。

public void update() {    update(1f / targetUPS);}public void render() {    render(1f);}

下一篇学习用shader来渲染。

0 0
原创粉丝点击