2026 年 6 月 17 日,Epic 引擎开发负责人 Marcus Wassmer 发了一篇博客,确认 UE6 将在 Q4 2027 进入 Early Access。里面有一句话被大家反复引用:「UE6 将逐步废弃 Actor 和 Blueprint 体系,转向 Scene Graph 实体和 Entity Component System。」
但这句话只说了故事的一半。
另一半藏在一行更不起眼的文字里:UE6 把 Verse 从 Fortnite 创作工具(UEFN)的脚本语言,升级为整个引擎的一级编程语言。
两件事加在一起,意思就完全不同了——Epic 不是在做一次架构升级,是在同时换掉整个编程语言和你的对象模型。C++ 退居引擎底层,Actor/Component 体系逐步废弃,取而代之的是一套从语言到运行时的全新技术栈。
这套技术栈在 UE6 公开仓库里已经可以编译运行,我花了几天终于把源码整理清楚,最后用例子说明 Verse 和 Scene Graph 是怎么咬合。
说明:本文所引用的行数、文件路径、opcode 名称等均来自 UE6 公开仓库(ue6-main 分支)当前快照。Epic 仍在快速迭代,Early Access 之前这些数字与命名都可能变动。文中给出的具体数字只用于说明「这套系统的体量与设计意图」。
Verse 编译器:约 5.4 万行 C++ 在做什么
Verse 编译器的代码在 Engine/Source/Runtime/VerseCompiler/。管线分四步。
Parser 是一个手写的 PEG 解析器,入口在 VerseGrammar.h。Epic 没用 Bison/Yacc,而是用 C++ 模板实现了一套零依赖的语法组合子框架:
// VerseGrammar.h - 每个语法规则返回 Result<T>,失败就短路
#define ULANG_GRAMMAR_RUN(e) \
{ \
auto GrammarTemp = (e); \
if (!GrammarTemp) \
return GrammarTemp.GetError(); \
}
严格地说,PEG(Parsing Expression Grammar)是一种形式文法,parser combinator 是一种实现手法。Epic 这里的做法是「用 C++ 模板写的、语义上等价于 PEG 的递归下降组合子」。这套组合子让 ParserPass.cpp 只用了约 3000 行就处理了完整的 Verse 语法——包括 race{}、spawn{}、branch{} 等并发原语的语法。
Desugarer(约 2080 行)把语法糖展开为核心 AST。for 循环被展开为迭代器协议调用,if 表达式被展开为 decides 效应上的分支。同时,Desugarer 运行 Tarjan 强连通分量算法处理包依赖的拓扑排序——Verse 的模块系统天然支持循环依赖检测,编译器在编译前就知道哪些包形成了环。
Tarjan SCC 在这里做的不只是「检测环」,更关键的是「分组」:把互相循环依赖的包归到同一个强连通分量里,让编译器对「环内」整体编译、对「环间」按拓扑序编译。这正是支持「合法的循环依赖」(而不是一概报错)的算法基础——这一点和 Rust 的 crate 之间禁止循环依赖形成了鲜明对比。
SemanticAnalyzer 是整个编译器最大的单文件——约 2.1 万行。它负责类型检查、效应推断、并发语义验证。效应系统的核心逻辑就在这里:
// SemanticAnalyzer.cpp - spawn 表达式允许 suspends 效应
if (ExprCtx.ResultContext == ResultIsSpawned)
{
AllowedEffects |= EEffect::suspends;
}
IRGenerator(约 2992 行)把类型检查后的 AST 转换成 Verse VM 字节码。生成字节码所用的 opcode 定义,由 VerseVMBytecodeGenerator.cs(C# 写的 UBT 工具)产出——它在编译期自动生成 C++ 的 opcode 结构体、分发器、内联实现。
为什么用代码生成器?opcode 的「枚举值、操作数布局、解释器分发分支、序列化逻辑」这四份代码必须严格一一对应,手写极易因为漏改某一处而产生隐蔽 bug。用单一的 opcode 描述生成全部四份,是把「一致性」从人的纪律变成编译期保证——这和 Verse 效应系统「把约束从约定变成编译期检查」是同一种工程哲学。
Effects System:Verse 跟所有主流语言的分界线
翻开 Effects.h,编译器内部用一组枚举定义了 Verse 的效应边界。这里需要区分两个层面:面向用户的效应说明符(specifier) 和 编译器内部的效应枚举——它们不是同一份清单。
面向 Verse 程序员、写在函数签名里的说明符主要是这几个:
| 说明符 | 含义 |
|---|---|
<computes> |
纯计算,无副作用,保证终止 |
<converges> |
保证终止(可能读不可变状态) |
<varies> |
相同输入未必产生相同输出 |
<transacts> |
读写可变状态,且这些写入可被事务回滚(函数默认效应) |
<decides> |
函数可能失败(failure context,类似 Result<T,E> 的「失败即回滚」) |
<suspends> |
函数可能暂停、等待异步结果 |
<no_rollback> |
函数的副作用不可回滚(例如真正的 I/O) |
而编译器内部的细粒度效应枚举更接近这一组:
// Effects.h(编译器内部枚举,非用户书写的说明符)
#define VERSE_ENUM_EFFECTS(v) \
v(suspends) \ // 可能暂停等待
v(decides) \ // 可能失败
v(diverges) \ // 可能不终止
v(reads) \ // 读取可变状态
v(writes) \ // 写入可变状态
v(allocates) \ // 分配内存
v(dictates) \ // 定义类型级约束
v(no_rollback) \ // 不可回滚
用户写的 <transacts> 在内部会被拆解成 reads | writes | allocates | diverges | dictates 这类细粒度效应的组合——用户说明符是「打包」的,编译器内部是「拆开」的。这是初学者最容易混淆的地方。
效应可以组合成效应集(EffectSet),用位掩码实现:
// Effects.h - 预定义的效应组合
constexpr SEffectSet Transacts =
EEffect::diverges | EEffect::reads |
EEffect::writes | EEffect::allocates | EEffect::dictates;
constexpr SEffectSet FunctionDefault = Transacts | EEffect::no_rollback;
这组词不是注释,是编译器强制检查的约束。一个没有声明 suspends 的函数不能调用会暂停的函数;一个没有声明 decides 的函数不能使用可能失败的操作。且效应会沿调用栈向上传播——你无法把一个 suspends 函数包一层「干净」的外壳来隐藏它的效应,调用者的签名必须如实反映被调用者的效应。所有检查发生在编译期,零运行时开销。
Verse 的默认策略是「默认允许(可回滚的)副作用,但默认不允许暂停和失败」。跟 Haskell 的纯函数默认相反,跟 Rust 的 Result<T,E> 显式错误处理也不同。这是一种务实的折中:游戏逻辑天然充满副作用(读写 Transform、播放音效、生成粒子),强制纯函数会让游戏程序员疯掉;但「暂停」和「失败」需要显式标注,因为它们改变控制流。
为什么效应系统对游戏引擎特别重要? 因为游戏代码最大的可扩展性瓶颈往往不是单点算法,而是并行化;而并行化最大的障碍不是硬件,是程序员(和编译器)不知道两个函数能不能同时跑。效应系统让编译器替你回答这个问题——reads/writes 标注让编译器在编译期就能判断两个函数是否存在数据竞争,从而决定能否并行,不需要程序员手动加锁。
Verse VM:基于寄存器的字节码解释器
Verse 运行时在 CoreUObject/Private/VerseVM/,核心是 VVMInterpreter.cpp(约 6541 行)。
值表示:64 位 NaN-boxing。 所有 Verse 值都用 64 位表示,借助 IEEE 754 双精度浮点的 NaN 编码空间来区分类型:
// VVMValue.h - 值标签
static constexpr uint64 Int32Tag = 0xffff'0000'0000'0000ull;
static constexpr uint64 PlaceholderTag = 0x1ull; // 惰性求值占位符
static constexpr uint64 UObjectTag = 0x3ull; // C++ UObject 指针
static constexpr uint64 CharTag = 0x4ull;
static constexpr uint64 TransparentRefTag = 0x6ull;
NaN-boxing 的原理一句话:双精度浮点里有一大片「永远不会被正常浮点运算产生」的 NaN 位模式,可以拿来藏「这不是浮点数,而是一个带标签的指针/整数」。好处是浮点数本身零成本(直接就是浮点),代价是非浮点值要做位运算解包。这一思路和 Lua 5.x、JavaScriptCore 同源,但 Verse 额外加了 Placeholder 和 TransparentRef 两个标签来支撑并发语义。
Placeholder 是惰性求值的占位符——一个 Task 读另一个 Task 尚未产生的值时,读到的是 Placeholder,读操作挂起,等值就绪后被唤醒。这是 Verse 把「异步等待」做成语言级原语、而非回调地狱的关键。
帧与寄存器。 Verse VM 是寄存器式而非栈式。每个函数调用创建一个 VFrame,包含固定数量的寄存器:
// VVMFrame.h - 寄存器约定
// Register 0 = Self
// Register 1 = Scope(泛型/闭包捕获)
// Register 2+ = 参数
这个约定是硬编码的——FRegisterIndex::SELF = 0、FRegisterIndex::SCOPE = 1、FRegisterIndex::PARAMETER_START = 2。
为什么选寄存器式而非栈式?栈式 VM(如 JVM、CPython 的字节码)指令更紧凑、生成更简单;寄存器式 VM(如 Lua 5.x、Dalvik)指令条数更少、更利于在解释器层做窥孔优化与减少冗余 load/store,通常解释执行更快。Epic 选寄存器式,是把「解释执行的吞吐」放在了「字节码体积」之前——对每帧都要跑大量逻辑的游戏运行时来说,这个权衡是合理的。
Suspend/Resume。 当字节码遇到需要等待的操作(比如读一个尚未就绪的 Placeholder),VM 创建一个 VBytecodeSuspension 对象,保存当前 PC、帧、效应令牌,然后挂起。当被等待的值就绪后,挂起点被重新调度执行。这就是 Verse 的 suspends 效应在运行时的实现——编译器生成的字节码中可能包含会挂起的 opcode,VM 在遇到这些 opcode 时自动保存和恢复执行上下文。
并发模型:五种原语,一个协作式调度器
Verse 的并发不是抢占式线程——它是基于 Task 的协作式并发。五种原语在 Expression.h 中定义:
| 原语 | 语义 | 类比 |
|---|---|---|
sync {A; B} |
顺序执行 | 普通代码块 |
race {A; B} |
并行执行,第一个完成的结果被采用,其余取消 | Promise.race |
rush {A; B} |
并行执行,等全部完成后继续 | Promise.all |
branch {A; B} |
并行执行,第一个完成后返回,其余继续后台运行 | 竞速+后台 |
spawn {A} |
启动后立即继续,不等结果 | fire-and-forget |
每种原语还有迭代版本:race(Item:Container) { ... } 对容器中每个元素启动一个并发任务。
在 VM 层面,每个并发块创建一个 VTask:
// VVMTask.h
struct VTask : VValueObject
{
bool bRunning{true};
enum class EPhase : int8 {
Active, CancelRequested, CancelStarted,
CancelUnwind, Canceled
};
EPhase Phase{EPhase::Active};
FOp* ResumePC{nullptr}; // 恢复执行的位置
TWriteBarrier<VFrame> ResumeFrame; // 恢复时的帧(带 GC 写屏障)
};
TWriteBarrier<> 包裹 ResumeFrame 不是随便加的——它告诉 GC「这里存了一个堆对象引用,赋值时要记录到屏障」,这是并发 + 增量 GC 能正确工作的前提。
Task 之间通过 Placeholder 同步——这套机制让 Verse 的并发模型在协作式调度下实现了类似 Goroutine 的体验。需要说明的是:Goroutine 由 Go 运行时跨 OS 线程做 M:N 抢占式调度,而 Verse Task 是协作式的(在挂起点让出)。两者「写起来都很轻」,但调度模型本质不同。
事务与回滚。 Verse 的事务系统建立在 AutoRTFM(Automatic Rollback Transactions for Failure Memory)之上。当一个 decides 函数执行失败时,VM 通过 Trail(变更日志)回滚所有在事务内的写入。Trail 记录了每次赋值的旧值,回滚时逆向恢复。这个过程对 Verse 程序员完全透明——你只需要声明函数是 decides,编译器自动生成事务边界代码。
关于 AutoRTFM 的两点澄清:
它的全称是 Automatic Rollback Transactions for Failure Memory(自动「为失败而回滚的事务」),不是 Runtime Transactional Memory。它的设计目标是「失败回滚」,不是用来做多线程并发控制。 实现上,Epic 维护了一个 Clang 的分支,由编译器自动把 C++ 代码改写成「能精确登记 undo 操作」的形式,从而让 C++ 和 Verse 共享同一套事务语义。这也是为什么 no_rollback这个效应必须存在——真正的 I/O(写文件、发网络包)是回滚不掉的,必须被显式隔离在事务之外。
Scene Graph:Actor 死后,谁来接管你的游戏世界
上面拆解了 Verse 的语言层。现在翻到 Engine/Plugins/EntityFramework/——约 12 万行 C++、638 个文件、6 个模块。这是 Verse 代码实际运行的「操作系统」。
Entity:空容器
打开 Entity.h,verse::entity 继承自 UBaseEntity,核心成员只有两个数组:
// Entity.h - UBaseEntity 的核心数据
UPROPERTY()
TArray<TObjectPtr<UObject>> Components; // Component 列表
UPROPERTY()
TArray<TObjectPtr<UObject>> OwnedEntities; // 子 Entity 列表
就这两个数组。Entity 本身没有 Transform、没有 Tick、没有渲染逻辑。它只做两件事:持有 Component 列表和持有子 Entity 列表。这跟 Unity 的 GameObject(持有 Component 列表,且 Transform 是强制内建的)不同——在 UE6 里,连 Transform 都是一个可选的 Component。
这个区别为什么重要:在 Unity 里,哪怕一个纯逻辑的、不存在于 3D 空间中的对象,也被迫携带一个 Transform,这是历史包袱。UE6 把 Transform 降级为可选 Component,意味着「一个纯数据/纯逻辑节点」可以零空间开销地存在于场景图中。这种设计让 Entity 可以表示任何东西——一个光源、一段声音、一棵 AI 行为树、一个 UI 元素——而不需要继承不同的基类。Entity 是纯粹的「场景图节点」,所有行为由 Component 提供。
Component:六阶段生命周期
Component.h 定义了 Component 的完整生命周期,六个阶段,严格有序:
// Component.h - Component 生命周期
// 1. OnInitialized
// 2. OnAddedToScene
// 3. OnBeginSimulation
// 4. OnEndSimulation
// 5. OnRemovingFromScene
// 6. OnUninitializing
源码里用 EEntityNotificationState 枚举追踪每个 Entity 和 Component 当前处于哪个阶段——从 Created 到 TearedDown 共 20 个状态。如果你在 Initializing 阶段添加 Component,引擎会确保这个新 Component 也走完 Initialized → AddedToScene → BeginSimulation,跟宿主 Entity 同步到同一个状态。
在 UE5 里,你在 BeginPlay 里加一个 Component,它可能要等到下一帧才被 Tick,状态会「慢半拍」追上来。在 UE6 里,这种状态同步是引擎层保证的,新加入的 Component 会被「补齐」到与宿主一致的生命周期阶段。
Entity 树与事件传播
Entity 形成一棵树。每个 Entity 有 Parent 和 Children。这棵树决定了场景事件的传播方向:
// Entity.h - 场景事件传播
bool SendUp(TInterfaceInstance<verse::scene_event> SceneEvent); // 冒泡
bool SendDown(TInterfaceInstance<verse::scene_event> SceneEvent); // 广播
SendUp 沿 Parent 链向上冒泡,SendDown 沿 Children 链向下广播。这跟 Web 的 DOM 事件模型在「树形传播」这一点上高度相似——子 Entity 上的 OnDamage 事件可以冒泡到父 Entity,父 Entity 可以广播 OnPause 事件到所有子 Entity,不需要手写遍历代码。
一个提醒:DOM 事件有「捕获→目标→冒泡」三段式,还有 stopPropagation/preventDefault 等一整套机制。Scene Graph 的 SendUp/SendDown 是两个方向明确的 API,语义上更简单直接,不应理解为「完整复刻了 DOM 事件模型」。类比有助于建立直觉,但精确语义以源码为准。
执行引擎:Tick 死了,DAG 活了
UE5 的 AActor::Tick(float DeltaTime) 有一个致命问题:所有 Actor 的 Tick 顺序是隐式的。你靠 TickGroup 和 AddTickPrerequisiteActor 手动管理依赖,稍不注意就会出现「物理还没算完,动画就开始读位置」的竞态。
UE6 用 Execution Engine 彻底重写了这个模型。核心在 ExecutionPhase.h:
// ExecutionPhase.h - 执行阶段是一个有向无环图
class FExecutionPhase final
{
// 同步节点(无回调,纯依赖排序)
FUpdateConnector RegisterSyncNode(FName Name, bool Conditional,
const FUpdateDependencies& Dependencies);
// 函数节点(每帧/按间隔调用)
FUpdateConnector RegisterFunction(TExecuteFn Function,
const FUpdateDependencies& Dependencies, float Interval = 0.0f);
// 对象节点(支持多线程批处理)
FUpdateConnector RegisterObject(UObject* Object,
TUObjectExecuteFn Function,
const FUpdateDependencies& Dependencies,
float Interval = 0.0f, uint8 MaxThreadWidth = 1);
};
一个 FExecutionPhase 内部是一个有向无环图(DAG)。每个节点通过 RunAfter/RunBefore 声明依赖关系,引擎在运行时自动拓扑排序。UE6 预定义了 6 个 Phase:PrePhysics → StartPhysics → DuringPhysics → EndPhysics → PostPhysics → EndFrame。
跟 UE5 Tick 的本质区别:
| 维度 | UE5 Tick | UE6 Execution Phase |
|---|---|---|
| 依赖管理 | 隐式(TickGroup + Prerequisite) | 显式(RunAfter/RunBefore DAG) |
| 并行化 | 手动(TaskGraph) | 自动(Object 节点 MaxThreadWidth) |
| 条件执行 | 手动 bool 判断 | 内置 Conditional Sync Node |
| 动态注册 | BeginPlay 里绑定 | 运行时 Register/Unregister |
最关键的区别:依赖是显式的。 在 UE5 里,同组两个 Actor 的执行顺序不确定;在 UE6 里,你写 B.RunAfter(A),引擎保证 B 一定在 A 之后。这把「靠经验和 TickGroup 微调出来的顺序」变成了「编译/注册期就能验证的依赖图」——一旦出现环(A 依赖 B、B 又依赖 A),拓扑排序会立刻暴露问题,而不是在运行时随机崩。
SceneGraphAPI:操作 Entity 的唯一入口
UE6 源码里有一个明确的约定:不要直接调用 Entity 的方法来修改场景结构,要通过 UE::SceneGraphAPI 命名空间。原因在注释里写得很清楚——直接调用不会触发编辑器刷新(Undo/Redo 栈、Outliner 更新、视口重绘)。
// Entity.h - SceneGraphAPI 的核心操作
namespace UE::SceneGraphAPI
{
TNotNull<verse::entity*> CreateEntity(...);
bool AddEntity(...);
bool RemoveOwnedEntity(...);
verse::component* CreateComponent(...);
verse::component* GetOrCreateComponentByType(...);
void BeginBatchOperation(TNotNull<UWorld*> InWorld); // 冻结编辑器刷新
void EndBatchOperation(TNotNull<UWorld*> InWorld); // 一次性刷新
}
BeginBatchOperation/EndBatchOperation 这对方法特别重要——当你需要一次性创建大量 Entity(例如数千个)时,把它们包在 Batch 里,编辑器只刷新一次,避免「每加一个就重绘一次 Outliner」的灾难性开销。
桥梁:Verse 和 Scene Graph 怎么咬合
上面拆了两套系统。现在看它们怎么接在一起——这是整个 UE6 新架构最核心的设计。
ECS 基础:Fragment、Archetype、Processor
UE6 的 ECS 实现在 Runtime/MassEntity/——Mass Entity 框架,最初由 Epic 的 AI 团队为大规模人群/交通模拟开发,最早在 2021 年的《The Matrix Awakens》Demo 中亮相。三个核心概念:
Fragment——纯数据,替代 Actor Component:
// EntityFragments.h
USTRUCT()
struct FTransformFragment : public FMassFragment
{
GENERATED_BODY()
UPROPERTY()
FTransform Transform;
};
USTRUCT()
struct FHealthFragment : public FMassFragment
{
GENERATED_BODY()
UPROPERTY()
float CurrentHealth = 100.0f;
};
Archetype——Fragment 的组合模板。同一个 Archetype 的所有实体共享相同的内存布局,让 Processor 可以连续遍历、对 CPU cache 友好。这正是 ECS 相对 OOP「逐对象虚调用」的核心性能优势来源。
Processor——纯逻辑,替代 Actor::Tick:
// MassProcessor.h
UCLASS(abstract, ...)
class UMassProcessor : public UObject
{
UE_API virtual void ConfigureQueries(...);
UE_API virtual void Execute(FMassEntityManager& EntityManager,
FMassExecutionContext& Context);
};
一句话说清 ECS 为什么快:传统 OOP 是「对象数组,每个对象自带数据和虚函数表,遍历时不断 cache miss + 间接跳转」;ECS 是「同类数据连续排布,一个 Processor 像流水线一样扫过去」。当实体数量从几百涨到几万、几十万时,这个差距是数量级的。这也是 UE6 敢把 Actor 体系退役的底气——新模型在「海量同质实体」场景下天生更能扩展。
从零搭建一个 NPC:四步走完 Verse → ECS 全链路
上面拆的是原理。但架构图再漂亮,不如一段能跑的代码有说服力。下面用一个 NPC 的完整生命周期——从数据定义到并发调度——走一遍 Verse 和 Scene Graph 是怎么协作的。
第一步:定义 Fragment(C++ 侧)
在 Actor/Component 体系下,一个 NPC 需要继承 ACharacter、挂载 UHealthComponent、UBehaviorTreeComponent、重写 Tick。在 ECS 体系下,NPC 只是几个 Fragment 的组合:
// NPCFragments.h - C++ 侧定义纯数据
USTRUCT()
struct FHealthFragment : public FMassFragment
{
GENERATED_BODY()
UPROPERTY()
float CurrentHealth = 100.0f;
UPROPERTY()
float MaxHealth = 100.0f;
};
USTRUCT()
struct FBehaviorStateFragment : public FMassFragment
{
GENERATED_BODY()
UPROPERTY()
int32 State = 0; // 0=Idle, 1=Patrol, 2=Combat, 3=Fleeing
};
FTransformFragment 引擎已经内置。几个 Fragment 组合成一个 Archetype,引擎自动分配连续内存布局。注意这里没有继承、没有虚函数、没有 Tick——Fragment 是纯 struct,所有字段都是 UPROPERTY(),可以直接序列化和网络复制。
第二步:在 Verse 中创建实体
UE6 的代码生成器自动为每个 FMassFragment 生成 Verse 类型绑定。OpNewObject 指令在运行时判断 Verse 类是否有 NativeRepresentation 标志,如果有就创建 C++ UObject 而非纯 Verse 堆对象。这意味着 Verse 代码可以直接操作 ECS 实体:
# NPC.verse - Verse 侧创建 NPC 实体
# <decides> 表示可能失败(比如空间被占用)
# <transacts> 表示有(可回滚的)副作用(修改 ECS 状态)
InitNPC<decides><transacts>(Position:vector3, PatrolPath:[]vector3):entity =
var NPC:entity = CreateEntity(
FTransformFragment{Transform := MakeTransform(Position)},
FHealthFragment{CurrentHealth := 100.0, MaxHealth := 100.0},
FBehaviorStateFragment{State := 1} # 1 = Patrol
)
# 如果创建失败(空间被占用等),decides 自动触发事务回滚
# 不需要手动写 if-failed-then-cleanup
NPC
注意 <decides> 标注——如果 CreateEntity 因为空间被占用而失败,Verse 的事务系统自动回滚所有在该事务内的副作用。在 UE5 里你需要手动写 if (SpawnActorFailed) { DestroyActor(); RefundResources(); };在 Verse 里,「失败即回滚」是语言原语。
第三步:定义 Processor(Verse 侧)
Processor 就是带有效应标注的 Verse 函数,被 ECS 调度器按帧调用:
# NPCBehavior.verse - Verse 侧定义 Processor 逻辑
# <reads> + <writes> 告诉编译器这个函数会读写哪些 Fragment
# 编译器据此验证:两个 Processor 如果都 writes 同一个 Fragment,不能并行
PatrolProcessor<reads(TransformFragment, BehaviorStateFragment),
writes(TransformFragment)>
(Entities:[]entity, DeltaTime:float):void =
for (NPC : Entities):
var State = GetFragment[NPC, FBehaviorStateFragment].State
var Transform = GetFragment[NPC, FTransformFragment].Transform
if (State = 1): # Patrol 状态
var NewPos = MoveAlongPath(Transform.Position, DeltaTime)
SetFragment[NPC, FTransformFragment]{
Transform := MakeTransform(NewPos)
}
这里 reads 和 writes 不只是文档注释——编译器会检查:如果另一个 Processor 也声明了 writes(TransformFragment),并且调度器试图让它们并行,编译器/调度器会拒绝这种不安全的并行。
第四步:并发编排
最后一步,用 Verse 的并发原语把多个 Processor 组织成一帧的执行计划:
# WorldTick.verse - 每帧的顶层调度
TickWorld(DeltaTime:float):void =
# rush: 三个 Processor 并行跑在不同实体组上
# 效应系统确保它们操作不同 Fragment 时不会冲突
rush:
PatrolProcessor(PatrolEntities, DeltaTime)
CombatProcessor(CombatEntities, DeltaTime)
PhysicsProcessor(AllPhysicsEntities, DeltaTime)
# 上面三个全部完成后,再跑依赖它们的 Processor
# sync 块内的 Processor 可以读上面三个的结果
sync:
AnimationProcessor(AllNPCs, DeltaTime)
AudioProcessor(AllNPCs, DeltaTime)
rush 直接映射到 ECS 的多 Processor 并行执行,sync 映射到依赖链。这四步走下来,一个 NPC 从数据定义到每帧调度,没有继承链、没有虚函数调用、没有手动加锁——Fragment 是纯数据,Processor 是纯逻辑,并发安全由效应系统在编译期/调度期保证。
效应系统 = ECS 并行安全的编译期保证
这是 Verse 效应系统和 Scene Graph 之间最深层的联系:
reads(Fragment)→ 编译器知道这个 Processor 只读某个 Fragment,可以跟其他「只读同 Fragment」的 Processor 并行writes(Fragment)→ 编译器知道这个 Processor 会写某个 Fragment,不能跟任何「读或写同 Fragment」的 Processor 并行allocates→ 编译器知道这个函数会创建新实体(分配 Archetype 内存),需要写屏障配合 GCsuspends→ 编译器知道这个函数可能等待异步操作,调度器可以挂起它而不阻塞整帧
这跟 Rust 的 borrow checker 同源但不同路——Rust 用所有权和生命周期在「单个数据」粒度上防数据竞争,Verse 用效应标注在「函数对 Fragment 集合」的粒度上判断可并行性。两者都实现了编译期的并行安全,但 Verse 的方案更接近「声明意图,编译器/调度器验证」,粒度更粗、更贴合 ECS 的批处理模型。
MCP:AI 进入 UE6 的门
MCP(Model Context Protocol)在 UE6 里的角色是把 Verse 暴露的引擎能力开放给外部 AI。源码在 Programs/UnrealConsole/Private/MCP/:
// UCMcpIntegration.h
namespace UE::UnrealConsole::MCP {
void Init(); // 注册所有 MCP 工具
bool RegisterToolsetClass(UClass* ToolsetClass);
}
Epic 在 UE6 公告里明确表示:将通过开放的 MCP 基础设施暴露引擎能力,让开发者可以「混搭」各家领先模型(公告中点名了 Claude 与 Codex 等),直接辅助引擎内容创作。这里的「引擎能力」包括 Verse API——AI 可以生成 Verse 代码来创建实体、修改 Fragment、调度 Processor。
一句话总结:MCP 是 AI 进入 UE6 的门,Verse 是 AI 在门里说的话。 开发者用 Verse 定义 Fragment 和 Processor,MCP 把这些 API 暴露给外部 AI,AI 通过 MCP 调用 Verse API 来辅助内容生成——整个链条是 Verse → ECS → MCP → AI → Verse,形成一个闭环。
为什么是「确定性 API」而非「让 AI 直接写引擎内存」?MCP 暴露的是一组结构化、带类型与权限边界的工具调用,而不是把裸指针交给模型。配合 Verse 的效应系统与事务回滚,AI 生成的操作即使出错,也能被「失败即回滚」兜底——这是「让 AI 安全地动你的工程」的工程前提。换句话说,效应系统不只是给人用的安全网,也是给 AI 用的安全网。
四层架构全景
把上面的所有模块拼在一起:
┌──────────────────────────────────────────┐
│ MCP │ AI 接口层
│ 暴露 Verse API 给 Claude/Gemini/Codex │
├──────────────────────────────────────────┤
│ Verse │ 语言层
│ 效应系统(并行安全) + 并发原语(编排) │
│ + 事务性内存(回滚) + GC(内存管理) │
├──────────────────────────────────────────┤
│ Scene Graph + ECS (Mass Entity) │ 运行时层
│ Fragment(数据) + Archetype(布局) │
│ + Processor(逻辑) + EntityManager(调度) │
│ + ExecutionPhase(DAG) + SceneGraphAPI │
├──────────────────────────────────────────┤
│ UE6 Core (渲染/物理/网络/音频...) │ 引擎层
└──────────────────────────────────────────┘
迁移视角:存量 UE5 项目怎么办
对绝大多数读者来说,比「源码有多漂亮」更现实的问题是:我手里这个 UE5 项目,会不会被这套新架构一刀切掉?几个判断:
不是「一夜切换」,是「长期共存」。 Epic 明确说 Actor/Blueprint 会在 UE6 早期版本中继续存在,等新框架「足够成熟」后才逐步废弃,并提供转换工具。Early Access 在 2027 年底,正式版还要再过 12–18 个月——这给了项目数年的过渡窗口。
C++ 不会消失,是「下沉」。 C++ 退居引擎底层与高性能模块,Verse 接管 gameplay 层。把它理解为「C++ 之于 gameplay,类似于汇编之于 C++」更准确——不是淘汰,是分层。
Mass/ECS 不是全新概念。 Mass Entity 自 UE5 起就是实验性插件,已经有团队用它做人群、交通、子弹幕等海量实体场景。提前在 UE5 里熟悉 Mass,是平滑迁移到 UE6 运行时模型的最低成本路径。
Verse 已经能学了。 Verse 在 UEFN 里已公开多年,语法、效应系统、failure context 这些核心概念都可以现在就上手——等 UE6 出来再学,等于把学习曲线压缩到迁移窗口里,风险更高。
一个务实的建议:如果你现在启动一个生命周期会跨越 2027–2029 的新项目,值得做的不是「赌 UE6 重写」,而是「在 UE5 里就采用对 UE6 友好的架构」——数据与逻辑分离、减少深继承链、关键系统优先考虑 Mass/数据驱动。这样无论迁移与否,工程都更健康。
风险与未解之问:这套架构还要回答什么
一篇负责任的拆解不该只讲优点。这套架构同样背着几个尚未被充分回答的问题:
调试与可观测性。 效应系统、事务回滚、协作式 Task、寄存器 VM——每一层都在「为正确性和性能做抽象」,但抽象越多,出问题时越难定位。「一个 decides 在深层失败、整串事务回滚」时,开发者怎么知道是哪一步触发的?工具链(断点、回放、效应可视化)能不能跟上语言的雄心,是成败关键。
性能可预测性。 协作式调度避免了锁,但也意味着「一个不让出的长任务会拖住同一调度上下文里的其他 Task」。NaN-boxing 让浮点零成本,却给非浮点值加了解包成本。这些权衡在 Demo 里很美,在真实的、有 GC 停顿和 cache 抖动的项目里表现如何,还需要大规模实战检验。
生态与人才。 Verse 是一门全新语言,functional logic + 效应系统的范式对习惯了 C++/Blueprint 的团队有真实的学习成本。第三方插件、教程、Stack Overflow 答案、AI 训练语料……整个生态要重新积累。Epic 把「AI 辅助」当成解法之一,但这也带来「AI 生成代码的质量与可维护性」这个新问题。
「换两样东西」的复合风险。 同时换语言和对象模型,意味着任何一层出问题都会拖累整体迁移。这是 Epic 自己也承认「比 UE4→UE5 更难」的根本原因。它的回报可能很大,但执行风险也实打实地翻倍。
把这些问题摆上桌,不是唱衰,而是因为:正因为这套架构的赌注足够大,它值得被认真地、带着怀疑地认真对待。
为什么这套架构值得认真对待
翻完 Verse 编译器 + Verse VM + EntityFramework + Mass Entity,几个判断:
第一,这不是「UE5 加了一层脚本」。 Verse 的效应系统被深度集成到编译器的每个阶段——Parser 解析效应标注、SemanticAnalyzer 验证效应兼容性、IRGenerator 生成效应追踪字节码、VM 在运行时执行效应令牌传递。整个工具链围绕效应系统重新设计。而效应系统直接服务于 Scene Graph 的并行安全——reads/writes 标注让编译器/调度器能判断两个 Processor 能否并行执行。语言和运行时是互相定义的。
第二,并发模型务实且完整。 race/rush/branch/spawn 几种原语覆盖了游戏开发中最常见的并发模式——加载资源、竞速搜索、后台任务、多路并行。这些原语天然支持效应追踪,直接映射到 ECS 的多 Processor 调度,不需要程序员手动管理线程池、mutex 或 TaskGraph。
第三,事务性内存解决了游戏逻辑中最头疼的问题之一。 游戏逻辑经常需要「尝试做一件事,失败就全部回滚」——放置建筑(检查空间→扣资源→生成实体→任一步失败全部撤销)。Verse 的事务系统把这种模式变成语言原语,编译器自动管理回滚日志。在 ECS 体系下,这对应「原子地修改一组实体的 Fragment」——要么全部成功,要么全部回滚。
第四,源码已经就位。 VerseCompiler + VerseVM + EntityFramework + Mass Entity——这些模块都已经在公开仓库里,可以编译、运行、调试。Epic 在 UE6 源码公开的较早阶段就把整个新编程模型放了进去,而不是只给一份 PPT。
第五,AI 是这套架构的一等公民。 MCP 不是后来加的外挂,是架构设计之初就预留的接口。Verse 定义引擎能力,MCP 暴露给 AI,AI 生成 Verse 代码操作引擎——这个闭环意味着 UE6 的架构假设是「AI 会参与游戏开发」,而不是「AI 也许有点用」。
Epic 同时换掉你的语言和对象模型,不是因为 C++ 不够快或者 Actor 不够好,而是因为他们看到了一个更大的机会:当语言层(Verse)和运行时层(Scene Graph + ECS)被一起重新设计时,它们之间的接口可以被优化到极致——效应系统保证并行安全,并发原语映射到调度器,事务性内存映射到 Fragment 原子操作。单独换语言、或单独换运行时,都做不到这一点。
我每周拆解一个 UE5/UE6 底层系统,从源码级讲到可运行的代码。觉得有用,关注不走丢。
夜雨聆风