游戏编程模式之游戏循环

实现用户输入和处理器速度在游戏行进时间上的解耦。
(摘自《游戏编程模式》)

  游戏作为一个实时互动软件,实时是其核心。在程序里,总有那么一段代码一直在软件运行周期里不断重复着以监听着用户的输入,并根据输入进行对应的响应,这就是游戏循环代码。要注意的是,在游戏循环中不会出现类似UI事件循环中的阻塞,即使你不输入,游戏依旧在运行着游戏循环。

  试着将游戏逻辑简化为输入处理ProcessInput()、逻辑更新LogicUpdate()、画面渲染Render()、物理更新PhysicUpdate()三个部分(事实上还会有很多模块的循环需要运行),则最简单的游戏循环代码如下:

while(true)
{
    ProcessInput()
    LogicUpdate();
    PhysicUpdate();
    Render();
}

  如果是单机游戏且是指定设备游玩的情况下,我想这样的游戏循环写法或许不会出现太多问题。但是,当今游戏运行设备、平台众多,性能不同导致其运行的效率也不同。我们在编写游戏循环代码时也要考虑到此游戏运行在低性能和高性能机器上会有什么样的差异、这些差异会导致什么问题。接下来我们将逐个进行讨论。

在此之前

术语:FPS

  FPS(Frame Per Second)这是一个频度单位,例如对于屏幕来说,fps代表的就是每秒屏幕刷新多少次。对于游戏循环来说,fps即每秒游戏循环执行多少次。

性能要求

  游戏循环是整个游戏的主*分。程序大部分的时间在运行着游戏循环中的内容,因此,需要优化好游戏循环中调用的函数。

游戏循环

  看看我们上面最简单的游戏代码,你能发现它的问题所在吗?在不同设备运行上述代码时是不一致的。对于性能好的机器,它会在1秒内快速的运行三个函数很多次(例如100次);而对于性能稍差的机器,它只能在1秒内运行5次。这是游戏不能接受的,这会使得游戏对象在不同硬件上所表现的游戏状态不同步(高性能电脑游戏进程快一些;低端电脑游戏进程慢一些)。

方案一:依托时间差的状态更新

  让游戏状态同步,我们可以将距离上一次更新的时间传递给状态更新函数,让游戏状态更新函数根据这个差值进行计算并更新游戏状态。这样编写的游戏循环会针对不同设备的不同时间差进行更新,以确保状态的一致性。

double lastedTime=Time.time;
while(true)
{
    double current=Time.time;
    double deltaTime=current-lastedTime;
    
    LogicUpdate(deltaTime);
    PhysicUpdate(deltaTime);
    
    Render();
    lastedTime=current;
}

  然而这样的方法同样存在缺陷。下面我们针对缺陷进行分析。

  • 对于高性能计算机。它的FPS高,更新的次数更多。而游戏中 的数据计算多为浮点数,计算次数多以为着误差变得更大,而这样会导致两者最后的状态不一致。
  • 为了提高性能,对于游戏的物理引擎会做减幅运算,这个减幅运算被小心地安排成以某个固定时长进行,并非随时调用且计算。然而,对不同设备而言,此方案对PhysicUpdate的调用频率是不一致的,这对物理引擎来说无疑是灾难性的(因为可能两者错开会导致物理引擎模块会丢失几帧的计算)。

方案二:恒定状态更新

  既然要保证不同设备下状态更新次数一致。我们需要限制状态更新相关函数的调用以保证任何机器运行时调用更新函数的频率是一致的(即恒定FPS)。代码如下:

double timeTag=Time.time;
double MS_UPDATE_INTERVAL=1000/60;
double accumulation=0;
while(true)
{
    double current=Time.time;
    double deltaTime=current-timeTag;
    timeTag=current;
    accumulation+=deltaTime;
    
    if(accumulation>MS_UPDATE_INTERVAL)
    {
        LogicUpdate(MS_UPDATE_INTERVAL);
        PhysicUpdate(MS_UPDATE_INTERVAL);
        accumulation=0;
    }
    
    Render();
}

  MS_UPDATE_INTERVAL值的设定有一定的讲究。首先,这个值最好是小于 1000 /60 ms (1秒中刷新次数大于60次),刷新频率过低可导致画面跳帧;其次,MS_UPDATE_INTERVAL设定的时间要大于Update处理的时间——我们需要Update完成后再准备进行下一次Update。

渲染和更新不同步的问题

  我们还有哪些问题可以优化。从上面代码可以看到:Render()代码是没有固定更新步长的,也就是说,状态更新和画面渲染不是在一个时序上,因此,下图的更新次序会导致一些问题的出现。当一次渲染指令在两次更新的中间时,即使前后更新了两次状态,但渲染只执行了一次!这就导致在下一次更新时游戏状态的变化幅度过大,从而导致跳帧。

游戏编程模式之游戏循环

我们可以借助之前对Update的思路:将时间差传递给渲染引擎,让渲染引擎知道应该渲染到何种程度。

Render(accumulation/MS_UPDATE_INTERVAL);

控制游戏循环方式的抉择

基于平台的事件循环

  • 我们不要花更多的时间去编写游戏循环核心代码
  • 针对平台的开发是得天独厚的
  • 失去了对时间的控制。且很多平台的事件循环并没有想象中的敏捷。
  • 无法跨平台

现成游戏引擎的游戏循环

  • 利用现成稳定的游戏循环
  • 无法获得时间的控制权,自定义选项完全依赖引擎现有的实现

自己编写游戏循环

  • 需要自己编写所有代码
  • 需要自己实现平台的接口

拓展

四种游戏循环的实现:https://blog.csdn.net/qq_38134452/article/details/88738879?spm=1001.2014.3001.5501

上一篇:微信小程序 用函数实现 防抖 和 节流


下一篇:扫描线系列