Skip to content

SPJ 与 UE6:Verse 语言与场景图

发布于  at 10:29 AM

SPJ

Simon Peyton Jones 应该说是目前世界上少有的学术和工程都顶尖的学者了。作为 Haskell 编程语言的大师,他在 2021 年 11 月在 Haskell 社区 发帖 说要离开微软研究院去 Epic 开发 Verse 语言,未来会应用在元宇宙中。

我个人还是很期待这个新的脚本语言的,因为我从一开始就认为 UE 的蓝图可维护性很差,我看过太多交错复杂的蓝图了。加上二进制对于今天的 LLM 来讲也并不是一个友好的选项。

前几年,我在 UEFN 中试用过 Verse,一种很明显的 Haskell 风味的脚本。最近,官方终于准备将 UEFN 的 Verse 逐步合并进入 UE6,并且 宣布 逐步替代蓝图。同时,UE6 将逐渐抛弃原有的 TickGameThread 模式,进一步加强并发功能。

还有一个很有趣的点在于,SPJ 作为编译理论的大师似乎为 UE 定制了一个基于 LLVM 特别版的编译器,大概是用来实现并发的事务化。

UE6

UE6 的核心不是单纯换代渲染,而是把 Gameplay、互操作标准、AI/MCP 管线三件事同时重构。这里最关键的是 Gameplay 模型从 Actor/Blueprint/C++ 逐步转向 Verse + Scene Graph + 事务语义。

Epic 认为 UE5 现有 Gameplay 框架还能继续做传统游戏,但不适合做跨游戏、持久化、大规模、多人实时世界。换言之,UE5 无法支撑元宇宙这一概念。

UE 现有 Gameplay 的核心是 Actor 这一套。Actor 能放进关卡、挂组件、Tick、复制属性和 RPC;C++ 负责底层性能,Blueprint 负责可视化逻辑和快速迭代。

Epic 官方文档也把 Actor 描述为可放置或生成的对象容器,组件控制移动、渲染等行为,Actor 还承担网络属性和函数调用复制。

因此,Actor 的问题就是太万能了。对于传统单机、几十人或百人级服务器游戏,这种模型可控。如果还想再往上做成能成千上万,甚至更多人同时在线的游戏就不太行了。

Game Thread 瓶颈

UE 并不是完全单线程,渲染、任务系统、加载、音频等很多部分都有异步或多线程机制。

比如 UE 的 Tasks System 可以异步执行 Gameplay 代码,并支持有依赖关系的 Task Graph;渲染器也运行在独立线程,通常比 Game Thread 落后一两帧。

问题在于,Actor / UObject 状态通常仍由 Game Thread 拥有。Epic 的线程渲染文档明确指出,渲染线程不能随便访问 UObject / Actor 状态,因为 Game Thread 可能正在写入,甚至 GC 可能已经回收对象;

导致这种 线程竞态 很难复现和调试。

UE 现在的多线程更像是局部加速。可以把路径计算、批量查询、数据处理、异步加载丢到任务系统里,但真正修改世界状态、Actor 状态、UObject 生命周期,往往还要回到 Game Thread。

结果就是 CPU 核心越来越多,但 Gameplay 主逻辑仍容易卡在单线程 串行更新上。

UE6 的并发

现代 CPU 多核已经很普遍,如果 Gameplay 模拟长期受 Game Thread 限制,渲染再强也会被游戏逻辑卡住。Epic 在 UE6 路线中明确说,Verse 目前为了保证语义正确仍运行在单线程上,但原则上可以扩展为自动在不同线程上并发运行事务。

进一步就是服务器集群的并发,Epic 正在研究完整的分布式软件事务内存系统,希望开发者像写单机代码一样写 Verse,运行时在需要对象时回滚当前事务,把对象迁移到合适服务器,再重新执行事务。

这不是普通多线程,而是把 Gameplay 状态变成可迁移、可回滚、可分布式执行的对象图。

根据官方的说法,准备将 STM(Software Transactional Memory,软件事务内存)引入 UE。也就是说,将数据库事务里的思想搬到共享内存并发里:一段代码作为事务执行,读写共享变量,最后要么整体提交,要么整体回滚。

Nir Shavit 和 Dan Touitou 1995 年的论文《Software Transactional Memory》

传统并发最大的问题是共享可变状态。两个线程或两个服务器同时改一个对象,谁先谁后?失败怎么办?回滚怎么办?网络延迟怎么办?

Verse 的思路是把函数执行放进原子事务里,必要时回滚和重新模拟;并且从 Verse 调用的 C++ 也要纳入事务语义。

Epic 官方说,Verse 函数都作为原子事务运行,事务语义也延伸到被 Verse 调用的 C++,目前通过 自定义 LLVM 编译器把 C++ 事务化。

Verse 预览

由于 UE6 还没有确定发布,Verse 语法往后大概率还有变动。下面只是根据已有的文档,粗略体验一下。

using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

# A Verse-authored creative device that can be placed in a level
score_device := class(creative_device):

    @editable
    StartScore:int = 0

    var Score:int = 0

    # Runs when the device is started in a running game
    OnBegin<override>()<suspends>:void =
        set Score = StartScore
        Print("Initial score: {Score}")

        for (I := 1..3):
            set Score += I
            Print("Round {I}, score = {Score}")

可以看到底层思想不是语句驱动,而是表达式 + 失败上下文 + 显式效果系统。

Book of Verse 明确说 Everything is an expression,也就是说 iffor、代码块等都可以产生值。这和 C++/C# 的直觉不同,更接近函数式语言。

Damage := if (IsCritical?):
    BaseDamage * 2
else:
    BaseDamage

两种函数

AlwaysSucceeds()      # 普通函数,用 ()
MightFail[]           # 可能失败的函数,用 []

例如调用数组,可能越界,所以需要使用 []

GetAlivePlayer()<decides><transacts>:player =
    Player := FindRandomPlayer[]
    Player.GetFortCharacter[].IsAlive[]
    Player

<transacts> 与事务有关。Verse 的失败机制不是简单异常,而是带有推测地执行。如果中途失败,前面已经发生的可回滚变更会被撤销。

所以,Verse 看起来最核心的就是用 failure 语义代替异常/空值检查;事务代替手写回滚,异步表达式/协程等思想代替传统线程思维。

本博客所有文章除特别声明外,均采用 CC BY-NC-SA 4.0 许可协议。转载请注明来自小谷的随笔

下一篇
Music | The Veil - Joshua Hyslop