![]()
引言
本课程将介绍现代游戏引擎所涉及的系统架构,技术点,引擎系统相关的知识。通过该课程,你能够对游戏引擎建立起一个全面且完整的了解。本节主要是介绍游戏引擎的数据组织和管理。
How to bring the game world to life
![]()
上一节课我们介绍了游戏引擎的基本结构。
有了这些知识只是知道这栋大厦长得是什么样子,但是我们并不知道里面的砖石、水电是怎么 work 的。而今天我们将带大家了解怎么去构建一个游戏世界。
Dynamic Game Objects
![]()
首先的话我们需要对游戏世界进行拆解。
如图所示,坦克、士兵等动态的游戏对象是我们最容易关注的东西,在现代游戏引擎里被称为 Dynamic Game Objects。
Static Game Object
![]()
另一类与之相对的就是静态物体。
例如高高的瞭望塔、机场的机棚、房子等,虽然这些物体无法交互,但是整个构成了游戏的各种各样的 GamePlay 元素。
Environments
![]()
除了静态物和动态物之外还有无处不在的地形系统,它是支撑前面两者的托盘。
Other Game Object
![]()
游戏中还存在大量的物体例如检测体、空气墙等等,甚至玩法规则本身也可以抽象成一个物体。
Everything is a Game Object
![]()
无论你是静态的还是动态的,我们都会把它统一抽象为 游戏对象(Game Object)。现代游戏引擎中我们一般会把所有的这些东西全部统一抽象成 GO。
How to Describe a Game Object
![]()
How Do We Describe a Drone in Reality
![]()
当我们在游戏世界描述物体的时候可以归类成两类:属性(property)和行为(behavior)。
![]()
class Drone |
![]()
class ArmedDrone |
基于此我们可以做更多的变换,比如设计一款查打一体的无人机。
![]()
如果大家有一定的语言基础就会意识到,可以用对象的派生和继承关系定义一个无人机 Drone 类,然后再派生一个查打一体无人机 ArmedDrone。这也是非常经典的面向对象的行为方式。
![]()
这个方法虽然简单易懂,但是缺陷在于随着我们的游戏世界越做越复杂,这些物体并没有特别清晰的父子关系。
Component Base
![]()
现代游戏引擎的常用解决方法是 组件化。
我们把对象的行为拆分为无数的组件,如图所示玩具挖土机的铲子可以换成各种各样的部件,组件可以把同一个基础的物体变为各种各样的物体。
![]()
同样的,在玩现代射击游戏中我们可以定制枪械的组件和模块。
Components of a Drone
![]()
回到无人机的案例,我们可以将它的行为和属性拆分为组件:
- Transform
- Motor
- Model
- Animation Physics
- AI
这些属性和行为都变成一个一个的小组件,最终拼接为自己的无人机。
Component
![]()
代码实现只需 ComponentBase 的基类,它统一好每个基础行为接口。然后位移、模型、动画这些类全部派生自这个基类,各种各样的小组件就可以协同工作。
![]()
再回到无人机的例子,我们只需替换 AI 模块和战斗模块就拼接为查打一体的无人机。
所以现代游戏引擎的核心理念是尽可能符合大家的直觉,整个基础结构需要让开发者好维护好理解,同时要交给大量的艺术家和设计师去使用。如第一节课所述,游戏引擎架构它不是技术炫耀体,它是一个生产力工具,大家方便理解才是架构设计的底层需求。
Components in Commercial Engines
![]()
Unity 和 Unreal 等商业引擎都会去提供 Component 的概念。
需要注意的是 Unreal 的 UObject 不是我们讲的 GO,更像是高级语言的 Object 用于确定对象生命周期的管理。而真正的 GO 则更像是 Actor。
Takeaways
![]()
- Everything is a game object in the game world
- Game object could be described in component-based way
How to Make the World Alive
![]()
Object-based Tick
![]()
我把每一个 GO 的 Component 依次 Tick 一遍,游戏世界就动起来了,也非常符合我们的直觉。
Component-based Tick
![]()
但是在现代游戏引擎中我们一般不是按照每个对象 Tick,而是把一个个系统 Tick。
Objected-based Tick vs. Component-based Tick
![]()
这可能有些反直觉,我们可以举一个例子。
汉堡的直觉制作方法是每个人烤面包、烤牛肉、洗蔬菜,最后组装在一起。而这样的生产效率并不高,现代工业的核心概念是流水线,最高效的方法是有人专门去烤面包,有人专门去洗蔬菜……大家配合好最后组装成汉堡。
How to Explode an Ammo in a Game
![]()
Hardcode
![]()
void Bomb::explode() |
Events
![]()
现代游戏引擎使用事件系统来优雅地解决这个问题,在系统架构中被称为解耦合。
Events mechanism in Commercial Engines
![]()
How to Manage Game Objects
![]()
很多游戏的 GO 动辄成百上千个,那我发生的每一件事情是如何通知的呢?
Scene Management
![]()
- 每个游戏 GO 会有一个唯一的编号 UID(类比资源管理的 GUID)
- 每个物体在空间上都会有位置 position
![]()
最简单的方式就是分而治之划格子,但当场景分布不均匀时可能会出现问题。
![]()
就像我们的地图一样,整个世界很大,但是我们可以把世界分成国家、国家分成行省、行省分成城镇、城镇分成区块……假设有一件事件发生,我只需要在某个区块去找就可以了。
这样一个 Hierarchical 的场景管理方法就是一个非常有效的场景管理方法。
![]()
回到刚才的示例,以空间四叉进行划分形成树状结构,也就是数据结构中典型的四叉树。
Spatial Data Structures
![]()
这其中也分很多流派,有二叉树、八叉树等等。现在游戏引擎比较流行 BVH 层次包围盒技术可以帮助我们快速定位,空间上的数据管理是场景管理的核心。
Takeaways
![]()
- Everything is an object
- Game object could be described in the component-based way
- States of game objects are updated in tick loops
- Game objects interact with each other via event mechanism
- Game objects are managed in a scene with efficient strategies
Others
![]()
![]()
![]()
![]()
![]()
- 如果一个 tick 时间过长怎么办?
一个比较简单的解决方案是直接跳过 tick。如果某个 tick 计算过于复杂,我们不必要把这些计算放在一帧处理而是分成几批,差个五帧处理完,而五帧在人的视觉中也就 0.2s 左右可以接受。
- 空气墙和其他 GO 有什么区别?
空气墙其实并不是一个很好的例子,游戏中我们用到最多的是透明的 Trigger。空气墙大部分情况作为一个 GO 就解决掉了,有时也会分为许多小 GO,作为引擎开发者不需要考虑这些策略,只需要知道空气墙一般使用最简单的形体来构建即可。
- tick 时,渲染线程和逻辑线程怎么同步?
一般来讲渲染线程和逻辑线程会分开,而 tickLogic() 会比 tickRender() 早一点。
- 空间划分怎么处理动态的游戏对象?
树的数据结构可以插入或者删除,前面提到的 BVH、BSP 等等都存在更新问题。一般会选择更新轻量化的算法与数据结构以提高效率,所以引擎推荐支持两到三种经典的空间划分算法,游戏产品根据自己的需求去选择。
- 组件模式有什么缺点?
组件模式的第一个缺点是组件模式如果是很基础的实现,它的效率肯定没有直接写一个 class 效率高。后面讲的 ECS 会把同样的组件、数据全放在一起,用方法快速处理这些数据,避免了切换成本过高。
组件模式的第二个缺点是组件之间需要有一套通讯接口机制,而这个机制在高频调用时对效率的影响是非常大的。
![]()