目录
一个游戏世界由什么构成
一个游戏世界我们首先关注到的就是一系列可移动可操作的动态物(Dynamic Game Objects),如坦克、火炮等;还有一系列静态物(Static Game Object),如石头、棚屋等,这些静态物虽然不能交互,却是GamePlay中很关键的元素;最容易被忽略却又无处不在的是游戏的环境(Environments),它一般由天空(Sky)、植被(Vegetation)和地形(Terrain)组成;游戏中还存在着大量其他物体,如触发检测体(Trigger Area)、导航网格(Navigation Mesh)等。以上的各种对象,无论静态还是动态,我们统称为Game Object(GO)。
![](https://img-blog.csdnimg.cn/img_convert/048f33ef831dba29650e12d7e3d6e239.jpeg)
如何描述一个Game Object
游戏中描述一个物体,可以分为属性(Property)和行为(Behaviour)两方面,这就和面向对象的语言相匹配了。我们可以为物体设计定义一个类,根据它的属性定义成员变量(如position、battery),根据它的行为定义成员函数(如move、scout)。
![](https://img-blog.csdnimg.cn/img_convert/2dee395937bfd53e7ff5544b28848ed9.jpeg)
根据上述定义描述出一个类对象后,我们还可以通过定义继承(Inheritance)类,衍生出更多的类对象,比如说一个物体在继承无人机类后,再定义弹药量属性和攻击行为,我们便得到了察打一体无人机类。
![](https://img-blog.csdnimg.cn/img_convert/d2a2debc3d0e5af457f70dbb94994d82.jpeg)
但是继承类的使用又给我们带来了新的问题:类似水陆两栖坦克的类是继承自坦克类还是航船类?这就需要考虑将对象组件化,通过Component的组合来设计定义GO。以C语言为例,我们只需定义一个ComponentBase基类,由此派生出其他行为接口类(如Transform、Model、Motor、AI等),再根据需要实现改造这些接口类。像这样以组件的形式代替继承类的使用可以使我们对GO的设计更加灵活。
![](https://img-blog.csdnimg.cn/img_convert/94ccaeb0f432af860c9579a3d8fc04a4.jpeg)
我们要注意到Unreal和Unity中的Object和上述Game Object还是有差别的。
如何使游戏世界动起来
这就需要用到之前提过的Tick函数,在每个Tick内都让世界向前走一小步,这样世界就动起来了。
- Object-based Tick:在每个Tick内,将每个GO的每个Component的Tick函数都调用一次。
- Component-based Tick:各个Component依次调用Tick函数,比如先将Motor组件中的任务执行完,然后执行Controller组件中的任务,再执行Animation中的任务。这样流水线般的处理方式效率更高,在现代游戏引擎中也更常用。
游戏世界中的Events
以一个坦克发射炮弹为例,早期的设计方式是定义一个炮弹对象,在其爆炸时检查周围GO的类型,并对应的造成影响。
现在可以通过事件机制来解耦(Decouple)GO之间的通信,通过向不同GO发送事件,将事件交由对应的GO处理,使得不同GO之间的逻辑解耦合,降低代码的复杂性。比如当一个炸弹接触到地面时,它向周围的GO发送一个Explode事件,像Health组件监听到这个事件后,它会对应处理事件造成一个如Damage的影响。
如何管理GO
每个GO都有一个唯一标识UID和一个位置,通过这两个元素我们可以对场景中的GO进行管理。当我们对于场景中的位置没有进行划分时,一个事件的发送可能需要遍历场景一定范围内的所有GO,这样处理的时间复杂度是极高的。
我们可以将场景分为数个网格,每个网格分别管理,当发送事件时,优先对邻近的网格发送。但是当每个网格中GO分布不均时,这样处理效率又会变得很低。
![](https://img-blog.csdnimg.cn/img_convert/8266f9746157172c9b4851fced466563.jpeg)
为了提高效率,我们可以对每一个网格进一步细分,每一个网格划分为更小的四个网格,构成四叉树结构,当我们需要搜查一个GO邻近的GO时,只需要在它的父节点、兄弟节点、邻近节点之间搜查即可。
还有许多其他的场景管理结构
![](https://img-blog.csdnimg.cn/img_convert/98d9df59d5cf72011cb1dce00bb3711b.jpeg)
*Tick的时序问题
如下图中GO之间一般会出现绑定的现象,那么在执行Tick函数时,绑定的双方哪一方先执行Tick函数呢?这就要考虑Tick的时序。一般会要求父节点先执行Tick函数,绑定其上的GO随后执行。因为不同Component的Tick函数是分散到不同CPU上并行处理的,所以不同Component的Tick时序问题则会更复杂。当两个Component互相发送事件时,因Tick函数是多线程执行的,我们没有办法确定哪一方先发送,但对于多个CPU来说Tick的时序很重要,这时我就们需要一个中介的事件发送器来转发事件,并确定Tick的时序。
![](https://img-blog.csdnimg.cn/img_convert/3e2ac77e2f52598fce1a0d430686957c.jpeg)
总结:
- 在游戏世界中一切事物都是GO
- 每个GO都可以由Component的组合的形式来描述
- 游戏世界是通过Tick函数的循环调用一点一点动起来的
- 每个GO通过事件机制与其他GO交互
- GO的管理需要采用高效的场景管理机制
以上即是我在第三课中的收获~