简化版 GAMES 104 现代游戏引擎 04:从0到1构建你的游戏世界
- [✨ 写在前面:小明的困惑](#✨ 写在前面:小明的困惑)
- [Bilibili 同步视频](#Bilibili 同步视频)
- [🎯 第一章:万物皆GO------游戏世界的统一抽象](#🎯 第一章:万物皆GO——游戏世界的统一抽象)
-
- [1.1 先拆个游戏世界看看里面有啥 🧐](#1.1 先拆个游戏世界看看里面有啥 🧐)
- [1.2 核心方法论:Everything is GO 🎯](#1.2 核心方法论:Everything is GO 🎯)
- [🧩 第二章:组件化大法好------告别继承的噩梦](#🧩 第二章:组件化大法好——告别继承的噩梦)
-
- [2.1 早期方案:面向对象继承 👴](#2.1 早期方案:面向对象继承 👴)
- [2.2 致命缺陷:钻石继承难题 💎](#2.2 致命缺陷:钻石继承难题 💎)
- [2.3 现代方案:组件化Component 🧱](#2.3 现代方案:组件化Component 🧱)
- [2.4 代码层面怎么实现? 💻](#2.4 代码层面怎么实现? 💻)
- [2.5 商业引擎都是这么干的! 🏢](#2.5 商业引擎都是这么干的! 🏢)
- [2.6 继承 vs 组件化 大比拼 🆚](#2.6 继承 vs 组件化 大比拼 🆚)
- [⏱️ 第三章:Tick循环------让世界"活"起来的秘密](#⏱️ 第三章:Tick循环——让世界"活"起来的秘密)
-
- [3.1 Tick是什么? 🤔](#3.1 Tick是什么? 🤔)
- [3.2 直觉方案:Per Object Tick 🏃](#3.2 直觉方案:Per Object Tick 🏃)
- [3.3 反直觉但更高效:Per System Tick 🏭](#3.3 反直觉但更高效:Per System Tick 🏭)
- [3.4 流水线的魔力 🍔](#3.4 流水线的魔力 🍔)
- [3.5 两种Tick模式对比 📊](#3.5 两种Tick模式对比 📊)
- [📬 第四章:事件机制------解耦交互的优雅方案](#📬 第四章:事件机制——解耦交互的优雅方案)
-
- [1 问题来了:GO之间怎么交互? 🤝](#1 问题来了:GO之间怎么交互? 🤝)
- [4.2 原始方案:硬编码Hard Code 🔨](#4.2 原始方案:硬编码Hard Code 🔨)
- [4.3 优雅方案:Event事件机制 📮](#4.3 优雅方案:Event事件机制 📮)
- [4.4 这就叫解耦! ✂️](#4.4 这就叫解耦! ✂️)
- [4.5 商业引擎怎么实现? 🏢](#4.5 商业引擎怎么实现? 🏢)
- [🌳 第五章:场景管理------N²灾难的破局之道](#🌳 第五章:场景管理——N²灾难的破局之道)
-
- [5.1 新问题:GO太多了怎么办? 📈](#5.1 新问题:GO太多了怎么办? 📈)
- [5.2 基础管理:UID + 位置 🆔](#5.2 基础管理:UID + 位置 🆔)
- [5.3 最朴素方案:全遍历 ❌](#5.3 最朴素方案:全遍历 ❌)
- [5.4 N平方的噩梦 😱](#5.4 N平方的噩梦 😱)
- [5.5 分而治之:画格子 📐](#5.5 分而治之:画格子 📐)
- [5.6 进阶方案:层级空间树 🌲](#5.6 进阶方案:层级空间树 🌲)
- [5.7 常见的空间数据结构 📚](#5.7 常见的空间数据结构 📚)
- [🎯 总结:四大核心心法,速速收藏!](#🎯 总结:四大核心心法,速速收藏!)
- [💬 写在最后](#💬 写在最后)
✨ 写在前面:小明的困惑
哈喽各位技术小伙伴们!👋 今天咱们来聊一个超级硬核但又贼有意思的话题------如何构建一个真正的游戏世界。
话说上节课,我们聪明好学又蜜汁自信的小明同学,已经掌握了游戏引擎的五层架构:从最底层的硬件平台层,到核心层、资源层、功能层,再到最上层的工具层,整个大厦的结构门儿清。🏗️
但是!重点来了------知道大厦长啥样,不等于知道大厦里的砖石水电是怎么一起work的啊!🤯
就像你背熟了汽车的构造图,不代表你会造发动机对吧?所以今天这节课,含金量直接拉满💯,咱们就来扒一扒:游戏世界到底是怎么从一堆代码变成能玩能嗨的虚拟世界的?
Bilibili 同步视频
🎯 第一章:万物皆GO------游戏世界的统一抽象
1.1 先拆个游戏世界看看里面有啥 🧐
小明玩了《战地2042》,觉得好玩是好玩,但bug多到离谱,一拍大腿:"爷自己做一个!"💪 那首先得想想,这个战争游戏世界里都有啥?
第一类:最吸睛的动态物 Dynamic Game Objects 🚁
地上跑的坦克、天上飞的无人机、端着枪冲的NPC小兵、火炮、导弹......这些玩意儿能动能交互,甚至你能直接上去操作,是大家最容易注意到的明星选手。
第二类:默默奉献的静态物 🏠
高高的瞭望塔、飞机的机棚、各种房子......它们安安静静待在那儿,虽然不能交互,但构成了游戏的关键玩法元素,属于幕后英雄。
第三类:无处不在的环境系统 🌍
-
地形系统:无限绵延的大地,是所有物体的"托盘",独立系统,后面单独讲
-
天空系统TOD:Time of Day日夜变换,阴晴云雨,云卷云舒,看着简单做起来贼复杂
-
植被系统:最神奇的存在!说它是静态吧,风吹会摇、炸弹炸了会倒;说它是动态吧,又固定在那儿。而且量特别特别大,属于环境界的"卷王"🌳
第四类:你看不见但无处不在的"隐形GO" 👻
这就有意思了!游戏里很多东西你根本看不见,但缺了它们真不行:
-
Trigger Box检测体:走到夺奇点积分就涨?就是它在干活!
-
空气墙:玩家最恨的东西没有之一!开飞机飞得正嗨,突然提示"你已飞出作战区域,5秒后击落",就是它搞的鬼😤
-
游戏规则:没错,连玩法规则本身都可以抽象成一个物体!
1.2 核心方法论:Everything is GO 🎯
划重点!在现代游戏引擎中,无论你是静态的、动态的、看得见的、看不见的,甚至连游戏规则本身,都会被统一抽象成一个东西------Game Object,简称GO。
注意啊!这个GO和Go语言、围棋的Go都不是一回事,别搞混了~ 😄
所以构建游戏世界的本质,说白了就是:管理好这一大堆GO。就这么简单?对,就这么简单!但简单的概念背后,是一整套精妙的设计哲学。
🧩 第二章:组件化大法好------告别继承的噩梦
2.1 早期方案:面向对象继承 👴
咱们先从最朴素的想法说起。描述一个物体,无非就是两类东西:
-
属性Property:外形、位置、血量、油量、电量......就是数据嘛
-
行为Behavior:移动、巡逻、攻击......就是函数嘛
于是乎,早期的游戏引擎很自然地就用了面向对象+类继承的思路。比如定义一个Drone无人机类:
cpp
class Drone {
// 属性
Vector3 position; // 位置
float health; // 血量 0-100
float battery; // 电量 0-1
// 行为
void Move(); // 移动
void Patrol(); // 巡逻
};
想做个查打一体无人机?简单!继承一下,加个攻击行为和弹药属性:
cpp
class CombatDrone : public Drone {
int ammo; // 弹药量
void Attack(); // 攻击
};
看上去很完美对不对?非常直觉,非常符合人类对世界的认知。早期游戏还真就是这么写的。
2.2 致命缺陷:钻石继承难题 💎
但是!随着游戏世界越来越复杂,问题就来了------很多物体根本没有清晰的父子关系,全是"混血儿"啊!🤯
举个经典例子:水陆两栖坦克。
坦克派生于车辆,船派生于船舶,那水陆两栖坦克它爹到底是坦克还是船?它爷爷到底是车辆还是船舶?
这就是经典的钻石继承问题,不仅游戏引擎有,现代编程语言里也是个老大难问题。
2.3 现代方案:组件化Component 🧱
怎么解?有一个非常经典、也是现在引擎最常用的方法------组件化!
啥意思呢?就是把对象拆分成一个个独立的组件,就像乐高积木一样。你看那个玩具挖土机,把铲子换成压路机的滚筒,它就变成压路机了;换成推土机的推板,它就变成推土机了。同一个基础底盘,配上不同组件,就能变出各种花样~ 🎮
这思想其实大家天天见!玩射击游戏自定义枪械的时候,加个三倍镜就是狙击枪,加个消音器就是微冲,这不就是组件化嘛!
核心思想:把物体的各种能力拆成独立、可插拔的组件,一个GO可以自由增删组件来实现不同功能。
咱们用无人机来重新理解一下:
-
Transform组件:管空间位置、位移,"我在哪儿"
-
Model组件:管模型外形,"我长啥样"
-
Motor组件:管移动属性,最快速度、加速度、惯性,"我能怎么动"
-
Health组件:管血量,"我有多抗揍"
-
AI组件:管行为逻辑,巡逻、追踪,"我要干啥"
-
Animation组件:管动画,"我怎么动才好看"
-
Physics组件:管物理碰撞,"我撞了会咋样"
把这些组件拼到一起,哎~ 一架无人机就出来了!想做查打一体无人机?简单!把AI组件换成AttackAI,再加个Combat战斗组件,齐活!✨
2.4 代码层面怎么实现? 💻
说穿了也很简单:
-
定义一个
ComponentBase基类,统一所有组件的基础接口,必须包含一个tick()函数(后面会讲为啥) -
Transform、Model、Animation等所有功能组件都继承自这个基类
-
一个GO里面管理一组各种各样的组件,大家协同工作
cpp
// 组件基类
class ComponentBase {
public:
virtual void Tick(float deltaTime) = 0; // 每个组件必须实现Tick
virtual ~ComponentBase() = default;
};
// 具体组件
class TransformComponent : public ComponentBase { /* ... */ };
class ModelComponent : public ComponentBase { /* ... */ };
class HealthComponent : public ComponentBase { /* ... */ };
// 游戏对象
class GameObject {
std::vector<ComponentBase*> components; // 一组组件
public:
void Tick(float deltaTime) {
for (auto* comp : components) {
comp->Tick(deltaTime);
}
}
};
2.5 商业引擎都是这么干的! 🏢
你去玩Unity、Unreal这些商业引擎,会发现它们全是这个路子:
-
Unity:点开任何一个物体,下面就是一长串组件,还能自己加组件、写脚本
-
Unreal:Actor就对应咱们说的GO概念,里面也是各种组件
注意区分:Unreal里的UObject不是咱们说的GO哦!UObject更像C#里的object,是用来管理生命周期、内存释放、GC的基类,是个底层句柄。真正对应GO概念的是Actor派生的那一系列东西,别搞混了~
2.6 继承 vs 组件化 大比拼 🆚
光说不练假把式,咱们来个直观对比:
| 对比维度 | 面向对象继承 | 组件化架构 |
|---|---|---|
| 直观程度 | ⭐⭐⭐⭐⭐ 非常符合直觉 | ⭐⭐⭐ 需要转变思维 |
| 代码复用 | ⭐⭐ 父类强耦合 | ⭐⭐⭐⭐⭐ 组件独立复用 |
| 扩展灵活性 | ⭐⭐ 受限于继承树 | ⭐⭐⭐⭐⭐ 自由组合拼装 |
| 多类型混合 | 💥 钻石继承灾难 | ✅ 完美解决 |
| 性能优化 | ⭐⭐ 数据分散 | ⭐⭐⭐⭐⭐ 便于批处理 |
| 适用场景 | 简单小游戏、类型清晰 | 现代3A大作、复杂世界 |
表1:继承 vs 组件化全方位对比
一句话总结:游戏引擎的本质是生产力工具,要让开发者好维护、好理解,还要让艺术家和设计师用着顺手。组件化就是这么一个符合直觉、又极其灵活的设计,是现代游戏引擎的核心理念。
⏱️ 第三章:Tick循环------让世界"活"起来的秘密
3.1 Tick是什么? 🤔
好了,现在我们有了GO,有了组件,但是......这个世界是静止的啊!坦克开不起来,飞机飞不起来,跟个模型展似的,有啥意思?😅
怎么让世界动起来?答案就是------Tick!
还记得上节课说的吗?游戏引擎最核心的一个函数叫tick,每隔1/30秒(或者1/60秒)就让整个世界往前走一步。就像真实世界里的普朗克时间一样------如果咱们生活在一个模拟世界里,那每个普朗克时间就是上帝给咱们设的tick。🕐
3.2 直觉方案:Per Object Tick 🏃
最直觉的想法是什么?每个GO依次tick,每个GO里的每个组件再依次tick。
比如一辆坦克:
-
先Tick Motor组件:速度10m/s,1/30秒就是0.03秒,往前挪30厘米
-
再Tick Animation组件:履带卷一下,主炮转一下
-
再Tick Combat组件:看看要不要开火
把整个游戏世界所有物体都Tick一遍,世界不就动起来了吗?完美!
这就是最直觉的 Per Object Tick 模式,每个对象自己把自己的事儿干完。
3.3 反直觉但更高效:Per System Tick 🏭
但是!现代游戏引擎一般不这么干。它们会把同类型的组件放一起,按系统批量Tick:
-
先把所有Motor系统全部Tick一遍:所有移动物体统一算位移
-
再把所有Controller系统Tick一遍:统一做物理碰撞检测
-
再把所有Animation系统Tick一遍:统一算动画
-
......以此类推
哎?这听上去有点反直觉啊!就好像说,一个人你不让他先动头再动手再动脚,你非得让所有人先一起动头,再所有人一起动手,再所有人一起动脚......这不是神经病吗?🤪
为啥要这么设计?答案就两个字:效率!
3.4 流水线的魔力 🍔
给大家举个最简单的类比:做汉堡包。🍔
假设汉堡店有5个工人,最直觉的做法是什么?每个人从头到尾做一个汉堡------烤面包、烤牛肉、洗蔬菜、抹黄油、组合起来。一个香甜可口的汉堡就出炉啦~
但这样效率高吗?显然不高!
现代工业最核心的概念叫什么?Pipeline流水线啊!
最高效的做法是:有人专门烤面包,有人专门烤牛肉,有人专门洗菜,大家配合好,到点啪的一下合成一个汉堡。这才是效率最高的。
放到计算机上还有个更大的好处:数据局部性。
图灵机小知识:还记得上节课讲的图灵机故事吗?在图灵机上效率最高的处理方式,就是把同样的数据尽可能放到一起,然后一次批处理完。无论读写都在一起,缓存命中率贼高,效率贼好!
这就是为什么高级课会讲ECS、DOTS这些架构------它们就是把这个思路做到了极致。
3.5 两种Tick模式对比 📊
咱们用图来直观感受一下两种模式的区别:
图1:两种Tick模式架构对比
一句话记住:造个流水线,批处理干活,效率就是高!就这么简单~ 😎
📬 第四章:事件机制------解耦交互的优雅方案
1 问题来了:GO之间怎么交互? 🤝
好了,现在世界能动了。但是......每个GO都在自顾自表演,坦克自己开,飞机自己飞,互相之间半毛钱关系没有,这叫啥游戏啊?😅
举个最简单的例子:我跳上坦克,朝远处敌人开了一炮,要把敌人击倒。那问题来了------我开炮这事儿,怎么让敌人那个GO知道自己被打了?
4.2 原始方案:硬编码Hard Code 🔨
最朴素的想法是什么?炮弹爆炸的时候,遍历周围所有物体,一个一个看:
-
你是人?扣你血!
-
你是飞机?扣你血!
-
你是坦克?扣你血!
-
你是石头?哦没事,你继续待着。
这就是所谓的Hard Code硬编码。
大家别笑,早期游戏引擎真就这么写的。但是世界一复杂,这就崩了------每次加个新物体类型,你就得去所有爆炸逻辑里改一遍,维护起来简直是噩梦。😱
4.3 优雅方案:Event事件机制 📮
那怎么解?现代游戏引擎有一个非常优雅的机制------Event事件机制,也叫消息机制。
啥意思呢?咱们别那么粗暴,直接敲人家门说"你被我打了"。咱们换个方式:**写邮件!**📧
每个人家门口都放个邮箱,我不需要认识你,我只需要知道方圆20米内的邻居,给你们每家寄一封邮件。邮件内容是:"不好意思,我炸了,扣你100点血。"(这邮件叫blackmail,敲诈信,哈哈)😂
然后呢,下个tick的时候,你打开邮箱一看:"哦?我被扣了100点血?"然后你的Health组件一查,自己只有70点血,那好吧,只能死给你看了。💀
就这么简单!一个本来超级复杂的耦合问题,通过一个事件机制,瞬间变得清晰又干净。
4.4 这就叫解耦! ✂️
在系统架构里,这叫解耦。
本来GO和GO之间通讯,需要知道所有其他GO的类型、每个GO里有哪些组件,太复杂了。现在统一变成事件机制,你只要发个事件给对应的GO,让它自己处理就完事儿了。
各个GO和组件之间的逻辑,干干净净,互不干扰。加新物体?不用改爆炸逻辑,只要给新物体加个事件处理函数就行。完美!👌
4.5 商业引擎怎么实现? 🏢
-
Unity:简单粗暴,用字符串匹配。注册一个叫"DAMAGE_EVENT"的事件,SendEvent就完事儿了。物体的Health组件收到事件,回调函数激活,自己扣血。
-
Unreal:复杂一些,用C++原生代码加反射机制。注册事件,绑定回调函数。好处是蓝图里也能用,可视化操作,策划也能上手。
引擎设计核心:做游戏引擎最核心的就是做一个可扩展的消息系统。让开发者能在引擎之上,不断定制和自己玩法相关的各种消息类型,再定制各种组件来处理这些事件------这就是现代游戏引擎最核心的工作。
🌳 第五章:场景管理------N²灾难的破局之道
5.1 新问题:GO太多了怎么办? 📈
等等,先别急着高兴。还有个问题:游戏世界里有那么多GO啊!
单机游戏里一般几百个动态GO,多的几千甚至上万个。那每次发生个事件,我怎么通知到这些GO?每次爆炸,我怎么找周围的物体?
咱们回到小明的战争游戏:NPC大兵、飞机、坦克、大炮......这么多东西,怎么管?
5.2 基础管理:UID + 位置 🆔
先讲两个最基本的概念:
-
UID唯一编号:每个GO都有一个全局唯一ID,就像门牌号一样,用来标识定位物体。还记得上节课讲的GUID吗?一个道理。
-
Position空间位置:每个GO都有个三维坐标,告诉我们它在哪儿。
有了这俩,我们就能开始做场景管理了。
5.3 最朴素方案:全遍历 ❌
最简单的管理是什么?不管理!😎
小明很粗暴:反正我也没几个兵,炮弹爆炸的时候,把场景里所有GO全部查一遍位置,判断是不是在爆炸半径内,是的话就发伤害消息。
这写行不行?做小游戏完全没问题,跑得动。但是!当场景里有几千上万个GO的时候,这就是一场灾难!
5.4 N平方的噩梦 😱
这就是游戏引擎里经典的N²挑战。
什么意思?每个物体都可能和其他物体发生互动,如果每次都要和其他所有物体问一遍,那就是N×(N-1)≈N²次运算。
一万个物体的话,N²是多少?**一亿!**💥
这对计算机来说是巨大的负载,尤其如果数据还分散在内存各个地方,那效率低到没法看。
5.5 分而治之:画格子 📐
那怎么破?最简单的方法:画格子!
Divide and Conquer,分而治之。把整个世界画成均匀的格子,每个格子里放哪些GO我都记着。爆炸的时候,我只需要查爆炸所在的格子和周围几个格子就行了,工作量瞬间降下来!
这方法听上去土,但很多场景真的好用。如果场景不大、物体分布均匀,简单画格子就够了,又快又好写。
5.6 进阶方案:层级空间树 🌲
但是画格子有个问题:如果场景物体分布不均匀呢?
实话告诉大家,现代3A游戏里,看着世界很广阔,实际上玩家能走的地方非常受限。以前在国外做游戏的时候,这叫Trench战壕------设计师给玩家挖了很多条战壕,玩家只能在战壕里走来走去。战壕里的东西放得又密又细,战壕外面稀稀拉拉啥也没有。
这时候你均匀打格子,是不是又慢又浪费?
于是就有了更聪明的方案:层级结构空间树。
啥意思呢?就像地图一样:整个世界很大,分成国家,国家分成省,省分成城市,城市分成街区,街区再分成街道。有事件发生在北京海淀区某条大街,那我只需要在北京海淀区那个小区域里找就行了,犯不着把全中国都搜一遍。🇨🇳
5.7 常见的空间数据结构 📚
| 数据结构 | 原理 | 适用场景 | 复杂度 |
|---|---|---|---|
| 均匀网格 | 世界均匀切分成格子 | 小型场景、物体分布均匀 | O(1) 查找 |
| 四叉树(2D) | 递归四等分空间,密集区细分 | 2D游戏、地面场景管理 | O(log N) |
| 八叉树Octree(3D) | 递归八等分3D空间 | 3D空间、全空间管理 | O(log N) |
| BSP二叉树 | 沿平面分割空间,常沿墙体 | 室内场景、FPS游戏 | O(log N) |
| BVH包围体层次树 | 物体包围盒自底向上合并 | 现代3A主流、射线检测、视锥裁剪 | O(log N) |
表2:常见空间管理数据结构对比
咱们用图来看看四叉树是怎么工作的:
图2:四叉树空间细分示意图(红色区域为物体密集区,持续细分)
**怎么选?**看你的游戏类型。做个2D超级马里奥那种平台游戏,根本不需要复杂的场景管理。但做COD、战地这种3A射击游戏,那必须花心思好好设计,才能尽量节约计算资源。
对小明的战争游戏来说,用个四叉树或者BVH,基本就够用了。
🎯 总结:四大核心心法,速速收藏!
好了,讲到这里,今天的核心内容就差不多了。动手能力强的小伙伴,估计已经手痒痒想去写代码了吧?😏
咱们来总结一下,今天这节课你只需要记住四件事,就掌握了现代游戏引擎的核心逻辑:
万物皆GO 🎯
游戏世界里几乎所有东西,全部统一抽象成Game Object。无论是看得见的坦克飞机,还是看不见的触发器、游戏规则,全都是GO。
组件是GO的原子 🧩
每个GO由各种各样的组件组合而成,用组件拼装替代类继承,灵活扩展,完美解决多类型混合的问题。
Tick驱动世界运行 ⏱️
每一帧Tick一次,世界往前走一步。现代引擎采用按系统批量Tick的流水线模式,追求极致性能。
事件+空间树 📬🌳
事件系统实现跨GO交互,完美解耦;空间树结构管理海量游戏对象,规避N平方性能灾难。
就这四点,记住了,你就明白了现代游戏引擎的组合基础逻辑。是不是感觉自己离做出下一个3A大作又近了一步?🚀
💬 写在最后
其实游戏引擎这东西,说难也难,说简单也简单。很多核心思想,说穿了都是些很朴素的道理------分而治之、流水线、解耦......这些计算机科学的经典思想,换个场景换个包装,就变成了游戏引擎里的核心设计。
最重要的是什么?是理解这些设计背后的为什么。为什么要用组件化不用继承?为什么要按系统Tick不按对象Tick?为什么要有事件系统?想明白这些"为什么",你才是真的懂了,而不是死记硬背几个概念。
毕竟,引擎不是用来炫技的,它是个生产力工具。让大家方便、容易理解,才是引擎设计最底层的需求。

好啦,今天的硬核分享就到这里!如果觉得有帮助,别忘了**点赞👍 收藏⭐ 关注👀**三连一波,咱们下节课继续扒游戏引擎的那些事儿~
有啥问题欢迎在评论区交流,咱们一起学习一起进步!💪
--- EOF ---