简化版 GAMES 104 现代游戏引擎 05:游戏引擎世界构建核心机制深度解析
- [✨ **写在前面** ✨](#✨ 写在前面 ✨)
- [📦 第一章:游戏世界里到底有些啥?------万物皆对象](#📦 第一章:游戏世界里到底有些啥?——万物皆对象)
-
- [🎯 1.1 游戏世界的四大构成要素](#🎯 1.1 游戏世界的四大构成要素)
- [🎯 1.2 大一统:Game Object(GO)的诞生](#🎯 1.2 大一统:Game Object(GO)的诞生)
- [🧩 第二章:GO怎么描述?------从继承地狱到组件化天堂](#🧩 第二章:GO怎么描述?——从继承地狱到组件化天堂)
-
- [🎯 2.1 描述一个GO的两大维度](#🎯 2.1 描述一个GO的两大维度)
- [🎯 2.2 传统方案:面向对象继承------看起来很美](#🎯 2.2 传统方案:面向对象继承——看起来很美)
- [🎯 2.3 现代方案:组件化架构------乐高积木的艺术](#🎯 2.3 现代方案:组件化架构——乐高积木的艺术)
- [🎯 2.4 组件化的核心优势](#🎯 2.4 组件化的核心优势)
- [⏱️ 第三章:世界怎么动起来?------Tick帧驱动机制](#⏱️ 第三章:世界怎么动起来?——Tick帧驱动机制)
-
- [🎯 3.1 什么是Tick?------游戏世界的「普朗克时间」](#🎯 3.1 什么是Tick?——游戏世界的「普朗克时间」)
- [🎯 3.2 直觉方案:按GO逐个Tick](#🎯 3.2 直觉方案:按GO逐个Tick)
- [🎯 3.3 高效方案:按组件类型批量Tick------流水线大法](#🎯 3.3 高效方案:按组件类型批量Tick——流水线大法)
- [🎯 3.4 为什么要批量Tick?------性能优化的底层逻辑](#🎯 3.4 为什么要批量Tick?——性能优化的底层逻辑)
- [🎯 3.5 延伸:ECS架构------把批量做到极致](#🎯 3.5 延伸:ECS架构——把批量做到极致)
- [📨 第四章:GO之间怎么交互?------Event事件消息系统](#📨 第四章:GO之间怎么交互?——Event事件消息系统)
-
- [🎯 4.1 原始方案:硬编码(Hard Code)------简单粗暴但烂](#🎯 4.1 原始方案:硬编码(Hard Code)——简单粗暴但烂)
- [🎯 4.2 优雅方案:Event事件系统------发邮件的艺术](#🎯 4.2 优雅方案:Event事件系统——发邮件的艺术)
- [🎯 4.3 事件系统的C++实现](#🎯 4.3 事件系统的C++实现)
- [🎯 4.4 事件系统的核心优势](#🎯 4.4 事件系统的核心优势)
- [🎯 4.5 商业引擎的实现](#🎯 4.5 商业引擎的实现)
- [🌳 第五章:海量GO怎么管?------空间加速结构](#🌳 第五章:海量GO怎么管?——空间加速结构)
-
- [🎯 5.1 N²的噩梦------为什么不能全遍历](#🎯 5.1 N²的噩梦——为什么不能全遍历)
- [🎯 5.2 入门方案:均匀网格------画格子大法](#🎯 5.2 入门方案:均匀网格——画格子大法)
- [🎯 5.3 进阶方案:四叉树/八叉树------自适应空间划分](#🎯 5.3 进阶方案:四叉树/八叉树——自适应空间划分)
- [🎯 5.4 工业级方案:BVH------现代引擎的首选](#🎯 5.4 工业级方案:BVH——现代引擎的首选)
- [🎯 5.5 怎么选?------不同场景的最佳实践](#🎯 5.5 怎么选?——不同场景的最佳实践)
- [🎯 第六章:全文总结------五大核心,构建你的游戏世界](#🎯 第六章:全文总结——五大核心,构建你的游戏世界)
-
- [🎯 6.1 五大核心机制](#🎯 6.1 五大核心机制)
- [🎯 6.2 一句话记住](#🎯 6.2 一句话记住)
- [🎯 6.3 写在最后](#🎯 6.3 写在最后)
✨ 写在前面 ✨
哈喽各位技术小伙伴们~👋 今天我们来聊一个超级硬核但又无比有趣的话题------「游戏引擎到底是怎么构建出一个完整游戏世界的?」
🤔 相信很多同学都玩过《战地》《COD》这类3A大作,看着里面坦克轰鸣、战机翱翔、NPC满地跑的宏大战场,是不是也曾好奇过:这背后到底是怎样的代码架构在支撑着这一切?
💡 别着急,今天这篇文章,我们就跟着「小明同学」的脚步,从0到1拆解现代游戏引擎的世界构建逻辑。从最基础的GameObject,到组件化架构,再到Tick驱动、事件系统、空间加速......一条龙给你讲得明明白白!
🚀 坐稳扶好,我们发车啦~
📦 第一章:游戏世界里到底有些啥?------万物皆对象
🎯 先问大家一个灵魂拷问:如果让你设计一个战争游戏,你觉得这个世界里都需要些什么东西?
坦克?飞机?小兵?房子?地形?天空?......没错,这些都对!但如果我们一个个单独处理,那代码岂不是要乱成一锅粥?🍜
🧠 别急,引擎设计师们早就想好了------「万物皆对象」!
🎯 1.1 游戏世界的四大构成要素
我们先来给游戏世界做个「人口普查」📊:
🔹 第一类:动态物(Dynamic Game Objects)
就是那些会动的、能交互的、玩家能操控的家伙们~比如地上跑的坦克🚗、天上飞的无人机✈️、端着枪冲的NPC小兵👮、还有各种火炮导弹💣。这些是玩家最容易关注到的,也是游戏玩法的核心载体。
🔹 第二类:静态物(Static Objects)
默默站岗的背景板选手~比如高高的瞭望塔🗼、飞机的机棚🏠、各种建筑物。虽然它们不能交互,但却是构成游戏场景、提供Gameplay空间的关键元素。没有它们,游戏世界就成了一片虚空~
🔹 第三类:环境系统(Environment)
最容易被忽略但又无处不在的「隐形大佬」👑:
• 🌍 地形系统:承载一切的大地母亲,无限绵延,是所有动态物和静态物的「托盘」
• 🌤️ TOD天空系统(Time of Day):日夜变换、阴晴雨雪、云卷云舒,看似简单实则复杂得很
• 🌳 植被系统:说它是静态吧,风吹会摇、炸弹炸了会倒;说它是动态吧,又固定在一个地方。而且数量特别特别多,是性能优化的重灾区
🔹 第四类:隐形逻辑对象(Invisible Logic Objects)
肉眼看不见,但玩法全靠它们!比如:
• 📦 Trigger Box(检测体):你看不见它,但你走进去它就知道了。夺奇点、剧情触发点全靠它
• 🧱 空气墙:玩家最讨厌的东西没有之一!开飞机飞得正嗨,突然告诉你「你已飞出作战区域」,然后五秒后击落......
• 📜 游戏规则载体:整个玩法的规则逻辑,也可以抽象成一个对象
🎯 1.2 大一统:Game Object(GO)的诞生
🤯 这么多类型的东西,难道要每种都写一套独立的管理逻辑?那引擎代码不得炸了?💥
✨ 于是,现代游戏引擎最核心的抽象之一诞生了------「Game Object」 ,简称GO!
📌 划重点 :在现代游戏引擎中,我们把场景里的所有实体、所有逻辑载体,全部统一抽象成「Game Object」这个概念。
不管你是会动的坦克、不会动的房子、看不见的检测体、甚至是游戏规则本身......统统都是GO!
⚠️ 注意:这里的GO和Go语言、围棋的Go都没有关系哦,就是Game Object的缩写~大家读的时候直接读字母「G-O」就行😉
💡 小明顿悟时刻:哦!原来构建游戏世界,本质上就是管理好这一大堆GO就行了嘛~
嗯......话是这么说没错,但怎么管理呢?别急,我们接着往下看~👇
🧩 第二章:GO怎么描述?------从继承地狱到组件化天堂
好,现在我们知道了万物皆GO。但问题来了:一个GO具体该怎么描述呢?🤔
就拿小明想做的「自动巡逻无人机」来说吧,它有外形、有位置、有血量、有电量,还能飞、能巡逻......这些东西该怎么组织成代码?
🎯 2.1 描述一个GO的两大维度
其实很简单,任何物体都可以拆成两部分:
📊 属性(Property):就是「它是什么样的」------数据
• 外形几何(三角形网格)
• 空间位置(三维坐标)
• 血量、油量、电量
• 弹药数量
• ......
⚡ 行为(Behavior):就是「它能做什么」------逻辑/函数
• 移动(Move)
• 巡逻(Patrol)
• 攻击(Attack)
• 播放动画
• ......
📌 划重点:属性 + 行为,就能描述几乎所有的游戏物体!
🎯 2.2 传统方案:面向对象继承------看起来很美
学过面向对象编程的小伙伴,第一反应肯定是:「这还不简单?写个类不就完了!」
没错,最早期的游戏引擎还真就是这么干的~我们来看看:
💻 【C++代码示例】传统继承方案
// 基类:普通侦察无人机
class Drone {
public:
// ===== 属性 =====
Vector3 position; // 空间位置
float health; // 血量 (0-100)
float battery; // 电量 (0-1)
// ===== 行为 =====
void Move(); // 移动
void Patrol(); // 巡逻AI
};
// 派生类:察打一体无人机
class CombatDrone : public Drone {
public:
// 新增属性
int ammo; // 弹药量
// 新增行为
void Attack(); // 攻击
};
👆 是不是很完美?数据和方法封装在一起,通过继承扩展功能,非常符合直觉~
但是!等等......🤔
如果小明又想做一个「水陆两栖坦克」呢?坦克继承自车辆类,船继承自船舶类,那两栖坦克到底该继承谁?它的爷爷到底是坦克还是船?
😱 这就是经典的「菱形继承问题」,也是面向对象架构的致命缺陷!
随着游戏世界越来越复杂,各种「混血儿」物体层出不穷,继承树会变得无比臃肿、难以维护。今天加个功能要改基类,明天加个物体要新建N个派生类......最后整个架构就变成了没人敢动的「屎山」💩
🎯 2.3 现代方案:组件化架构------乐高积木的艺术
既然继承不行,那怎么办呢?💡
引擎设计师们想到了一个绝妙的主意:「把对象拆成一个个独立的组件,像搭积木一样拼起来!」
🧱 就像玩具挖土机一样,你把铲子换成压路机的滚筒,它就变成了压路机;换成吊车臂,它就变成了起重机。同一个基础,换不同的部件就是不同的东西~
🎮 这就像游戏里的武器自定义系统:装个三倍镜就是狙击枪,装个消音器就是微冲。组件化,就是这么灵活!
好,我们用组件化的思想重新设计无人机:
💻 【C++代码示例】组件基类设计
// 组件基类:所有组件都继承自它
class ComponentBase {
public:
virtual ~ComponentBase() = default;
// 每个组件都要有Tick函数,每一帧更新逻辑
virtual void Tick(float deltaTime) = 0;
// 所属的GO
class GameObject* owner = nullptr;
};
👆 看到那个 Tick 函数了吗?别着急,我们后面会专门讲它有多重要~先记住:每个组件都要能「每帧更新」。
好,现在我们来把无人机拆成一个个组件:
💻 【C++代码示例】各种功能组件
// 变换组件:负责空间位置、旋转、缩放
class TransformComponent : public ComponentBase {
public:
Vector3 position;
Vector3 rotation;
Vector3 scale = Vector3(1, 1, 1);
void Tick(float deltaTime) override {
// 变换组件一般不需要每帧更新逻辑
}
};
// 模型组件:负责外观渲染
class ModelComponent : public ComponentBase {
public:
Mesh* mesh = nullptr; // 网格模型
Material* material = nullptr; // 材质
void Tick(float deltaTime) override {
// 渲染相关的更新
}
};
// 运动组件:负责移动物理
class MotorComponent : public ComponentBase {
public:
float maxSpeed = 50.0f; // 最大速度
float acceleration = 10.0f; // 加速度
Vector3 velocity; // 当前速度
void Tick(float deltaTime) override {
// 根据速度更新位置
TransformComponent* transform = owner->GetComponent<TransformComponent>();
if (transform) {
transform->position += velocity * deltaTime;
}
}
};
// 血量组件:负责生命值
class HealthComponent : public ComponentBase {
public:
float maxHealth = 100.0f;
float currentHealth = 100.0f;
void Tick(float deltaTime) override {
// 血量回复、中毒持续伤害等逻辑
}
void TakeDamage(float damage) {
currentHealth -= damage;
if (currentHealth <= 0) {
// 死亡逻辑
}
}
};
// AI组件:负责巡逻、追踪等行为
class AIComponent : public ComponentBase {
public:
enum class AIState { Patrol, Chase, Attack };
AIState currentState = AIState::Patrol;
void Tick(float deltaTime) override {
// 根据状态执行不同的AI逻辑
switch (currentState) {
case AIState::Patrol: UpdatePatrol(deltaTime); break;
case AIState::Chase: UpdateChase(deltaTime); break;
case AIState::Attack: UpdateAttack(deltaTime); break;
}
}
private:
void UpdatePatrol(float deltaTime);
void UpdateChase(float deltaTime);
void UpdateAttack(float deltaTime);
};
哇塞!是不是瞬间清晰了很多?🤩
每个组件只负责一件事,各司其职,互不干扰。想要什么功能,就把对应的组件挂到GO上就行~
那GO本身呢?它就变成了一个「组件容器」:
💻 【C++代码示例】GameObject类设计
class GameObject {
public:
// 唯一ID
uint64_t uid = 0;
// 组件列表
std::vector<ComponentBase*> components;
// 添加组件
template<typename T>
T* AddComponent() {
T* comp = new T();
comp->owner = this;
components.push_back(comp);
return comp;
}
// 获取组件
template<typename T>
T* GetComponent() {
for (auto* comp : components) {
T* result = dynamic_cast<T*>(comp);
if (result) return result;
}
return nullptr;
}
// 每帧更新所有组件
void Tick(float deltaTime) {
for (auto* comp : components) {
comp->Tick(deltaTime);
}
}
~GameObject() {
for (auto* comp : components) {
delete comp;
}
components.clear();
}
};
🎉 完美!现在GO就是一个空盒子,你想给它装什么组件就装什么组件~
我们来组装一架侦察无人机:
GameObject* CreateScoutDrone() {
GameObject* drone = new GameObject();
drone->uid = GenerateUID();
drone->AddComponent<TransformComponent>();
drone->AddComponent<ModelComponent>();
drone->AddComponent<MotorComponent>();
drone->AddComponent<HealthComponent>();
drone->AddComponent<AIComponent>(); // 普通巡逻AI
return drone;
}
那察打一体无人机呢?简单!换个AI组件,再加个战斗组件就行:
// 战斗组件:负责瞄准、开火
class CombatComponent : public ComponentBase {
public:
int ammo = 100;
float damage = 25.0f;
float fireRate = 0.5f; // 每秒开火次数
float fireCooldown = 0.0f;
void Tick(float deltaTime) override {
if (fireCooldown > 0) {
fireCooldown -= deltaTime;
}
}
bool Fire() {
if (fireCooldown <= 0 && ammo > 0) {
ammo--;
fireCooldown = 1.0f / fireRate;
// 发射子弹逻辑...
return true;
}
return false;
}
};
// 战斗AI组件(继承自普通AI,扩展攻击行为)
class CombatAIComponent : public AIComponent {
public:
void Tick(float deltaTime) override {
// 先执行基础AI逻辑
AIComponent::Tick(deltaTime);
// 额外的战斗逻辑
if (currentState == AIState::Attack) {
CombatComponent* combat = owner->GetComponent<CombatComponent>();
if (combat) {
combat->Fire();
}
}
}
};
GameObject* CreateCombatDrone() {
GameObject* drone = CreateScoutDrone();
// 把普通AI换成战斗AI
// (实际项目中会有更优雅的组件替换机制)
drone->AddComponent<CombatComponent>();
return drone;
}
🚀 是不是超级灵活?想要什么功能加什么组件,完全不需要改继承关系!
🎯 2.4 组件化的核心优势
为什么现代引擎(Unity、Unreal)都不约而同地选择了组件化架构?因为它的优势实在是太大了:
✅ 高内聚低耦合:每个组件只干一件事,组件之间通过标准接口交互,改一个不影响另一个
✅ 极致的灵活性:想要新功能?加个组件就行!想要修改功能?换个组件就行!完全不需要动继承树
✅ 易于非程序员使用:艺术家、策划可以在编辑器里拖拽增减组件,像搭积木一样创造物体,不需要写代码
✅ 便于内存优化:同类组件的数据可以集中存放,方便做缓存优化(后面讲ECS的时候会深入)
📌 划重点:游戏引擎的架构本质上是「生产力工具」,不是技术炫耀的舞台。让开发者好理解、好维护、艺术家也能用,才是引擎设计的底层需求。
💡 商业引擎对应关系:
• Unity:打开Inspector面板,下面那一长串就是组件~Transform、MeshRenderer、Rigidbody、各种脚本......全是组件
• Unreal:Actor 就相当于我们讲的GO,里面的StaticMeshComponent、LightComponent、SkeletalMeshComponent......也都是组件
⚠️ 注意区分:Unreal里的UObject ≠ GO!UObject是负责内存管理、生命周期、GC(垃圾回收)的基类,更像是C#里的object。而Actor才是场景里的实体,相当于我们讲的GO。别搞混了哦~😉
⏱️ 第三章:世界怎么动起来?------Tick帧驱动机制
好,现在我们有了GO,有了组件,能描述静态的世界了。但问题是......这个世界是死的啊!😱
坦克不会开,飞机不会飞,小兵不会跑......这还叫游戏吗?跟个3D模型展览馆似的~
那怎么让世界动起来呢?💡
🎯 3.1 什么是Tick?------游戏世界的「普朗克时间」
不知道大家有没有想过一个问题:我们的现实世界是连续的吗?
从物理上来说,时间可能有最小单位------普朗克时间。宇宙就是一帧一帧「跳着走」的,只是因为帧率太高了,我们感觉不到而已~
🎮 游戏世界也是一样的道理!
📌 划重点 :游戏引擎每隔固定时间(比如1/30秒、1/60秒),就让整个世界「往前走一步」。这个单步更新的函数,就叫做 Tick。
就像上帝给这个世界按下了「快进键」,每按一次,所有东西都更新一点点~按得快了,就连贯成了流畅的动画~
🎯 3.2 直觉方案:按GO逐个Tick
那这个Tick具体怎么执行呢?
最直觉的想法当然是:遍历所有GO,每个GO依次Tick它的所有组件~
就像这样:
💻 【C++代码示例】按GO逐个Tick
// 游戏世界类
class GameWorld {
public:
std::vector<GameObject*> allObjects;
// 全局Tick函数
void Tick(float deltaTime) {
// 遍历每个游戏对象
for (auto* obj : allObjects) {
// 每个对象依次Tick自己的所有组件
obj->Tick(deltaTime);
}
}
};
👆 是不是很直觉?坦克先更新移动、再更新动画、再更新战斗;然后飞机更新移动、动画、战斗......一个一个来。
这就像一个人做汉堡:先烤面包、再烤牛肉、再洗蔬菜、再抹酱、再组合起来。一个人从头到尾做完一整个汉堡~
但是!等等......🤔
这样效率高吗?
🎯 3.3 高效方案:按组件类型批量Tick------流水线大法
大家都知道现代工业的核心是什么吗?是流水线(Pipeline)!🏭
做汉堡最高效的方式,不是每个人做完整的一个,而是:有人专门烤面包,有人专门烤牛肉,有人专门洗菜......大家分工协作,批量处理,效率才最高!
游戏引擎的Tick也是一样的道理~
现代引擎的做法是:不是按GO逐个Tick,而是按组件类型批量Tick!
先把所有物体的移动组件全部更新一遍,再把所有物理碰撞全部检测一遍,再把所有动画全部更新一遍......
💻 【C++代码示例】按系统批量Tick
class GameWorld {
public:
// 按类型分组存储组件(而不是按GO分组)
std::vector<TransformComponent*> allTransforms;
std::vector<MotorComponent*> allMotors;
std::vector<HealthComponent*> allHealths;
std::vector<AIComponent*> allAIs;
std::vector<ModelComponent*> allModels;
void Tick(float deltaTime) {
// 第一步:所有移动组件批量更新
for (auto* motor : allMotors) {
motor->Tick(deltaTime);
}
// 第二步:所有物理碰撞批量检测
PhysicsSystem::Tick(deltaTime);
// 第三步:所有AI批量更新
for (auto* ai : allAIs) {
ai->Tick(deltaTime);
}
// 第四步:所有动画批量更新
AnimationSystem::Tick(deltaTime);
// 第五步:渲染
RenderSystem::Tick(deltaTime);
}
};
👆 看到了吗?这就是「按系统批量Tick」的思路~
乍一听有点反直觉:为什么不让每个人自己动完头手脚,而是让所有人先一起动头、再一起动手、再一起动脚?感觉怪怪的......
但它的优势实在是太大了!🚀
🎯 3.4 为什么要批量Tick?------性能优化的底层逻辑
为什么要这么「反直觉」地设计?因为计算机底层的工作方式决定了:批量处理就是快!
还记得上节课讲的图灵机故事吗?那条无限长的纸带~📜
在图灵机(也就是我们的计算机)上,效率最高的处理方式是什么?
📌 划重点:把同样类型的数据尽可能放在一起,然后一次性批量处理完!
为什么?因为**CPU缓存(Cache)**的存在~
💡 技术小科普:
CPU的运算速度超级快,但内存的读写速度慢得多。为了弥补这个差距,CPU里有一块高速缓存(Cache)。当你读取一个数据时,CPU会把它周围的一大块数据都搬到缓存里。
如果你的数据是连续存放的,读完一个,下一个正好已经在缓存里了------这叫「缓存命中」,速度飞快!
如果你的数据是东一个西一个散落在内存各处的,每次都要去内存里重新读------这叫「缓存失效」,速度会慢几十上百倍!
所以,当我们把所有Motor组件的数据集中放在一起,然后批量处理时:
✅ 数据内存连续,缓存命中率极高
✅ 一次读取可以处理很多个组件
✅ 整体性能可以提升几倍甚至几十倍!
而如果按GO逐个Tick呢?每个GO里的组件类型都不一样,数据在内存里是跳来跳去的,缓存命中率极低,性能自然就差了~
📌 一句话总结:批量Tick = 流水线作业 + 缓存友好 = 性能爆炸!💥
🎯 3.5 延伸:ECS架构------把批量做到极致
如果把「按组件批量处理」的思想推到极致,会得到什么?
答案是:ECS架构(Entity-Component-System)!
这是现在游戏引擎界最火的架构之一,Unity的DOTS、Unreal的Mass都是这个路子~
简单说一下ECS的核心思想:
• 🆔 Entity(实体):就是我们的GO,但它只是一个ID,什么数据都没有
• 📦 Component(组件):只有纯数据,没有任何逻辑(注意!和我们之前讲的组件不一样哦)
• ⚙️ System(系统):只有纯逻辑,处理一批特定类型的组件
数据和逻辑完全分离,所有处理都是批量的,性能直接拉满!🚀
不过ECS的水很深,今天就不展开了~大家只要知道:它是组件化思想的终极进化形态,核心目标就是性能、性能、还是TMD性能!
📌 本节小结:
-
Tick就是游戏世界的「单步更新」,每隔固定时间执行一次
-
直觉方案:按GO逐个Tick,简单但低效
-
高效方案:按组件/系统批量Tick,流水线作业,缓存友好,性能高
-
现代引擎越来越倾向于批量Tick,追求极致性能
📨 第四章:GO之间怎么交互?------Event事件消息系统
好,现在世界能动起来了!坦克能开,飞机能飞,小兵能跑~
但是......它们各玩各的啊!😅
坦克自顾自地开,飞机自顾自地飞,小兵自顾自地跑......它们之间完全没有任何交互!这叫什么游戏啊?跟个大型沙盘似的~
那怎么让它们交互呢?比如我开一炮,怎么让对面的小兵知道「我被打了」?
🎯 4.1 原始方案:硬编码(Hard Code)------简单粗暴但烂
最容易想到的方法是什么?
炮弹爆炸了,对吧?那我就在炮弹的爆炸逻辑里,遍历周围所有的GO,一个一个判断:
• 如果你是人,扣血
• 如果你是飞机,扣血
• 如果你是坦克,扣血
• 如果你是石头,啥也不做
• ......
💻 【C++代码示例】硬编码方案(反面教材)
// 炮弹组件
class BulletComponent : public ComponentBase {
public:
float damage = 50.0f;
float explosionRadius = 5.0f;
void OnExplode() {
// 获取周围所有物体
std::vector<GameObject*> nearbyObjects =
SceneManager::GetObjectsInRadius(owner->GetPosition(), explosionRadius);
// 遍历每个物体,硬编码判断类型
for (auto* obj : nearbyObjects) {
// 是小兵吗?
if (obj->IsType("Soldier")) {
SoldierComponent* soldier = obj->GetComponent<SoldierComponent>();
if (soldier) soldier->TakeDamage(damage);
}
// 是飞机吗?
else if (obj->IsType("Aircraft")) {
AircraftComponent* aircraft = obj->GetComponent<AircraftComponent>();
if (aircraft) aircraft->TakeDamage(damage);
}
// 是坦克吗?
else if (obj->IsType("Tank")) {
TankComponent* tank = obj->GetComponent<TankComponent>();
if (tank) tank->TakeDamage(damage);
}
// 是石头吗?啥也不做
else if (obj->IsType("Rock")) {
// 石头不受伤害
}
// ...... 还有N种类型要判断
}
}
void Tick(float deltaTime) override {
// 炮弹飞行逻辑...
if (HasHitSomething()) {
OnExplode();
}
}
};
👆 看到了吗?这就是「硬编码」方案。
先别急着笑,最早期的游戏真的就是这么写的~😂
但是!这个方案的问题太大了:
❌ 耦合度爆炸:炮弹组件需要知道所有可能被它影响的物体类型,还要知道每个物体的扣血函数叫什么
❌ 扩展性极差:每次加个新物体类型,都要去改炮弹的代码。加100种物体,炮弹里就要有100个if-else
❌ 维护噩梦:哪天某个物体的扣血函数改了名字,炮弹里的代码也要跟着改,漏改一个就是BUG
❌ 违反开闭原则:对扩展开放,对修改关闭?不存在的,全是修改!
用不了多久,整个代码就会变成一团乱麻,谁都不敢动~💩
🎯 4.2 优雅方案:Event事件系统------发邮件的艺术
那有没有更优雅的方法呢?
当然有!就是我们今天的主角------事件(Event)消息系统!🎉
💡 想象一下:
你住在一个小区里,想给邻居们发个通知。你需要认识每一个邻居、知道他们家在哪、然后挨家挨户敲门去说吗?
不需要啊!你只要写一封信,扔进每家的邮箱里就行了。至于邻居收到信之后怎么处理,那是他们自己的事~
📨 事件系统就是这个道理!
炮弹爆炸了,它不需要知道周围有谁、谁会扣血、谁会怎么样。它只要发出一个「爆炸伤害事件」,然后就完事了~
至于谁关心这个事件、收到事件后要做什么,那是接收方自己的事。发送方完全不需要知道!
这就是「发布-订阅模式」(Publish-Subscribe Pattern),也叫观察者模式~
🎯 4.3 事件系统的C++实现
好,我们来看看怎么用C++实现一个简单的事件系统:
💻 【C++代码示例】事件基类与事件管理器
// 事件类型枚举
enum class EventType {
Damage, // 伤害事件
Explosion, // 爆炸事件
TriggerEnter, // 进入触发区
TriggerExit, // 离开触发区
Spawn, // 生成物体
Death, // 死亡事件
// ... 可以无限扩展
};
// 事件基类
class GameEvent {
public:
virtual ~GameEvent() = default;
virtual EventType GetType() const = 0;
// 发送者(可选)
GameObject* sender = nullptr;
};
// 伤害事件
class DamageEvent : public GameEvent {
public:
float damage = 0.0f;
Vector3 hitPosition;
EventType GetType() const override { return EventType::Damage; }
};
// 事件回调函数类型
using EventHandler = std::function<void(GameEvent*)>;
// 事件管理器(单例)
class EventManager {
public:
static EventManager& GetInstance() {
static EventManager instance;
return instance;
}
// 订阅事件
void Subscribe(EventType type, GameObject* listener, EventHandler handler) {
listeners[type][listener] = std::move(handler);
}
// 取消订阅
void Unsubscribe(EventType type, GameObject* listener) {
auto& typeListeners = listeners[type];
typeListeners.erase(listener);
}
// 发送事件
void SendEvent(GameEvent* event) {
EventType type = event->GetType();
auto& typeListeners = listeners[type];
for (auto& [listener, handler] : typeListeners) {
handler(event);
}
}
private:
// key: 事件类型, value: map<监听者, 回调函数>
std::unordered_map<EventType, std::unordered_map<GameObject*, EventHandler>> listeners;
};
👆 这就是一个最简单的事件管理器了~
我们来看看怎么用:
💻 【C++代码示例】发送和接收事件
// ========== 发送方:炮弹 ==========
class BulletComponent : public ComponentBase {
public:
float damage = 50.0f;
float explosionRadius = 5.0f;
void OnExplode() {
// 1. 获取爆炸范围内所有物体
std::vector<GameObject*> nearbyObjects =
SceneManager::GetObjectsInRadius(owner->GetPosition(), explosionRadius);
// 2. 给每个物体发送伤害事件
DamageEvent damageEvent;
damageEvent.sender = owner;
damageEvent.damage = damage;
damageEvent.hitPosition = owner->GetPosition();
for (auto* obj : nearbyObjects) {
// 注意:这里只发送事件,完全不知道对方会怎么处理!
// 甚至不知道对方关不关心这个事件!
EventManager::GetInstance().SendEvent(&damageEvent);
}
// 3. 炮弹自己销毁
owner->Destroy();
}
void Tick(float deltaTime) override {
if (HasHitSomething()) {
OnExplode();
}
}
};
// ========== 接收方:血量组件 ==========
class HealthComponent : public ComponentBase {
public:
float currentHealth = 100.0f;
// 初始化时订阅伤害事件
void Initialize() {
EventManager::GetInstance().Subscribe(
EventType::Damage,
owner,
[this](GameEvent* event) {
this->OnDamage(event);
}
);
}
// 收到伤害事件时的处理
void OnDamage(GameEvent* event) {
DamageEvent* damageEvent = dynamic_cast<DamageEvent*>(event);
if (damageEvent) {
TakeDamage(damageEvent->damage);
}
}
void TakeDamage(float damage) {
currentHealth -= damage;
if (currentHealth <= 0) {
Die();
}
}
void Die() {
// 死亡逻辑...
// 还可以发送死亡事件,让其他系统知道
DeathEvent deathEvent;
deathEvent.sender = owner;
EventManager::GetInstance().SendEvent(&deathEvent);
owner->Destroy();
}
void Tick(float deltaTime) override {}
~HealthComponent() {
// 析构时记得取消订阅!
EventManager::GetInstance().Unsubscribe(EventType::Damage, owner);
}
};
🎉 完美!看到了吗?
发送方(炮弹)完全不知道接收方(血量组件)的存在,它只是把事件「扔出去」就完事了~
接收方(血量组件)自己决定要不要订阅这个事件,收到事件后怎么处理也是它自己的事~
🎯 4.4 事件系统的核心优势
为什么事件系统是现代游戏引擎的标配?因为它解决了大问题:
✅ 彻底解耦:发送方和接收方完全不需要知道对方的存在,甚至不需要知道对方是什么类型
✅ 超强扩展性:想加新的事件类型?加个枚举值就行!想让新物体响应事件?订阅一下就行!完全不需要改原有代码
✅ 符合开闭原则:对扩展开放,对修改关闭------完美!
✅ 便于非程序员使用:在Unity/Unreal里,策划可以在蓝图里直接「发送事件」「接收事件」,不需要写代码
✅ 方便调试和日志:所有事件都经过事件管理器,加个日志就能看到整个游戏世界里发生了什么
📌 划重点:事件系统是游戏引擎的「神经系统」,负责在各个GO、各个系统之间传递信息。没有它,整个世界就是一盘散沙~
🎯 4.5 商业引擎的实现
我们来看看商业引擎是怎么做的:
🎮 Unity:
• 可以用 SendMessage() 发送消息,通过字符串匹配回调函数
• 也可以用 C# 的 event/delegate 自己实现更高效的事件系统
• Unity 自己的 UI 系统、物理系统也都是基于事件的
🎮 Unreal:
• 基于C++反射系统实现,事件可以在蓝图里可视化绑定
• 有Dispatcher、Delegate、Event等多种事件机制
• 蓝图里拖几根线就能实现事件的发送和接收,策划也能玩转~
虽然实现细节不同,但核心思想都是一样的:发布-订阅,解耦交互!
🌳 第五章:海量GO怎么管?------空间加速结构
好,现在我们有了GO、组件、Tick、事件......感觉一个游戏世界差不多能跑起来了?
别急,还有个大问题!🤔
场景里有几千上万个GO,每次炮弹爆炸、每次物理碰撞检测,都要遍历所有物体吗?
那性能不得炸了?💥
🎯 5.1 N²的噩梦------为什么不能全遍历
先给大家算笔账:
假设场景里有10000个GO。每次爆炸要检测所有物体是否在爆炸范围内,那就要做10000次距离判断。
如果每一帧有10个爆炸,那就是 10 × 10000 = 10万次运算......好像还能接受?
但是!如果是物理碰撞检测呢?每个物体都要和其他所有物体检测是否碰撞,那就是 N × (N-1) ≈ N² 次!
10000个物体的话,就是 10000 × 10000 = 1亿次!😱
一帧1亿次运算?你的CPU怕是要冒烟了~🔥
📌 划重点 :这就是游戏引擎的经典挑战------N²问题!
每个物体都可能和其他物体交互,如果每次都全量遍历,计算量就是N的平方,物体一多直接卡死。
那怎么办呢?
💡 答案是:分而治之(Divide and Conquer)!
不要每次都查整个世界,只查「可能会发生交互的那一小块区域」就行~
🎯 5.2 入门方案:均匀网格------画格子大法
最简单的分而治之是什么?画格子啊!🧱
把整个世界切成一个个大小相同的格子,每个格子里放这个格子内的物体。
当你要查询「某个点周围5米内的物体」时:
-
先算出这个点在哪个格子里
-
再查这个格子以及周围的几个格子
-
只对这些格子里的物体做精确距离判断
这样一来,需要检测的物体数量就大大减少了!
💻 【C++代码示例】均匀网格实现
// 均匀网格空间管理器
class GridManager {
public:
float cellSize = 10.0f; // 每个格子的大小
// key: 格子坐标(x, z), value: 这个格子里的物体列表
std::unordered_map<std::pair<int, int>, std::vector<GameObject*>> grid;
// 计算某个位置属于哪个格子
std::pair<int, int> GetCellIndex(const Vector3& pos) {
int x = static_cast<int>(std::floor(pos.x / cellSize));
int z = static_cast<int>(std::floor(pos.z / cellSize));
return {x, z};
}
// 添加物体到网格
void AddObject(GameObject* obj) {
auto cell = GetCellIndex(obj->GetPosition());
grid[cell].push_back(obj);
}
// 查询某个点周围的物体
std::vector<GameObject*> GetObjectsInRadius(const Vector3& center, float radius) {
std::vector<GameObject*> result;
// 计算需要检查的格子范围
int minX = static_cast<int>(std::floor((center.x - radius) / cellSize));
int maxX = static_cast<int>(std::floor((center.x + radius) / cellSize));
int minZ = static_cast<int>(std::floor((center.z - radius) / cellSize));
int maxZ = static_cast<int>(std::floor((center.z + radius) / cellSize));
// 遍历相关格子
for (int x = minX; x <= maxX; x++) {
for (int z = minZ; z <= maxZ; z++) {
auto it = grid.find({x, z});
if (it != grid.end()) {
// 对格子里的每个物体做精确距离检测
for (auto* obj : it->second) {
float dist = Vector3::Distance(center, obj->GetPosition());
if (dist <= radius) {
result.push_back(obj);
}
}
}
}
}
return result;
}
};
👆 是不是很简单?画格子大法好!
但是......均匀网格有个致命问题:如果物体分布不均匀呢?
🎮 举个例子:
在战地这类游戏里,玩家能走的地方(战壕、道路、城市)物体特别密集,而野外、山里物体特别稀疏。
如果你把格子画得很大,那密集区域的格子里还是有好多物体,性能提升有限;
如果你把格子画得很小,那稀疏区域有好多空格子,浪费内存,而且查询时要遍历好多格子。
这就是均匀网格的痛点:**「旱的旱死,涝的涝死」**~😂
🎯 5.3 进阶方案:四叉树/八叉树------自适应空间划分
既然均匀格子不行,那能不能「按需划分」呢?
物体多的地方,格子划细一点;物体少的地方,格子划粗一点。
💡 这就是**四叉树(Quadtree)**的思想!
📌 四叉树原理:
-
整个世界是一个大正方形节点
-
如果一个节点里的物体数量超过阈值,就把它四等分,变成四个子节点
-
每个子节点继续这个过程,直到节点里的物体足够少,或者达到最小尺寸
这样就形成了一棵树:根节点是整个世界,叶子节点是一个个小区域,每个叶子节点里的物体数量都不多。
🌳 就像行政区划一样:世界 → 国家 → 省份 → 城市 → 街区......一级一级细分。
查询的时候,从根节点开始,只往「可能包含目标区域」的子节点走,很快就能定位到需要检查的物体。
💻 【C++代码示例】四叉树实现
// 四叉树节点
class QuadtreeNode {
public:
// 节点的边界(AABB:轴对齐包围盒)
Vector2 center;
float halfSize;
// 子节点:0=左上, 1=右上, 2=左下, 3=右下
QuadtreeNode* children[4] = {nullptr};
// 这个节点里的物体
std::vector<GameObject*> objects;
// 最大物体数量(超过就分裂)
static constexpr int MAX_OBJECTS = 4;
// 最小节点大小(小于就不再分裂)
static constexpr float MIN_SIZE = 1.0f;
QuadtreeNode(const Vector2& c, float hs) : center(c), halfSize(hs) {}
// 是否是叶子节点
bool IsLeaf() const {
return children[0] == nullptr;
}
// 插入物体
void Insert(GameObject* obj) {
// 如果不是叶子,先往子节点插
if (!IsLeaf()) {
int childIndex = GetChildIndex(obj->GetPosition());
children[childIndex]->Insert(obj);
return;
}
// 是叶子,先加进来
objects.push_back(obj);
// 如果超过最大数量,而且还能再分裂,就分裂
if (objects.size() > MAX_OBJECTS && halfSize > MIN_SIZE) {
Split();
}
}
// 分裂成四个子节点
void Split() {
float quarterSize = halfSize * 0.5f;
children[0] = new QuadtreeNode(
Vector2(center.x - quarterSize, center.y + quarterSize), quarterSize);
children[1] = new QuadtreeNode(
Vector2(center.x + quarterSize, center.y + quarterSize), quarterSize);
children[2] = new QuadtreeNode(
Vector2(center.x - quarterSize, center.y - quarterSize), quarterSize);
children[3] = new QuadtreeNode(
Vector2(center.x + quarterSize, center.y - quarterSize), quarterSize);
// 把当前节点的物体分到子节点里
for (auto* obj : objects) {
int childIndex = GetChildIndex(obj->GetPosition());
children[childIndex]->Insert(obj);
}
objects.clear();
}
// 判断物体属于哪个子节点
int GetChildIndex(const Vector3& pos) {
int index = 0;
if (pos.x > center.x) index += 1;
if (pos.z < center.y) index += 2; // 注意:这里用z轴作为2D的y轴
return index;
}
// 查询范围内的物体
void Query(const Vector2& queryCenter, float queryRadius,
std::vector<GameObject*>& result) {
// 先检查查询范围和当前节点是否相交
if (!Intersects(queryCenter, queryRadius)) {
return;
}
// 如果是叶子,检查里面的物体
if (IsLeaf()) {
for (auto* obj : objects) {
float dist = Vector2::Distance(
Vector2(obj->GetPosition().x, obj->GetPosition().z),
queryCenter);
if (dist <= queryRadius) {
result.push_back(obj);
}
}
return;
}
// 不是叶子,递归查询子节点
for (int i = 0; i < 4; i++) {
children[i]->Query(queryCenter, queryRadius, result);
}
}
// 检查查询圆和节点是否相交
bool Intersects(const Vector2& queryCenter, float queryRadius) const {
// 找到矩形上离圆心最近的点
float closestX = std::clamp(queryCenter.x,
center.x - halfSize, center.x + halfSize);
float closestY = std::clamp(queryCenter.y,
center.y - halfSize, center.y + halfSize);
float dx = queryCenter.x - closestX;
float dy = queryCenter.y - closestY;
return (dx * dx + dy * dy) <= queryRadius * queryRadius;
}
~QuadtreeNode() {
for (int i = 0; i < 4; i++) {
delete children[i];
}
}
};
// 四叉树管理器
class Quadtree {
public:
QuadtreeNode* root = nullptr;
void Build(const Vector2& worldCenter, float worldHalfSize) {
root = new QuadtreeNode(worldCenter, worldHalfSize);
}
void Insert(GameObject* obj) {
if (root) root->Insert(obj);
}
std::vector<GameObject*> Query(const Vector2& center, float radius) {
std::vector<GameObject*> result;
if (root) root->Query(center, radius, result);
return result;
}
~Quadtree() { delete root; }
};
👆 这就是一个完整的四叉树实现了~
是不是很巧妙?物体多的地方自动细分,物体少的地方保持大块,完美解决了均匀网格的痛点!
📌 延伸一下:
• 2D游戏用四叉树(四个子节点)
• 3D游戏用八叉树(Octree)(八个子节点,xyz三个方向都切一刀)
原理都是一样的,就是维度不同~
🎯 5.4 工业级方案:BVH------现代引擎的首选
四叉树/八叉树很好,但还有没有更牛的?
当然有!就是现在3A游戏、光线追踪里最常用的------BVH(Bounding Volume Hierarchy,包围盒层次树)!🏆
📌 BVH原理:
-
每个物体都有一个「包围盒」(Bounding Box),就是刚好能包住这个物体的长方体
-
把相邻的物体两两合并,形成更大的包围盒
-
继续合并,直到最后形成一个根节点的大包围盒
这样就形成了一棵「自底向上」的树:叶子节点是单个物体的包围盒,中间节点是合并后的大包围盒,根节点是整个场景的包围盒。
🌳 和四叉树的区别:
• 四叉树是「自顶向下」按空间均分的,不管物体在哪
• BVH是「自底向上」按物体位置合并的,物体在哪就包到哪
BVH的优势是什么?
✅ 更紧凑:包围盒紧紧贴着物体,没有多余的空白空间
✅ 更高效:查询时排除的节点更多,需要检测的物体更少
✅ 更灵活:适合动态物体、动画物体(虽然更新成本高一点)
所以,现代3A游戏的碰撞检测、光线追踪、视锥裁剪......几乎全是用BVH做的!
💡 小知识:你玩的游戏里,子弹打出去为什么能那么快判断打中了什么?背后就是BVH在默默工作~
🎯 5.5 怎么选?------不同场景的最佳实践
说了这么多,到底该用哪个?
📌 选择指南:
🎮 小型2D游戏(比如马里奥、小手游):
→ 均匀网格就行!简单、好写、性能够用
🎮 中型3D游戏、俯视角游戏:
→ 四叉树/八叉树!效果好,实现难度中等
🎮 3A大作、光线追踪、复杂物理:
→ BVH!性能最强,虽然实现复杂,但值得
当然,实际商业引擎里往往是多种方案混合使用的,不同的子系统用不同的空间加速结构~
📌 配套管理手段:
除了空间加速,还有两个基础管理手段:
• 🆔 UID 全局唯一ID:每个GO分配一个独立编号,像身份证号一样,方便快速定位、索引物体
• 📍 空间坐标存储:每个GO都有空间坐标,配合空间树完成范围检索
🎯 第六章:全文总结------五大核心,构建你的游戏世界
好,讲到这里,现代游戏引擎构建世界的核心机制就全部讲完了!🎉
我们来做个大总结,把知识点串起来~
🎯 6.1 五大核心机制
📌 第一核心:万物皆对象 ------ Game Object(GO)
场景里的一切实体、逻辑载体,全部统一抽象为GO。不管是坦克、房子、检测体、还是游戏规则,都是GO!
📌 第二核心:组件化架构 ------ Component
GO是空容器,所有数据和逻辑都拆成独立的组件。想要什么功能就加什么组件,灵活组合,彻底解决继承地狱。
📌 第三核心:帧驱动更新 ------ Tick
每隔固定时间,整个世界往前走一步。现代引擎采用「按系统批量Tick」的流水线架构,缓存友好,性能爆炸。
📌 第四核心:事件消息系统 ------ Event
GO之间通过「发布-订阅」的事件机制交互,彻底解耦。发送方不需要知道接收方是谁,扩展性极强。
📌 第五核心:空间加速结构 ------ 四叉树/八叉树/BVH
分而治之,解决N²性能问题。只查询可能相关的局部区域,大幅减少计算量,让大世界也能流畅运行。
🎯 6.2 一句话记住
💡 现代游戏引擎 = GO容器 + 组件积木 + Tick心跳 + 事件神经 + 空间骨架
掌握了这五大核心,你就理解了现代游戏引擎的底层逻辑。从Unity到Unreal,从独立游戏到3A大作,核心思想都是相通的~
🎯 6.3 写在最后
其实游戏引擎的设计哲学,和现实世界的很多道理是相通的:
• 组件化告诉我们:高内聚,低耦合,各司其职才好维护
• 批量Tick告诉我们:流水线作业,专业的人干专业的事,效率才最高
• 事件系统告诉我们:不要强耦合,通过标准化接口沟通,大家都轻松
• 空间加速告诉我们:分而治之,大问题拆成小问题,逐个击破
技术的底层,往往是最朴素的道理~
好了,今天的内容就到这里啦~希望这篇文章能帮你打开游戏引擎世界的大门!🚀
如果觉得有帮助,别忘了**点赞👍 收藏⭐ 关注👀**三连哦~你们的支持是我更新的最大动力!
我们下篇文章再见~👋

📚 参考资料:
• Games104 现代游戏引擎:理论与实践
• Game Engine Architecture (Jason Gregory)
• Unity / Unreal Engine 官方文档
🏷️ 关键词:游戏引擎 | GameObject | 组件化 | ECS | Tick | 事件系统 | 四叉树 | BVH | 空间加速 | C++ | 游戏开发