Games104学习笔记(3)
GAMES104:如何构建游戏世界
课程:GAMES104《现代游戏引擎:从入门到实践》
讲次:Lecture 03 / How to Build a Game World
讲者:王希(Xi Wang,Booming Tech)
视频时长:约 59 分 17 秒
笔记性质:根据本地视频中的幻灯片与讲解主题整理,不是逐字稿;时间点为复习定位用的近似值。
一句话理解
游戏世界可以看成由大量 Game Object(游戏对象,GO) 构成的动态系统。对象自身保存属性并通过组件获得能力;引擎再用 Tick、事件、依赖关系和场景管理,持续推进这些对象的状态变化。
Game Object
├─ 属性:位置、旋转、速度、血量、资源引用……
├─ 组件:模型、动画、物理、输入、脚本……
├─ Tick:随时间更新状态
└─ 事件:与其他对象或系统通信
本讲学习目标
- 了解游戏世界中有哪些对象,以及为什么很难做出完美分类。
- 理解 Game Object 与 Component 的关系。
- 比较对象式 Tick 和组件式 Tick。
- 理解事件机制、组件依赖和场景管理的作用。
- 认识 Unity、Unreal 等商业引擎中的组件化思路。
- 建立从“对象—组件—系统—场景”理解游戏世界的框架。
1. 游戏世界由什么组成
1.1 Everything is a Game Object
从引擎的角度看,游戏世界中的元素都可以抽象成 Game Object:
- 动态物体:角色、飞机、无人机、车辆、敌人等。
- 静态物体:建筑、墙壁、地面、装饰物等。
- 环境系统:地形、天空、植被和天气等。
- 检测体:触发器、区域、空气墙和碰撞体等。
- 可视对象:模型、粒子、特效和 UI 等。
- 规则或逻辑对象:任务、关卡状态、出生点和游戏管理器等。
一个 GO 通常至少包含两类信息:
- 属性(Property):描述对象当前是什么状态,例如位置、旋转、速度、生命值和资源引用。
- 行为(Behavior):描述对象能够做什么,例如移动、巡逻、攻击、播放动画和响应事件。
1.2 对象不是简单的“一个类”
如果为每一种对象都设计一个完整的继承类,初期看起来直观,但对象类型一多就会遇到问题:
- 某些对象同时具有多种身份,难以放入单一分类。
- 父类层次会越来越深,修改一个基类可能影响大量对象。
- 不同对象之间会出现重复功能。
- 设计师需要的组合方式不一定符合程序员预先设计的继承树。
因此,现代引擎通常将“对象是谁”和“对象能做什么”分开处理。
2. 没有完美的游戏对象分类
视频用飞机、直升机、无人机等例子说明:现实世界和游戏世界中的分类边界并不总是清晰。
例如,一个对象可能同时具备:
- 飞行能力。
- 载人能力。
- 武器系统。
- 物理碰撞。
- AI 控制。
- 可被玩家操控。
- 可播放动画或特效。
如果把这些能力全部写进一个“飞机类”或“角色类”,分类会不断膨胀。更灵活的方式是把能力拆出来,让对象按需求组合:
对象 = 基础身份 + 一组可组合能力
战斗直升机 = Transform
+ Mesh Renderer
+ Physics
+ Motor
+ Weapon
+ AI 或 Player Input
组件化并不是否定对象,而是把对象的复杂行为拆成更容易复用和管理的部分。
3. Component:组件化
3.1 Game Object 与 Component
组件是挂载到 Game Object 上的功能模块。一个对象通常由多个组件共同组成:
Game Object
├─ Transform Component
├─ Render / Mesh Component
├─ Animation Component
├─ Physics Component
├─ Input Component
└─ Script / Gameplay Component
组件化带来的好处:
- 复用:相同能力可以挂到不同对象上。
- 组合:对象可以按项目需要组合能力。
- 扩展:增加新能力时不必修改庞大的继承树。
- 解耦:各组件关注自己的职责,通过接口、事件或数据通信。
- 编辑器友好:设计师可以在编辑器中添加、删除和配置组件。
3.2 商业引擎中的组件化
视频展示了 Unity 和 Unreal 的对象结构作为例子。不同引擎的命名和实现方式不同,但共同思想是:
- 引擎对象负责生命周期、标识和基本管理。
- 组件负责具体能力。
- 编辑器通过组件暴露对象的可配置部分。
- 运行时由多个组件共同完成对象行为。
可以用下面的抽象理解它们:
引擎对象基类
↓
游戏对象 / Actor / GameObject
↓
多个功能组件
↓
对象在场景中的具体行为
组件化并不意味着组件之间没有关系。动画可能依赖骨骼和模型,物理可能依赖碰撞形状,角色移动可能依赖输入或 AI,因此还需要显式管理组件依赖。
4. Tick:让对象随时间更新
4.1 Tick 的基本含义
游戏世界不是一组静态文件。引擎必须不断推进时间并更新对象状态,这个周期性更新过程通常称为 Tick。
读取时间与输入
↓
更新对象和组件
↓
处理事件与依赖
↓
更新动画、物理和其他系统
↓
得到新的世界状态
一个简单的对象式更新可能类似于:
for (GameObject* object : objects) {
object->tick(delta_time);
}
这种写法直观,容易理解对象的完整行为,也便于早期调试。
4.2 Object-based Tick
对象式 Tick 以对象为中心:每次取出一个 Game Object,再让对象内部的所有功能依次更新。
优点:
- 结构简单、直观。
- 对象的行为集中,容易跟踪。
- 初学和小型项目容易实现。
- 调试一个对象时上下文比较完整。
局限:
- 不同对象的同类操作可能被分散执行。
- 数据访问不够连续,缓存利用率可能较低。
- 组件之间的并行机会不容易暴露。
- 对象数量增加后,调度和依赖关系会变复杂。
4.3 Component-based Tick
组件式 Tick 以组件类型或系统为中心:先收集同类组件,再批量处理它们。
for (TransformComponent* transform : all_transforms) {
update_transform(transform, delta_time);
}
for (PhysicsComponent* physics : all_physics) {
update_physics(physics, delta_time);
}
优点:
- 同类数据和操作更集中。
- 更适合批处理和并行处理。
- 有利于减少缓存未命中。
- 可以降低对象之间不必要的访问。
- 更容易把工作拆成系统任务。
代价:
- 对象的完整行为被分散到多个系统。
- 需要明确组件之间的执行顺序和依赖。
- 调试一条对象行为链时,需要跨多个系统追踪。
- 组件访问和生命周期管理更复杂。
4.4 两种方式并非绝对对立
对象式 Tick 适合强调直观性和开发便利性的场景;组件式 Tick 适合对象数量大、批处理和并行要求高的场景。实际引擎可能混合使用:
- 由对象管理生命周期。
- 由组件或系统执行批量更新。
- 对特殊对象保留独立 Tick。
- 对可并行工作使用线程池或任务系统。
选择取决于对象规模、数据布局、系统依赖和性能目标。
5. Event:对象之间的通信
5.1 为什么需要事件
如果每个对象直接调用其他对象的具体函数,会形成紧密依赖:
玩家类 → 直接知道敌人类
敌人类 → 直接知道 UI 类
UI 类 → 直接知道关卡类
对象数量增多后,修改一个系统就可能影响许多其他系统。事件机制可以让发送者只表达“发生了什么”,而不必了解所有接收者。
事件发送者
↓ 发布“受到伤害”
事件系统 / 消息分发
├─ Health Component
├─ UI Component
├─ 音效系统
└─ 特效系统
5.2 事件的优点
- 降低对象之间的直接耦合。
- 让多个系统响应同一件事。
- 便于增加新响应者,而不修改原发送者。
- 适合碰撞、伤害、任务、输入和关卡触发等场景。
5.3 事件设计中的问题
事件并不是越多越好,需要考虑:
- 事件由谁发送、谁接收?
- 接收者是否会被销毁或失效?
- 事件是立即处理还是延迟到下一帧?
- 事件顺序是否重要?
- 多线程下如何保证安全?
- 事件链是否会形成循环?
- 调试时能否追踪事件来源和去向?
5.4 立即事件与延迟事件
视频展示了商业引擎中常见的事件 / 消息调用方式。可以粗略区分为:
- 立即事件:发送后立即调用接收者,逻辑直接,但可能产生深层调用链。
- 延迟事件:先放入队列,在指定阶段统一处理,便于排序、跨线程和批处理,但会引入延迟。
选择哪种方式,取决于响应速度、确定性、线程模型和系统依赖。
6. 组件依赖与对象绑定
6.1 组件之间存在依赖
组件并不是完全独立的。例如:
- Motor 需要 Transform 才能改变位置。
- Animation 需要 Skeleton 或 Mesh 才能播放。
- Physics 需要碰撞形状和 Transform。
- Render 需要 Mesh、Material 和 Transform。
- Gameplay 可能需要 Input、Camera 和 Physics。
可以把依赖关系表示为:
Input ───────┐
↓
Motor ─→ Transform ─→ Render
↓ ↑
Physics ───────┘
↑
Collision
6.2 依赖管理的目标
- 在对象创建时检查所需组件是否存在。
- 明确哪个组件拥有数据,哪个组件只读取数据。
- 控制组件的初始化和销毁顺序。
- 保证 Tick 顺序与数据依赖相符。
- 避免组件之间形成难以维护的双向依赖。
- 让编辑器能够提示缺少的组件或配置。
6.3 绑定关系
Game Object 的绑定关系可以包含:
- 对象与组件的绑定。
- 父对象与子对象的层级绑定。
- 组件与资源的引用绑定。
- 系统与事件的订阅绑定。
- 功能之间的依赖绑定。
绑定关系越多,越需要稳定的对象标识、清晰的生命周期和可追踪的依赖图。
7. Scene Management:场景管理
7.1 为什么需要场景管理
当场景中的对象很少时,遍历全部对象还可以接受;当场景包含大量对象时,每次查询都扫描整个世界会产生很高的成本。
常见查询包括:
- 哪些对象位于摄像机视野内?
- 某个范围内有哪些敌人?
- 射线可能击中哪些碰撞体?
- 哪些对象需要加载或卸载?
- 哪些对象可以参与当前物理计算?
场景管理的目标是把“在一大堆对象中找一个对象”变成更快的空间查询。
7.2 不分区与分区
最简单的方式是把所有对象放在一个列表中:
查询区域
↓
遍历整个场景中的所有对象
↓
逐一判断是否命中
这种方式容易实现,但对象数量增加后效率较低。
分区方法会把空间划分成多个区域:
┌────┬────┬────┬────┐
│ │ │ │ │
├────┼────┼────┼────┤
│ │查询│ │ │
├────┼────┼────┼────┤
│ │ │ │ │
└────┴────┴────┴────┘
查询时只检查相关区域,从而减少无关对象的判断。
7.3 常见的空间组织方式
- 网格(Grid):实现简单,适合规则空间和快速邻域查询。
- 四叉树(Quadtree):递归划分二维空间。
- 八叉树(Octree):递归划分三维空间。
- BVH(Bounding Volume Hierarchy):用包围体组织对象层级。
- 场景图(Scene Graph):按层级关系组织对象和变换。
- 区域流式加载:按玩家或摄像机位置加载、卸载场景块。
7.4 分区的取舍
空间分区不是免费优化:
- 对象移动时需要更新所属区域。
- 动态对象可能跨越多个区域。
- 分区结构本身需要内存和维护成本。
- 不同查询类型适合不同数据结构。
- 过度细分会增加管理开销。
合适的方案取决于场景大小、对象运动方式、查询类型和目标平台。
8. 从对象到整个游戏世界
可以把本讲内容串成一条层次关系:
游戏世界
└─ 场景 Scene
└─ Game Object
├─ 属性与标识
├─ Component 组件
│ ├─ Transform
│ ├─ Render
│ ├─ Animation
│ ├─ Physics
│ └─ Gameplay
├─ Tick 更新
├─ Event 通信
└─ 与其他对象的绑定和依赖
引擎需要同时解决四个问题:
- 对象如何表示?
- 能力如何复用和组合?
- 对象如何随时间更新?
- 大量对象如何被高效查询和管理?
Game Object、Component、Tick、Event 和 Scene Management 分别回答了这四个问题。
9. 本讲最重要的结论
9.1 对象是世界中的实体,组件是能力的组织方式
Game Object 负责成为场景中的对象,Component 负责提供可复用的能力。二者结合后,既能表达世界中的实体,又能避免继承树无限膨胀。
9.2 组件化带来灵活性,也带来依赖管理问题
拆分组件可以提高复用和扩展能力,但组件之间仍然存在数据依赖、初始化顺序和 Tick 顺序。组件化不是简单地把代码拆成多个类。
9.3 Tick 决定世界如何随时间演进
对象式 Tick 易懂,组件式 Tick 更适合批处理和并行。大型引擎通常会在可读性、缓存效率、并行能力和调试成本之间取舍。
9.4 事件机制用于降低耦合
事件让系统围绕“发生了什么”通信,而不是互相了解内部实现。事件的时机、顺序、生命周期和线程安全必须被明确设计。
9.5 场景管理决定大世界查询效率
场景管理的核心思想是:不要每次都在全部对象中搜索,只检查和查询目标相关的空间区域或层级。
9.6 游戏世界的组织方式会影响性能
对象结构、组件布局、Tick 调度、事件队列和空间分区,既是架构问题,也是性能问题。引擎设计需要同时考虑表达能力和运行效率。
10. 复习用速记
Game Object = 属性 + 行为
组件化:
对象负责身份和生命周期
组件负责具体能力
系统负责批量处理组件
Object-based Tick:
直观、易懂、易调试,但不利于批处理和并行
Component-based Tick:
更适合数据局部性、批处理和并行,但依赖和调试更复杂
Event:
发送“发生了什么”,降低对象之间的直接耦合
Scene Management:
用网格、树、包围体或场景图减少空间查询范围
11. 自测题
- 为什么说游戏世界中的很多东西都可以抽象成 Game Object?
- 为什么现实世界中的分类不能直接变成游戏对象的继承树?
- Game Object 和 Component 各自应该负责什么?
- 对象式 Tick 与组件式 Tick 的主要差别是什么?
- 组件式 Tick 为什么更容易进行批处理和并行?
- 组件化之后,哪些依赖关系必须被显式管理?
- 事件机制如何降低对象之间的耦合?
- 立即事件和延迟事件分别有什么优缺点?
- 场景中对象数量很多时,为什么不能每次都遍历全部对象?
- 网格、四叉树、八叉树和 BVH 分别适合解决什么类型的空间组织问题?
- 如果一个角色移动组件需要 Transform 和 Physics,你会如何表示它们的依赖?
- 组件化、Tick 和场景管理之间为什么会互相影响?
12. 术语表
| 术语 | 含义 |
|---|---|
| Game Object(GO) | 场景中的游戏对象,包含属性、行为和组件 |
| Component | 挂载到对象上的可组合功能模块 |
| Property | 描述对象状态的属性,如位置、速度和血量 |
| Behavior | 描述对象如何行动的逻辑 |
| Tick | 引擎推进一小段时间并更新状态的过程 |
| Object-based Tick | 以对象为中心,逐个更新对象及其功能 |
| Component-based Tick | 以组件或系统为中心,批量更新同类组件 |
| Event | 用于对象或系统之间通信的事件 / 消息 |
| Scene | 一组对象及其空间、资源和状态的集合 |
| Scene Management | 对场景对象进行组织、查询、加载和卸载的系统 |
| Spatial Partitioning | 将空间划分为区域,以减少查询范围 |
| Scene Graph | 按层级组织对象和空间变换的数据结构 |
| GUID | Global Unique Identifier,全局唯一对象或资源标识 |
| DCC | Digital Content Creation,数字内容创建工具 |
13. 参考资料
- GAMES104 B站视频链接
- GAMES104 官方课程主页
- GAMES104 官方课程 PPT 与视频页面
- 推荐阅读:Jason Gregory,《Game Engine Architecture / 游戏引擎架构》