我们如何将游戏逻辑与显示分开?
如何使每秒显示帧独立于游戏逻辑?因此,无论视频卡可以渲染多快,游戏逻辑都以相同的速度运行。
解决方案
回答
我们可以使游戏循环如下所示:
int lastTime = GetCurrentTime(); while(1) { // how long is it since we last updated? int currentTime = GetCurrentTime(); int dt = currentTime - lastTime; lastTime = currentTime; // now do the game logic Update(dt); // and you can render Draw(); }
然后,我们只需要编写" Update()"函数来考虑时差即可。例如,如果我们有一个物体以一定的速度" v"移动,则每帧通过" v * dt"更新其位置。
回答
我认为这个问题揭示了对游戏引擎应该如何设计的一些误解。这是完全可以的,因为它们是该死的复杂的事情,很难解决;)
正确的印象是我们想要所谓的帧速率独立性。但这不仅指渲染帧。
单线程游戏引擎中的框架通常称为"刻度"。我们处理的每一笔订单都将处理输入,处理游戏逻辑并根据处理结果渲染一帧。
我们要做的是能够以任何FPS(每秒帧数)处理游戏逻辑并获得确定的结果。
在以下情况下,这成为一个问题:
检查输入:
输入是关键:" W"表示我们将玩家角色向前移动10个单位:
playerPosition += 10;
现在,由于每帧都执行此操作,因此,如果以30 FPS的速度运行,则每秒将移动300个单位。
但是,如果我们以10 FPS的速度运行,则每秒只能移动100个单位。因此,游戏逻辑与帧速率无关。
幸运的是,要解决此问题并使游戏玩法逻辑独立于帧速率是一项相当简单的任务。
首先,我们需要一个计时器,该计时器将计算每帧渲染所需的时间。然后,将以秒为单位的数字(因此,完成一次滴答需要0.001秒)乘以我们想要独立于帧速率的数值。因此,在这种情况下:
按住" W"时
playerPosition += 10 * frameTimeDelta;
(Delta是"事物发生变化"的花哨词。)
因此,玩家将在一个"滴答"中移动十分之一的分数,并且在"滴答"一秒钟后,我们将移动了整个10个单位。
但是,当涉及变化率也随时间变化的属性(例如,加速的车辆)时,该值将下降。这可以通过使用更高级的集成器(例如" Verlet")来解决。
多线程方法
如果我们仍然对问题的答案感兴趣(因为我没有回答,但提出了替代方法),就在这里。将游戏逻辑和渲染分为不同的线程。它有缺点。足够使绝大多数游戏引擎保持单线程。
并不是说只有一个线程在所谓的单线程引擎中运行。但是,所有重要任务通常都集中在一个中心线程中。诸如"碰撞检测"之类的东西可能是多线程的,但通常Tick的"碰撞"阶段会阻塞,直到所有线程都返回,并且引擎返回到单个执行线程。
多线程提出了一个非常大的类问题,甚至是一些性能问题,因为所有内容(甚至容器)都必须是线程安全的。游戏引擎一开始是非常复杂的程序,因此很少值得多线程处理它们的复杂性。
固定时间步长法
最后,正如另一位评论者所述,具有固定大小的时间步长并控制我们"逐步"执行游戏逻辑的频率也可以是一种非常有效的处理方法,它具有许多优点。
为了完整起见,在此处链接,但是其他评论者也链接到它:
修正时间步骤
回答
根据我的经验(不多),杰西和亚当的答案应该可以使我们走上正确的道路。
如果我们需要进一步的信息并了解其工作原理,我发现TrueVision 3D的示例应用程序非常有用。
回答
Koen Witters有一篇非常详细的文章,介绍了不同的游戏循环设置。
他涵盖:
- FPS取决于恒定的游戏速度
- 游戏速度取决于可变FPS
- 恒定的游戏速度和最高的FPS
- 恒定游戏速度与可变FPS无关
(这些是按需要从文章中拉出的标题。)
回答
过去有一篇关于flipcode的出色文章。我想将其挖掘出来并呈现给我们。
http://www.flipcode.com/archives/Main_Loop_with_Fixed_Time_Steps.shtml
这是一个运行游戏的深思熟虑的循环:
- 单螺纹
- 在固定的游戏时间
- 使用插值时钟尽可能快地处理图形
好吧,至少我是这样认为的。 :-)可惜的是,在此发布之后进行的讨论很难找到。也许Wayback机器可以在那里提供帮助。
time0 = getTickCount(); do { time1 = getTickCount(); frameTime = 0; int numLoops = 0; while ((time1 - time0) TICK_TIME && numLoops < MAX_LOOPS) { GameTickRun(); time0 += TICK_TIME; frameTime += TICK_TIME; numLoops++; // Could this be a good idea? We're not doing it, anyway. // time1 = getTickCount(); } IndependentTickRun(frameTime); // If playing solo and game logic takes way too long, discard pending time. if (!bNetworkGame && (time1 - time0) TICK_TIME) time0 = time1 - TICK_TIME; if (canRender) { // Account for numLoops overflow causing percent 1. float percentWithinTick = Min(1.f, float(time1 - time0)/TICK_TIME); GameDrawWithInterpolation(percentWithinTick); } } while (!bGameDone);
回答
Enginuity有一个稍微不同但有趣的方法:任务池。
回答
在显示图形之前有时间延迟的单线程解决方案很好,但是我认为渐进方式是在一个线程中运行游戏逻辑,然后在另一个线程中显示。
但是,我们应该以正确的方式同步线程;)实现将花费很长时间,因此,如果游戏规模不大,则单线程解决方案就可以了。
同样,将GUI提取到单独的线程中似乎也是一种很好的方法。我们是否曾在RTS游戏中移动单位时看到"任务完成"弹出消息?我就是这个意思 :)
回答
这不涉及较高的程序抽象内容,即状态机等。
通过调整帧时差来控制运动和加速度是很好的选择。
但是诸如此类的事,比如在此之后2.55秒后触发声音,或者改变
游戏等级18.25秒后,依此类推。
可以将其与经过的帧时间累加器(计数器)相关联,但是这些计时可以
如果帧速率低于状态脚本分辨率,就会搞砸
也就是说,如果高级逻辑需要0.05秒的粒度,并且速度低于20fps。
如果游戏逻辑在单独的"线程"上运行,则可以保持确定性
(在软件级别,我更愿意在此级别,或者在OS级别)具有固定的时间片,与fps无关。
代价可能是,如果发生的事情不多,我们可能会浪费帧间的CPU时间,
但我认为这可能是值得的。