【GAMES-104现代游戏引擎】3、游戏架构(Tick函数,组件模式,事件系统,场景划分算法)

1 游戏对象GO(Game Object)

游戏世界中的天空、植被、地形、玩家、NPC等等所有游戏对象都统称为 GO(Game Object)

如何描述一个GO?

1.1 面向对象的GO

  • 一个GO类应该包含属性行为,比如一个无人机可以如下定义
    在这里插入图片描述

(为了教学目的 不同于一般游戏引擎,引擎基本上都有一个基类 class Object 作为所有其他对象的基类,Name这种极为通用的属性一般是在Object类中定义的)

  • 根据上方法定义出一个无人机类后,我们还可以派生出一个附带攻击能力的无人机
    在这里插入图片描述
  • 缺点:面向对象的设计理念在游戏中会很容易出现 菱形继承,比如水陆两栖坦克的爷爷到底是船还是车?
  • 这里就引出了新的设计理念: 组件(Component)

1.2 面向组件的GO

组件模式是目前绝大部分引擎所采用的方式(UE、Unity等)

  • 此时我们设计一个无人机就非常灵活,一个无人机类需要什么组件就给他加什么组件,让其拥有飞行器的外表、位移飞行能力、自动索敌AI、飞行动画等等
    在这里插入图片描述
  • 以C++为例,我们必须定义一个 ComponentBase 基类,因为所有的组件类都需要一个tick()函数,所以设计一个基类一方面方便管理,另一方面提供一个纯虚函数tick(),GameObjectBase 基类也同理。给每个组件子类定义属性和行为,最终全部附加给飞行器类,飞行器就拥有了这所有的组件的功能
    在这里插入图片描述

现代游戏引擎中每个GO都必须拥有tick()函数,还需要一个最顶层基类统一管理所有GO的生命周期,因此以Unreal Engine为例,他的最顶层的基类为UObject,其下才是 AActorUActorComponent

总之游戏中所有的元素都被称为GO,每一个GO都是由多个组件构成


2 Tick()函数

Tick函数是游戏世界内最重要的一个函数,也是最基本的时间度量单位

如何使游戏世界动起来?—— 每一次tick,都读取一遍输入,走一遍逻辑计算,再走一遍渲染,就能得到新的一帧画面,这样世界就动起来了。

Tick又被分为两种

  • Object-based Tick:在每个Tick内,调用每个GO的tick(),每个GO再调用自己的每个组件tick(),很直观,不高效
    在这里插入图片描述
  • Component-based Tick:现代游戏引擎都是 按照组件系统进行tick 。各个组件系统依次调用Tick函数,比如先将所有Motor组件计算一遍,再计算Controller组件…。这样流水线般的处理方式效率更高
    在这里插入图片描述

Tick的先后顺序

  • 面向GO的Tick,基本不会出现先后顺序的麻烦问题,因为GO之间如果是绑定的状态,按照逻辑顺序的话应该是 父节点先于子节点执行tick(),但是现代的tick系统都是逐组件批量计算的!
  • 面向组件的tick处理方式,一般而言,为了快,像上图这种的,每个组件系统是放到不同线程上计算,这里面就存在非常头疼的时序问题!
    • 如:对象1给对象2发送一封分手信,对象2也给对象1发送了一封,第二帧的时候,双方同时看到这封分手信,到底是谁甩了谁?不知道。我们没有办法确定哪一方先发送。这就存在不确定性,我们相同的输入,在同一个游戏产生了不确定的结果
    • 游戏回放功能是记录的用户的输入,回放时根据记录的输入重新跑一遍游戏,因此同一输入不能产生二义性
    • 这时我就们需要一个中介的事件发送器来转发事件,并确定Tick的时序。 其中的小细节非常多,许多组件都是循环依赖互相影响的,总之多线程tick时序问题,需要重点关注

3 事件(Event)

GO之间是需要通信

  • 硬编码(Hardcode)通信(不好用)
    • 以坦克发射炮弹为例,定义一个炮弹对象,在炮管处发射之后,每个tick往前走一点,当碰撞系统计算出 炮弹与地面或者其他GO接触时,炮弹就要发出一个我要炸了事件,在其爆炸时检查周围GO的类型,传入switch中判断类型,是什么GO就执行对应的行为(扣血、消失、破坏等)
    • 这种写死的方式很符合直觉,但是当GO类型特别多的时候,这将是一场噩梦
      在这里插入图片描述
  • Event事件通信
    • 消息发送与接受,炮弹爆炸,发送事件给目标GO,目标GO在下个tick()进行响应
    • 这就通过事件机制达到 解耦合(Decouple) 的效果。本来如果是硬编码,我们需要知道其他所有可能的对象类型包括每个GO中的组件类型,这实在是太复杂了。事件机制只需要发送一个事件给对应的GO,对应的GO自己来处理这个事件即可
  • 商业引擎中的 事件机制
    • unity中就是简单的注册一个事件ApplyDamage 从一个GO发送消息,所有与其相关的GO如果内部实现了处理函数会接收到消息并处理
    • UE4中会相对复杂一些,注册event的时候,需要反射到蓝图上,所以有些反射代码会比较头疼
      在这里插入图片描述

4 场景管理

每个GO都有一个UID位置,通过这两个元素我们可以对场景中的GO进行管理。对于场景中的位置没有进行划分时,一个事件的发送需要遍历场景一定范围内的所有GO,这样处理的时间复杂度是极高的,因此我们可以对场景进行区域划分。

  • 简单划分:把场景均匀划分成一个个的格子,爆炸事件发生,只对邻近格子发送事件消息。但是这样的划分在GO分布不均匀的时候很不好用
  • 层级划分:八叉树、四叉树、BVH等,GAMES101中有说到,完全一样的道理。如下四叉树为例,对空间沿着某个轴进行切分,区域内如果GO依然很多,则继续划分,直到某个区域GO数量足够少则停止。当某个节点中某个位置发出一个事件消息时,只需遍历该节点的兄弟、父、子节点发送消息即可
    在这里插入图片描述

BVH的作用非常大,可用于视椎体裁剪、光线追踪、射线检测等等


5 总结

  • 万物皆是Object
  • 每个GO都应该基于组件来描述
  • 游戏世界是通过Tick函数的循环来驱动的
  • GO之间的消息传递通过事件机制

Q&A部分总结

  • 逻辑层 先于 渲染层
  • 空间划分算法有很多,各有优劣,各有适用场景。动态物体的空间划分BVH 则会更高效

猜你喜欢

转载自blog.csdn.net/Motarookie/article/details/126986782