中EN
概念页

游戏世界的加载与流送

Loading & Streaming Game Worlds

世界加载系统负责两件事:把世界块和所需资源从磁盘读进内存的文件 I/O,以及这些资源的内存分配与释放;另外还要管理游戏对象的生成与销毁。GEA 按演进顺序介绍:一次只载一个关卡的栈式加载(LSR 资源压栈底),用小块气闸 (air lock) 区域实现基本无缝的过渡,再到把所有资产切成等大块、用块池分配器按关卡加载区域流送的细粒度方案(Naughty Dog);对象生成的内存问题有"不许运行时分配"、按尺寸分池、小内存分配器和内存重定位四种对策;存档则分检查点和随处存档(GEA4 卷 II 17.4)。

简单关卡加载

最早的游戏一次只允许载入一个世界块(关卡),开始游戏和关卡之间显示静态或简单动画的二维加载画面(卷 II 17.4.1)。内存管理很简单,非常适合栈分配器(卷 I 7.2.2.7,见 自定义内存分配器):

flowchart LR
  A["载入 LSR 数据<br/>记下栈标记"] --> B["载入关卡 A 的资源"]
  B --> C["卸载关卡 A:<br/>栈指针退回标记"]
  C --> D["载入关卡 B 的资源"]

上图对应 GEA 图 17.11:游戏启动时把所有关卡都要用的载入并常驻 (load and stay resident, LSR) 资源载入栈底并记录栈指针;每个世界块及其网格、纹理、音频、动画等资源压在 LSR 之上;关卡完成后把栈指针重置到 LSR 顶部即释放全部关卡内存。缺点:玩家只能看到离散的块,无法做巨大连续的无缝世界;加载期间内存里没有游戏世界,只能看加载画面。

走向无缝加载:气闸

最好的办法是让玩家在下一个世界块加载时继续玩(卷 II 17.4.2)。

  • 把世界内存平分两块,玩 A 时在另一块里用流式文件 I/O(在单独线程中加载)载入 B:每个关卡大小因此减半。
  • 改为不等分:一块能装"完整"世界块,一块只够装很小的块,即气闸。开始时载入一个完整块和一个气闸块;玩家穿过完整块进入气闸后,某种门或障碍保证他既看不到也回不去之前的区域,于是卸载完整块、载入新的完整块;加载期间玩家在气闸里做点事,可以只是走过一段走廊,也可以是解谜或打敌人。
  • 这依赖异步文件 I/O(卷 I 7.1.3,见 引擎文件系统与异步文件读写)。开新游戏时仍需要加载画面,因为初次加载时内存里没有可玩的世界;之后玩家就不必再看加载画面。
  • 实例:Xbox 上的《光环》大区域之间总有较小较封闭、防止回头的区域,大约每 5–10 分钟遇到一个;PS2 上的《杰克 2》以主城为中心,各分支区域通过狭小的气闸区与主城相连。

游戏世界流送

很多设计要求玩家感到身处巨大、连续、无缝的世界,最好不要周期性地被关进气闸。现代引擎用流送 (streaming) 实现,目标总是:(a) 在玩家正常游玩时加载数据;(b) 管理内存以消除碎片,同时允许按需加载卸载(卷 II 17.4.3)。

  • 粗粒度:内存分成例如三个等大缓冲,先载入 A、B、C,玩家进入 B 且看不到 A 后卸载 A、载入 D,依此循环。问题是所有块必须大致等大:足够填满一个缓冲的大部分,但绝不能更大。
  • 细粒度:把所有资产(世界块、前景网格、纹理、动画库……)都切成等大的数据块,用块式、基于池的分配系统(卷 I 7.2.2.7)按需加载卸载,不必担心碎片。GEA 说这基本就是 Naughty Dog《最后生还者》引擎的做法,另外还有利用未填满块末尾空间的精巧技术。

决定载入哪些资源:关卡加载区域

Naughty Dog 引擎用相对简单的关卡加载区域 (level load region) 系统(卷 II 17.4.3.1)。神秘海域和最后生还者系列都发生在多个地理上分离的连续世界里(例如《神秘海域 4》的巴拿马、新奥尔良、苏格兰、马达加斯加和海盗乌托邦利伯塔利亚),每个世界有自己的世界空间,又划分为许多相邻的块。

  • 每个块被一个简单的凸体区域包围,区域之间略有重叠;每个区域列出玩家在其中时应在内存中的世界块。
  • 任意时刻玩家位于一个或多个区域内,取包围主角 Nathan Drake 的所有区域的块列表并集作为主块列表。
  • 加载系统定期比较主列表与内存中的块:从列表中消失的块被卸载,释放其占用的所有分配块;新出现的块被载入到能找到的空闲分配块里。
  • 区域与块的设计保证玩家永远看不到块消失,并且从开始加载到内容第一次可见之间有足够时间完成流送(GEA 图 17.12)。

PlayGo:PS4 和 PS5 的 PlayGo 只下载玩第一段游戏所需的最小数据子集,其余内容在后台下载、玩家不受打断;前提是游戏支持上面这种无缝关卡流送(卷 II 17.4.3.2)。

与 UE5 的对照:世界分区 把世界划分为网格单元,按流送源(通常是玩家)周围的加载范围自动加载卸载,取代了手工划分子关卡的做法。GEA 的关卡加载区域是由设计者显式放置的凸体,世界分区则是基于网格和距离的自动划分。两者都要求"开始加载到可见之间有足够时间",这是对比归纳。

对象生成的内存管理

世界载入后还要管理动态对象的生成。动态分配可能很慢;对象尺寸各异,动态分配还会造成碎片、提前耗尽内存(卷 II 17.4.4)。

离线分配:有的引擎干脆禁止游戏过程中动态分配:载入世界块时立即生成所有动态对象,此后不再创建或销毁对象,可以理解为"游戏对象守恒定律"(卷 II 17.4.4.1)。

  • 好处:每个块的对象内存需求事先已知且有界,可以由世界编辑器离线分配、放进世界块数据里,和其他资源一样不易碎片化;内存使用高度可预测,不会突然冒出一大群对象撑爆内存。
  • 坏处:限制策划。可以在编辑器里预先放一个隐身、休眠的对象,"生成"时激活并显示,但策划必须预估每种对象的总数;想要无限供应的血包、武器或敌人就得设法回收对象,否则无解。

动态分配(卷 II 17.4.4.2):策划更喜欢真正的动态生成。主要问题仍是碎片:对象大小不同(同类对象的实例有时也不同),不能直接用无碎片的池分配器;销毁顺序与生成顺序不同,也不能用栈分配器,只剩容易碎片化的堆分配器。对策:

  • 每种对象类型一个内存池:前提是同类型实例大小相同;其实只需每种尺寸一个池。可以完全避免碎片,但池很多,还要猜每种对象需要多少个:多了浪费,少了生成失败。
  • 小内存分配器 (small memory allocator):允许从元素比对象大的池里分配,大大减少池的数量,代价是每个池里有些浪费。例如一组池的元素大小依次翻倍:8、16、32、64、128、256、512 字节,也可以用其他合适的序列或根据游戏运行时收集的分配统计来定。分配时找元素大小不小于对象的最小池;超过最大池的请求交给通用堆分配器,因为大块碎片远没有小块碎片严重。它能消除能放进池的那些分配的碎片,也能显著加速小块分配:池分配只需两次指针操作,把元素从空闲链表中取下。
  • 内存重定位:直接对付碎片,把已分配的块移到相邻空洞里。移动内存容易,难在要修正所有指向被移动块的指针(卷 I 6.2.2.2)。

池与对象回收见 对象池 和 自定义内存分配器。

存档

存档系统与世界块加载系统相似,都从磁盘或存储卡载入世界状态,但需求不同,所以通常是两个独立(或只部分重叠)的系统(卷 II 17.4.5)。

  • 世界块描述所有动态对象的初始状态和所有静态元素的完整描述;背景网格、碰撞数据等很占空间,所以一个世界块有时由多个文件组成,总量通常很大。
  • 存档只需存对象的当前状态,且不必重复存能从世界块数据得到的信息(比如静态几何);对玩法无影响的对象可以完全省略,其他对象可能只需部分状态。只要玩家分辨不出存读档前后的差别(或差别无关紧要),存档系统就是成功的。所以存档比世界块小得多,更强调压缩和省略。旧主机的小存储卡要求这样;即使现在主机有大硬盘和云存档,存档也应尽量小。

检查点 (checkpoint):只允许在特定点存档。好处是每个检查点附近的状态大多已在当前世界块里,与是谁在玩无关,所以存档可以极小:可能只要存最后到达的检查点名,或许再加玩家生命、剩余命数、背包物品、武器和各自弹药;有的游戏连这些都不存,每个检查点都从已知状态开始。坏处是检查点稀疏时容易让玩家沮丧(卷 II 17.4.5.1)。

随处存档 (save anywhere):任意时刻都能保存。存档会大很多:每个状态影响玩法的对象的当前位置和内部状态都要保存再恢复。这时存档基本等于世界块减去静态部分,理论上两者可以用同一种格式,但可能不可行,例如世界块格式为灵活性设计,而存档格式为减小体积而压缩。减小存档的方法仍是省略无关对象和无关细节,例如不必记住每个动画的精确时间索引或每个刚体的精确动量和速度,可以依赖人类玩家不完美的记忆,只存近似状态(卷 II 17.4.5.2)。

UE 的对应:存档系统 让开发者从 USaveGame 派生、只把需要跨会话保存的数据拷进去再写入槽位,这与 GEA 说的"只存影响玩法的部分状态"一致;UE 文档没有区分检查点与随处存档,具体策略由游戏决定(对比归纳)。

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

相关页面

游戏开发游戏世界与游戏对象Game World & Game Objects游戏由玩法 (gameplay) 而不是技术定义;游戏机制 (game mechanics) 是支配游戏中各实体交互的规则,以及目标、成败标准、玩家能力、非玩家实体和整体流程(GEA 引用的论文称实现玩法的软件系统为游戏的 G 因子)。大多数游戏世界由静态元素和动态元素组成,划分为若干世界块,并由高层游戏流程组织玩家目标;动态元素通常实现为游戏对象 (GO),引擎提供的建模设施称为游戏对象模型,且分为工具侧和运行时两套(GEA4 卷 II 16、16.1–16.2)。游戏开发 · UE5世界分区World Partition世界分区 (World Partition) 是 UE5 的自动数据管理与基于距离的关卡流送 (Level Streaming) 系统:整个世界存为一个持久关卡,按可配置的运行时网格划分为单元格 (Cell),由流送源 (Streaming Source,默认是玩家控制器) 按距离加载/卸载。它配合一 Actor 一文件 (One File Per Actor, OFPA) 支持多人协作编辑,用数据层 (Data Layers) 按玩法状态切换内容,用分层细节层次 (Hierarchical Level of Detail, HLOD) 在远处以代理网格显示未加载的单元格。游戏开发自定义内存分配器Custom Memory Allocators内存从两方面影响性能:动态分配(malloc / 全局 new)很慢,以及访问模式主导现代 CPU 的性能。GEA 的经验法则是「尽量少做堆分配,绝不在紧密循环里做堆分配」,并介绍游戏引擎常用的自定义分配器:栈分配器(含双端栈)、池分配器、对齐分配、单帧与双缓冲分配器;以及如何用栈/池避免碎片,或用碎片整理加指针重定位(智能指针、句柄)解决碎片。游戏开发引擎文件系统与异步文件读写Engine File System & Asynchronous File IO资源管理器大量依赖文件系统。引擎通常把操作系统的文件 API 包装成自己的 API:一是跨平台、屏蔽硬件差异;二是操作系统缺少引擎需要的功能(如流式加载),主机还要统一访问记忆棒、硬盘、蓝光盘、网络文件系统等介质。引擎文件系统 API 通常涵盖:文件名与路径操作、打开/关闭/读写文件、扫描目录、处理异步 I/O 请求(流式加载)。游戏开发资源管理器Resource Manager游戏由网格、材质、纹理、着色器、动画、音频、关卡布局、碰撞与物理参数等大量资源(也叫资产、媒体)构成。内存有限,所以引擎要保证每个资源在内存中只有一份(五个网格共用一张纹理时只载入一份)。每个引擎都有某种资源管理器:有的是统一集中的子系统,有的散布在多年间由不同人写的各个子系统中,但职责和要解决的问题是一样的。本页讲运行时部分:职责、文件组织与归档、文件格式、资源 GUID、资源注册表、生命周期(引用计数)和资源的内存管理;交叉引用与加载后初始化见 资源交叉引用与加载后初始化,离线工具链见 资产管线与工具链。游戏开发 · UE5存档系统SaveGameUE 的存档围绕自定义的 USaveGame 子类进行:保存时把需要持久化的数据从游戏世界拷进 SaveGame 对象,再用 UGameplayStatics 写入某个存档槽(槽名 + 用户索引);读档时反过来。官方推荐用 AsyncSaveGameToSlot 异步保存,避免掉帧,也避免某些平台的认证问题。游戏开发世界块数据格式与生成器World Chunk Data Formats & Spawners世界块中的动态部分要为每个游戏对象记录两样东西:属性初始值和对象类型。GEA 比较了三种存储方式:二进制对象映像(生成最简单,但要处理指针、虚表和字节序,且对变化很脆弱)、序列化描述(XML/JSON/私有二进制格式,配合反射与类工厂),以及生成器 (spawner) + 类型模式 (type schema):生成器是只含类型 id 和键值对的轻量数据块,彻底切断世界编辑器与运行时代码的耦合;模式里的默认值配合"与默认值相同就省略键"可以让默认值的修改自动传播到旧实例(GEA4 卷 II 17.3)。游戏开发对象池Object Pool对象池 (Object Pool) 预先一次性分配一组可复用对象,需要时从池里取一个未使用的对象并初始化,用完标记为未使用,而不是逐个 new/delete。它能避免频繁分配的开销和堆碎片,常用于粒子、子弹、正在播放的声音等。代价是池大小要调校、同时活跃的数量有上限、复用的对象不会被自动清零,在有 GC 的环境里还可能让本该回收的对象一直活着。游戏开发 · UE5资产管理与异步加载Asset Management & Async LoadingUE 资产默认按引用自动加载:硬引用 (Hard Reference) 会让被引用资产随引用者一起加载,不加控制就可能在启动时加载全部内容。软引用 (Soft Reference,TSoftObjectPtr/FSoftObjectPath) 只存路径,配合流式管理器 (Streamable Manager) 异步按需加载;资产管理器 (Asset Manager) 进一步把内容划分为主资产 (Primary Asset) 和次资产 (Secondary Asset),用资产包 (Asset Bundle) 管理加载、卸载、烘焙分块与内存审计。游戏开发运行时对象模型架构Runtime Object Model Architectures玩法基础系统 (gameplay foundation system) 是位于"引擎与游戏分界线"附近、供构建游戏规则和动态元素的一组运行时组件,其中最复杂的是运行时对象模型:它在运行时实现世界编辑器展示给策划的抽象对象模型。GEA 把架构分为两大类:以对象为中心(每个工具侧对象对应一个或一小组类实例)和以属性为中心(对象只是一个 id,属性分散在按属性类型划分、以 id 为键的表里)。前者从单体类层次走向组合与组件,后者更省内存、更缓存友好、更易数据驱动,但难以表达属性间关系、也更难调试(GEA4 卷 II 17.1–17.2)。游戏开发游戏对象引用与查询Game Object References & Queries游戏对象需要唯一 id 来区分、查找和充当通信目标;找到对象后还要能引用它,引用的寿命可以从一次函数调用到许多分钟。GEA 比较三种实现:裸指针最快最简单,但有孤儿对象、悬垂指针、非法指针三类问题,且不能配合内存重定位;智能指针可以检测对象已删除、做引用计数,但很难写对,GEA 个人建议远离它,非用不可就用 C++11 标准库或 Boost 的成熟实现;句柄是全局句柄表的整数索引,加上唯一 id 防止槽位复用后指向错误对象,简单而安全。对象查询则不追求通用数据库,而是为最常用的查询建专门的数据结构(GEA4 卷 II 17.5)。综合引擎运行时架构总览Engine Runtime Architecture Overview本页把 wiki 中分散在 GEA 第 1、6、8、17 章及 GPP、UE5 相关页面里的内容串成一条线,回答"一个游戏引擎在运行时是怎样组织起来的":静态结构上是自下而上的分层,上层依赖下层;启动关闭按依赖顺序显式进行;每一帧由主循环驱动各子系统批量更新,游戏对象的逻辑被安排在子系统阶段之间,并按依赖分桶或设先后关系;多核上一帧被拆成成百上千个作业;玩法基础系统(对象模型、事件、脚本、世界加载流送、高层流程)位于引擎与游戏的分界线附近。各结论的细节与出处见所链接的概念页。综合游戏中的内存管理Memory Management in Games本页把 wiki 中关于内存的内容串成一条线:内存从两方面影响游戏,一是访问模式(缓存未命中远比计算昂贵),二是分配本身(通用堆分配慢且产生碎片,而主机上没有自动换页的虚拟内存兜底)。对策按生命周期组织:常驻与关卡数据用栈、每帧临时数据用单帧/双缓冲分配器、同尺寸小对象用池或小内存分配器、大小不一的动态对象用堆加碎片整理与句柄;资源按引用计数和分块加载管理,流送系统把世界切成等大的块;UE 则在 UObject 层用垃圾回收管理对象生命周期。最后,没有准确的追踪工具,以上一切都无从验证。各结论的细节与出处见所链接的概念页。游戏开发事件与消息传递Events & Message Passing游戏天然是事件驱动的:爆炸、玩家被敌人发现、血包被捡起都是事件,系统要通知感兴趣的对象并让它们以各自的方式处理。用虚函数 OnExplosion() 通知属于静态类型的后期绑定,迫使基类为所有可能的事件声明虚函数;GEA 主张把事件封装成对象(类型 + 参数),实现动态类型的后期绑定,从而获得单一处理函数、持久性和盲转发。GEA 依次讨论事件类型(枚举 vs 字符串/字符串 id)、参数(派生类、Variant 数组、键值对)、处理器、职责链、兴趣注册、排队与否的利弊,最后是数据驱动的事件系统和以数据通路连接端口的图形化系统(如 UE 蓝图)(GEA4 卷 II 17.8)。

输入关键词开始搜索

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