通过线程实现游戏引擎决定论

Achieving game engine determinism with threading

本文关键字:引擎 决定论 游戏 实现 线程      更新时间:2023-10-16

我想在我的游戏引擎中实现确定性,以便能够保存和回放输入序列,并使网络更容易。

我的引擎目前使用一个可变的时间步长:我计算每一帧更新/绘制最后一帧所花费的时间,并将其传递给实体的更新方法。这使得1000FPS的游戏看起来像30FPS的游戏,但引入了不确定的行为。

一个解决方案可能是将游戏固定为60FPS,但这会使输入更加延迟,并且不会获得更高帧速率的好处。

因此,我尝试使用一个线程(它不断调用update(1),然后休眠16ms),并在游戏循环中尽可能快地绘制。这有点奏效,但它经常崩溃,我的游戏变得无法玩了。

有没有一种方法可以在我的游戏循环中实现线程化,以实现确定性,而不必重写所有依赖引擎的游戏?

您应该将游戏框架与图形框架分开。图形框架应该只显示图形,而不显示其他内容。对于回放,无论你的计算机能够执行多少图形帧,无论是每秒30帧还是每秒1000帧,回放计算机都可能以不同的图形帧速率进行回放。

但你确实应该修复游戏框架。例如,达到每秒100个游戏帧。在游戏框架中,游戏逻辑被执行:与你的游戏(和回放)相关的东西。

只要没有必要的游戏帧,你的游戏循环就应该执行图形帧,所以如果你把游戏固定为每秒100个游戏帧,那就是每个游戏帧0.01秒。如果你的计算机只需要0.001就可以在游戏帧中执行该逻辑,那么剩下的0.009秒用于重复图形帧。

这是一个小但不完整且并非100%准确的例子:

uint16_t const GAME_FRAMERATE = 100;
uint16_t const SKIP_TICKS = 1000 / GAME_FRAMERATE;
uint16_t next_game_tick;
Timer sinceLoopStarted = Timer(); // Millisecond timer starting at 0
unsigned long next_game_tick = sinceLoopStarted.getMilliseconds();
while (gameIsRunning)
{
//! Game Frames
while (sinceLoopStarted.getMilliseconds() > next_game_tick)
{
executeGamelogic();
next_game_tick += SKIP_TICKS;
}
//! Graphical Frames
render();
}

以下链接包含关于创建准确游戏循环的非常好和完整的信息:

http://www.koonsolo.com/news/dewitters-gameloop/

要在网络上具有确定性,您需要一个单一的事实点,通常称为"服务器"。游戏社区里有句话叫"客户掌握在敌人手中"。这是真的。你不能相信任何为了公平竞争而在客户身上计算的东西。

例如,如果你的游戏变得更容易,因为某些原因,你的线程每秒只更新59次,而不是60次,人们会发现的。也许一开始他们甚至不会恶意。他们当时只是让机器满负荷运转,而你的处理速度并没有达到每秒60次。

一旦你有了一个不关心图形或更新周期并以自己的速度运行的服务器(甚至可能是作为单个播放器中的线程进行处理),它就足够具有确定性,至少可以为所有播放器获得相同的结果。基于计算机不是实时的事实,它可能仍然不是100%确定的。即使你告诉它每$frequency更新一次,它也可能不会,因为计算机上的其他进程占用了太多负载。

服务器和客户端需要通信,因此服务器需要向每个客户端发送其状态的副本(为了提高性能,可能是上一个副本的增量)。客户端可以以可用的最佳速度绘制此副本。

如果你的游戏与线程崩溃,也许可以选择将"服务器"从进程中删除并通过网络进行通信,这样你会很快发现哪些变量需要锁,因为如果你只是将它们移到另一个项目中,你的客户端将不再编译。

将游戏逻辑和图形分离到不同的线程中。游戏逻辑线程应该以恒定的速度运行(比如说,它每秒更新60次,如果你的逻辑不太复杂,甚至更高,以实现更流畅的游戏)。然后,图形线程应该始终以尽可能快的速度绘制逻辑线程提供的最新信息,以实现高帧率。

为了防止绘制部分数据,您可能应该使用某种双缓冲,其中逻辑线程写入一个缓冲区,图形线程从另一个缓冲区时读取。然后,每当逻辑线程完成一次更新时,就切换缓冲区。

这样可以确保您始终充分使用计算机的图形硬件。当然,这确实意味着你对最低cpu速度进行了限制。

我不知道这是否有帮助,但如果我没记错的话,Doom存储了你的输入序列,并用它们来生成AI行为和其他一些东西。Doom中的演示块是一系列数字,它们不是代表游戏的状态,而是代表你的输入。根据这些输入,游戏将能够重建所发生的事情,从而实现某种决定论。。。尽管我记得它有时会不同步。