概述
Lecture3
- 1 游戏对象GO(Game Object)
- 1.1 面向对象的GO
- 1.2 面向组件的GO
- 2 Tick()函数
- 3 事件(Event)
- 4 场景管理
- 5 总结
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
,其下才是 AActor
和 UActorComponent
总之游戏中所有的元素都被称为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类型特别多的时候,这将是一场噩梦
- 以坦克发射炮弹为例,定义一个炮弹对象,在炮管处发射之后,每个tick往前走一点,当碰撞系统计算出 炮弹与地面或者其他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
则会更高效
最后
以上就是背后吐司为你收集整理的【GAMES-104现代游戏引擎】3、游戏架构(Tick函数,组件模式,事件系统,场景划分算法)1 游戏对象GO(Game Object)2 Tick()函数3 事件(Event)4 场景管理5 总结的全部内容,希望文章能够帮你解决【GAMES-104现代游戏引擎】3、游戏架构(Tick函数,组件模式,事件系统,场景划分算法)1 游戏对象GO(Game Object)2 Tick()函数3 事件(Event)4 场景管理5 总结所遇到的程序开发问题。
如果觉得靠谱客网站的内容还不错,欢迎将靠谱客网站推荐给程序员好友。
发表评论 取消回复