How to Build a Game World

引言

本课程将介绍现代游戏引擎所涉及的系统架构,技术点,引擎系统相关的知识。通过该课程,你能够对游戏引擎建立起一个全面且完整的了解。本节主要是介绍游戏引擎的数据组织和管理。

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
{
public:
/* Properties */
Vector3 position;
float health;
float fuel;
...

/* Behavior */
void move();
void scout();
...
}

class ArmedDrone
{
public:
/* Properties */
Vector3 position;
float health;
float fuel;
float ammo;
...

/* Behavior */
void move();
void scout();
void fire();
...
}

基于此我们可以做更多的变换,比如设计一款查打一体的无人机。

如果大家有一定的语言基础就会意识到,可以用对象的派生和继承关系定义一个无人机 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()
{
...
switch(go_type)
{
case GoType.humen_type:
{
/* process soldier */
}
case GoType.drone_type:
{
/* process drone */
...
}
case GoType.tank_type:
{
/* process tank */
...
}
case GoType.stone_type:
{
/* process stone */
...
}
default:
{
break;
}
}
...
}

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 会把同样的组件、数据全放在一起,用方法快速处理这些数据,避免了切换成本过高。

组件模式的第二个缺点是组件之间需要有一套通讯接口机制,而这个机制在高频调用时对效率的影响是非常大的。


GAMES104_Lecture_03