zhouqijie

实时Update游戏对象

Jason:游戏是动态、基于时间的模拟,一个游戏对象的状态时描述他在某一刻的组态。另一个说法是,一个游戏对象的时间是离散的(discrete),而不是连续的(continuous)。但是可以想象他是连续的,然后在引擎中离散地采样。

Jason:大多数低阶引擎子系统都需要周期性更新,通常是通过游戏循环的主循环来更新子系统,或者使用多线程。游戏对象模型也可以当作一个需要周期更新的引擎子系统。

Jason:游戏对象更新是一个动态、实时、基于代理的计算机模拟。游戏是基于代理模拟中一个较复杂的应用。



1.最直接的Update方式(不可行)

最直接的方法是遍历游戏对象集合,并调用每个对象的Update虚函数。通常是每帧执行一次,调用Update函数时,可以传入距离上一帧的时间差deltaTime。

对于基于组件的对象模型,则是每帧调用每个组件的Update函数。

管理游戏对象集合:

游戏引擎通常会用一个单例管理所有Active游戏对象,可以命名为GameObjectsManager

游戏对象集合一般是动态的,因为要动态生成销毁。所以使用链表是较高效的方式。很多引擎会使用比链表更复杂的数据结构。

Cre:全局游戏对象句柄表可以吗?

Update函数问题:

大多数游戏对象会和一个或者多个子系统互动。最直接的办法是在游戏对象的Update函数里分别更新动画、渲染、音效等子系统。

这样的Update方式,游戏循环基本只需要更新游戏对象。但是这种方式有很多问题。不能用于商业级引擎。



2.批次更新

Jason:大部分低阶引擎系统都有很严峻的性能限制,它们需要处理大量数据和进行大量运算。所以对于大多数引擎来说批次式更新很有用。例如相比逐对象更新动画及交错进行其他动画无关运算,把动画组成一个批次更新要高效很多。

这种更新方式,更新一个游戏对象时。只会改变子系统的属性,但是不会立即更新这些子系统。

批次更新的优点:

  1. 最高的缓存一致性:加强了游戏引擎子系统内的缓存一致性,因为子系统能把各个对象的相应数据分配到一个连续内存中。
  2. 最少的重复运算:可以先执行整体的运算,之后在各对象中重用,无需每次在对象中重新计算。
  3. 减少资源分配:如果某个子系统的更新与其他子系统的更新交错进行,处理每个对象时需要重复释放再分配一些资源。
  4. 高效流水线:不用批次更新的话不能实现一些并行设计。

补充:

Jason:性能有时不是使用批次更新的唯一原因,一些引擎子系统根本上不能以对象为单位进行更新。例如多刚体的碰撞决议(collision resolution)等。



3.对象及子系统的相互依赖

Jason:即使我们不关注性能,当游戏对象依赖其他对象时,简单的逐对象更新仍然不可行。

Jason:引擎子系统之间的依赖也是很常见的问题。例如布娃娃模拟系统中动画和刚体物理之间的相互依赖。

分阶段更新:

Jason:子系统之间的依赖,可以在游戏主循环中明确地硬编码子系统更新次序。

Jason:游戏对象更新通常不能简化为每帧每对象调用一次Update。每个游戏对象通常需要多次更新,因为有时需要多个引擎子系统的中间结果。

Cre:动画系统和物理人偶再加上IK,大概需要多次调整才能得到最终姿势。

Jason:每次遍历所有游戏对象调用Update,开销可能很高,并且不是所有对象都需要所有更新阶段。所以可以每个更新阶段管理一个对象链表。

分组更新:

Jason:对象之间的依赖,可以看作多个依赖树组成的树林。

Jason:解决更新次序的问题的一个方法是把对象分组,每个树深度分为一组。

把主循环的各子系统更新细分到每个组,在主循环中更新组。

状态一致性和一帧延迟问题:

由于游戏对象是逐个更新的,所以在某一个时刻两个游戏对象的状态可能不一样,单个游戏对象的内部状态也可能不一样。

例如对象B依赖对象A的属性。在更新对象B的时候对象A可能未更新,这就会产生延迟一帧的问题。一个解决方法是前文的游戏对象按组分类,把被依赖对象放在先更新的组中。

对象加上时间戳(time-stamp),就可以分辨游戏状态是在之前还是当前时间。

查询一个对象的状态,检查它的的时间戳以断言获得恰当的状态。

一个能优化一致性的方法状态缓存,更新时不必覆盖原状态。

  1. 优势一:任何对象都可以查询其他对象之前的状态,不受更新次序影响。
  2. 优势二:保证更新过程中永远有一个完全一致的状态。
  3. 优势三:可以通过线性地向前后两个状态插值。

Jason:Havok物理引擎就是纯粹为了这个原因保存每个刚体的前后状态。



4.并行设计

并行处理方式有多种,使得游戏引擎能充分利用有并行处理能力的硬件。

并行处理大概分为游戏对象模型本身并行化引擎子系统的并行化

游戏对象模型并行化:

Jason:游戏对象模型本身的并行很困难,因为各种依赖和交流很复杂并且不可预期。对象间通信须使用线程同步机制,这是效能不容许的。

Jason:但是理论上仍然可以并行更新游戏对象。需要谨慎设计整个对象模型,保证对象不能直接查看其他游戏对象的状态,而是通过一个高效的消息传递机制来传递消息。有些研究采用分布式(distributed)编程语言(如Erlang)编写游戏对象模型。

引擎子系统并行化:

Jason:尖端的并发分布式对象模型在理论上可行。但是大部分游戏引擎不会使用,而是会使用单线程游戏对象模型,然后把注意力集中在低阶引擎子系统的并行化。

Jason:相比游戏对象模型,低阶子系统才是性能关键,因为低阶引擎子系统会在每帧处理大量数据,而游戏对象模型所需的CPU资源通常较少。(80-20规则)

Jason:两个重要思维:异步和批处理。

  1. 何时开始请求。(越早启动越有机会在实际需要结果时完成工作)
  2. 可以等待多久。(有些子系统例如AI系统可以容忍等待一帧以上)

(END)