中EN
概念页

世界块数据格式与生成器

World Chunk Data Formats & Spawners

世界块中的动态部分要为每个游戏对象记录两样东西:属性初始值和对象类型。GEA 比较了三种存储方式:二进制对象映像(生成最简单,但要处理指针、虚表和字节序,且对变化很脆弱)、序列化描述(XML/JSON/私有二进制格式,配合反射与类工厂),以及生成器 (spawner) + 类型模式 (type schema):生成器是只含类型 id 和键值对的轻量数据块,彻底切断世界编辑器与运行时代码的耦合;模式里的默认值配合"与默认值相同就省略键"可以让默认值的修改自动传播到旧实例(GEA4 卷 II 17.3)。

世界块里有什么

静态部分:一张大三角网格或多块小网格(同一网格可多次实例化,例如所有门口共用一个门网格)、以三角形汤或凸形/平面/盒/胶囊/球表示的碰撞数据、检测事件或划分区域的体积、AI 导航网格、可供玩家角色攀抓的边缘线段等;GEA 说这些格式在前面章节已讨论过(卷 II 17.3)。

动态部分是游戏对象。对象的行为直接或间接由类型决定:以对象为中心时,类型直接决定实例化哪些类;以属性为中心时,行为是属性行为的合成,但类型仍决定对象有哪些属性(也可以说属性定义了类型)。所以每个对象要存:

  • 属性初始值:对象第一次生成到世界中时的状态;
  • 类型说明:以对象为中心时可以是字符串、哈希字符串 id 或其他唯一类型 id;以属性为中心时可以显式存储,也可以由属性集合隐式定义。

架构分类见 运行时对象模型架构。

二进制对象映像

把每个对象在运行时内存里的样子原样写入文件,加载后对象"现成"可用(卷 II 17.3.1)。问题:

  • 需要特殊处理指针和虚表,可能还要做字节序交换(卷 I 7.2.2.9,见 资源交叉引用与加载后初始化);
  • 不灵活、对修改不健壮,而玩法是项目中最易变的部分。

所以 GEA 认为它通常不适合存游戏对象,但适合网格、碰撞几何等更稳定的数据结构。

序列化的游戏对象描述

写出时让对象产生足以重建自身的数据流;读入时先创建相应类的实例,再读属性流初始化内部状态。它比二进制映像更可移植、更容易实现(卷 II 17.3.2)。

  • 语言支持:C# 和 Java 原生提供到 XML 的标准序列化,C++ 没有标准设施,但业内外有许多成功的 C++ 序列化系统。
  • 格式:XML 标准、较易读、擅长层次数据,但解析出了名地慢、会拉长加载时间,所以有引擎用更快更紧凑的私有二进制格式;很多引擎转向 JSON(GEA 举例说 Facebook API 只用 JSON 通信)。
  • 实现方式:(1) 基类提供 SerializeOut() / SerializeIn() 虚函数,派生类各自实现;(2) 为 C++ 类实现反射,再写通用的序列化系统。反射数据是类内容的运行时描述:类名、数据成员及其类型和在对象内存映像中的偏移,以及所有成员函数。难点是为所有相关类生成反射数据:用 #define 宏包裹数据成员、由派生类重写返回反射数据的虚函数、为每个类手写反射结构,或其他巧妙方法。
  • 类工厂:数据流总会包含对象的类名或类 id(字符串、哈希字符串 id 等),用于读入时实例化正确的类。C++ 无法仅凭名字或 id 创建实例,类名必须在编译期硬编码(如 new ConcreteClass),所以 C++ 序列化系统总会带一个类工厂:最简单是一张数据表,把类名/id 映射到硬编码实例化该类的函数或函数对象。

UE 的对应:UE 的反射由 UHT 根据 UCLASS/UPROPERTY 宏生成代码,序列化、编辑器详情面板和蓝图都建立在它之上,见 UObject 与反射系统。这属于 GEA 列举的"用宏提取反射信息"一类的实践(对比归纳)。

生成器与类型模式

二进制映像和序列化有同一个"阿喀琉斯之踵":它们都由运行时对象实现来定义,所以世界编辑器必须深入了解引擎运行时实现,要么链接运行时代码,要么精心手写与运行时布局一致的字节块;序列化耦合较松,但仍需访问 SerializeIn/Out 或反射信息(卷 II 17.3.3)。

生成器 (spawner) 用与实现无关的方式描述对象,从而切断这种耦合:它是轻量的、只有数据的对象表示,包含工具侧类型 id 和一张描述初始属性的简单键值对表,通常含模型到世界的变换。生成时按类型实例化相应的类,运行时对象再从键值字典初始化数据成员。

  • 可以加载后立即生成,也可以休眠,等游戏中稍后被要求时再生成;
  • 可以实现为一等对象,有函数接口,还能存元数据;
  • 还能用于生成对象以外的用途:Naughty Dog 引擎里策划用位置生成器/定位器 (locator) 定义重要的点或坐标轴,用于 AI 兴趣点、让一组动画完美同步播放的参考坐标系、粒子或音效的发出位置、赛道路点等。

对象类型模式 (type schema):在基于生成器的编辑器里,对象类型可以用数据驱动的模式来表示,定义该类型在编辑器中可见的属性集合;运行时再以硬编码或数据驱动的方式把工具侧类型映射到要实例化的类(卷 II 17.3.3.1)。模式可以存为简单文本文件,GEA 的示例:

enum LightType { Ambient, Directional, Point, Spot }

type Light {
    String     UniqueId;
    LightType  Type;
    Vector     Pos;
    Quaternion Rot;
    Float      Intensity : min(0.0), max(1.0);
    ColorARGB  DiffuseColor;
    ColorARGB  SpecularColor;
    // ...
}

type Vehicle {
    String        UniqueId;
    Vector        Pos;
    Quaternion    Rot;
    MeshReference Mesh;
    Int           NumWheels : min(2), max(4);
    Float         TurnRadius;
    Float         TopSpeed : min(0.0);
    // ...
}

(注:PDF 提取文本把这段的类型列和名字列拆开了,这里按语义重排。)

要点:每个属性有名字和数据类型,类型可以是简单类型,也可以是向量、四元数、ARGB 颜色、网格/碰撞等资产引用,还可以定义枚举;模式还给编辑器提供额外信息,如用什么 GUI 控件编辑(字符串用文本框,布尔用复选框,向量用三个文本框或专门的 3D 操纵控件)、数值的最小最大值、下拉框的可选项。有的引擎允许模式像类一样继承,例如类型和唯一 id 放在顶层模式里,其他模式都从它派生。

默认属性值

模式里属性很多,每放一个实例都要填大量数据,所以在模式里定义默认值很有帮助:策划可以轻松摆放"原味"实例,需要时再对单个实例微调(卷 II 17.3.3.2)。

难点在默认值改变时:例如兽人 (Orc) 原定 20 点生命,几个月后改为默认 30 点;新放的兽人会是 30,那之前放下的呢?理想是让默认值的变化自动传播到所有没有显式覆盖该值的已有实例。简单做法:与默认值相同的属性就不写键值对,生成器缺这个键时就用默认值。这要求引擎能读到模式文件里的默认值;也可以由工具来填,这种情况下传播新默认值需要重新构建受影响的世界块。于是多数旧兽人生成器根本没有 HitPoints 键,默认值从 20 改为 30 后它们自动用新值。

有的引擎还允许派生类型覆盖默认值:Vehicle 模式的默认 TopSpeed 为 80 英里/小时,派生的 Motorcycle 模式可以改成 100。

与 UE 的对照:本库 UObject 与反射系统 依据 UE 文档记录,UProperty 序列化时等于默认值的属性不写出,新增属性取新 CDO 默认值,删除的属性被静默忽略;关卡中已放置的实例,属性仍等于旧 CDO 值的加载时会更新为新值,单独改过的保留(官方例子:敌人血量由 100 改为 150)。这与 GEA 的"与默认值相同就省略键""不认识的键忽略、缺少的键用默认值"是同一思路,CDO 扮演了类型模式中默认值的角色(对比归纳),另见 原型模式 的 UE 对应一节。

生成器与类型模式的好处

(卷 II 17.3.3.3)

  • 简单:管理键值对表比管理需要指针修正的二进制映像或自定义序列化格式简单得多。
  • 灵活、健壮:对象遇到不认识的键可以忽略,缺少需要的键可以用默认值,所以对策划和程序员的修改都很健壮。
  • 简化编辑器:编辑器只需要会管理键值对列表和类型模式,不必与运行时引擎共享任何代码,与引擎实现只是很松的耦合(编辑器的整体设计见 数据驱动与游戏世界编辑器)。
  • 解耦进度:策划几乎不需要程序员介入就能在编辑器里定义新类型模式,程序员按自己的排期再实现运行时版本;新对象数据可以没有运行时实现而安全地存在于世界块中,运行时实现也可以没有对应数据。

GEA 在这里写"生成器与原型 (archetype)",原型概念与 类型对象 和 原型模式 讨论的"用数据定义类型"相通。

  1. Game_Engine_Architecture_4th_Edtion_Volume_2_-_Jason_Gregory.pdf
  2. Jason Gregory《Game Engine Architecture》第 4 版(2026)卷 II 第 17 章 17.3 节

相关页面

游戏开发游戏世界与游戏对象Game World & Game Objects游戏由玩法 (gameplay) 而不是技术定义;游戏机制 (game mechanics) 是支配游戏中各实体交互的规则,以及目标、成败标准、玩家能力、非玩家实体和整体流程(GEA 引用的论文称实现玩法的软件系统为游戏的 G 因子)。大多数游戏世界由静态元素和动态元素组成,划分为若干世界块,并由高层游戏流程组织玩家目标;动态元素通常实现为游戏对象 (GO),引擎提供的建模设施称为游戏对象模型,且分为工具侧和运行时两套(GEA4 卷 II 16、16.1–16.2)。游戏开发数据驱动与游戏世界编辑器Data-Driven Engines & Game World Editors当游戏行为可以部分或全部由美术和策划提供的数据控制,而不只由程序员写的代码控制时,引擎就是数据驱动的。它能提高团队效率、缩短迭代时间,但工具、健壮的运行时和预览手段都要花大量成本,过度设计反而降低生产力,所以要权衡并坚持 KISS。玩法领域里对应 DCC 工具的是游戏世界编辑器:创建世界块并放入静态和动态元素、设定对象初始状态和行为。GEA 列出了它的典型功能,并比较了不同引擎在何时做平台相关优化上的取舍(GEA4 卷 II 16.3–16.4)。游戏开发运行时对象模型架构Runtime Object Model Architectures玩法基础系统 (gameplay foundation system) 是位于"引擎与游戏分界线"附近、供构建游戏规则和动态元素的一组运行时组件,其中最复杂的是运行时对象模型:它在运行时实现世界编辑器展示给策划的抽象对象模型。GEA 把架构分为两大类:以对象为中心(每个工具侧对象对应一个或一小组类实例)和以属性为中心(对象只是一个 id,属性分散在按属性类型划分、以 id 为键的表里)。前者从单体类层次走向组合与组件,后者更省内存、更缓存友好、更易数据驱动,但难以表达属性间关系、也更难调试(GEA4 卷 II 17.1–17.2)。游戏开发资源交叉引用与加载后初始化Resource Cross-References & Post-Load Initialization资源之间以任意方式相互引用(网格引用材质,材质引用纹理),整个资源数据库是一张有向依赖图。内存中的指针离开运行中的程序就失去意义,所以存盘时要把引用改写为 GUID 或文件内偏移,加载后再修正 (fix up) 回指针;含非 POD 的 C++ 对象还要用 placement new 调用构造函数;跨文件的外部引用要先加载全部相关文件。加载后通常还需要按资源类型做加载后初始化(以及释放前的拆除)。游戏开发 · UE5UObject 与反射系统UObject & Reflection System虚幻引擎 (Unreal Engine) 的对象基类是 UObject。在 C++ 类、属性、函数上加 UCLASS/UPROPERTY/UFUNCTION 等宏,再由构建前的预处理工具虚幻头文件工具 (Unreal Header Tool, UHT) 扫描头文件生成代码,引擎就能在运行时“看见”这些类型——这就是反射系统 (Reflection System)。垃圾回收、序列化、编辑器面板、蓝图、网络复制都建立在它之上。游戏开发原型模式Prototype Pattern原型模式 (Prototype Pattern) 的核心思想是“一个对象可以生成与自己相似的对象”:生成器持有一个原型实例,通过 clone() 复制出类和状态都相同的新对象。Nystrom 认为这个设计模式本身在现代游戏中很少是最佳答案,更有价值的是“原型委托 (delegation)”用于数据建模,让数据条目继承另一条目的属性,减少重复。游戏开发类型对象Type Object类型对象 (Type Object) 用“一个类的不同实例”来表示不同的逻辑类型:例如不为每种怪物写一个子类,而是一个 Monster 类加一个 Breed(种族)类,每个 Breed 实例描述一种怪物,怪物只持有指向自己 Breed 的引用。这样新增或修改种类只需要改数据,不必重新编译;代价是要自己管理这些类型对象,而且给每种类型定义不同的“行为”会变难。游戏开发游戏世界的加载与流送Loading & Streaming Game Worlds世界加载系统负责两件事:把世界块和所需资源从磁盘读进内存的文件 I/O,以及这些资源的内存分配与释放;另外还要管理游戏对象的生成与销毁。GEA 按演进顺序介绍:一次只载一个关卡的栈式加载(LSR 资源压栈底),用小块气闸 (air lock) 区域实现基本无缝的过渡,再到把所有资产切成等大块、用块池分配器按关卡加载区域流送的细粒度方案(Naughty Dog);对象生成的内存问题有"不许运行时分配"、按尺寸分池、小内存分配器和内存重定位四种对策;存档则分检查点和随处存档(GEA4 卷 II 17.4)。游戏开发字符串与唯一标识符Strings & Unique Identifiers字符串在 C/C++ 中只是字符数组,变长意味着要么硬编码长度上限、要么动态分配;比较和复制都是 O(n)。引擎内部大量用字符串做资源名和对象 ID,处理不当会严重拖慢游戏。GEA 的建议:运行时代码尽量避免字符串类,若用则了解其成本并按引用传递;路径类是值得专门做的例外;对象与资产的唯一标识用哈希字符串 ID(string id),兼得字符串的可读性与整数的比较速度。综合引擎运行时架构总览Engine Runtime Architecture Overview本页把 wiki 中分散在 GEA 第 1、6、8、17 章及 GPP、UE5 相关页面里的内容串成一条线,回答"一个游戏引擎在运行时是怎样组织起来的":静态结构上是自下而上的分层,上层依赖下层;启动关闭按依赖顺序显式进行;每一帧由主循环驱动各子系统批量更新,游戏对象的逻辑被安排在子系统阶段之间,并按依赖分桶或设先后关系;多核上一帧被拆成成百上千个作业;玩法基础系统(对象模型、事件、脚本、世界加载流送、高层流程)位于引擎与游戏的分界线附近。各结论的细节与出处见所链接的概念页。

输入关键词开始搜索

滚轮或双指缩放 · 拖动平移 · 双击放大 / 还原