Verse Wiki — 写给 UE6 时代的 Verse 互动手册
参考手册 · REFERENCE

术语表:50 个词读懂 Verse

读文档、逛论坛、看报错,总会撞上眼生的词。这一页把 Verse 的 50 个核心术语按主题分成五组:英文原词、中文译名、两三句人话解释,再配一条直达相关课程的传送门。当词典用,随查随走。

一、基础与模块

语言是什么、代码放在哪、词是怎么进来的——一切从这里开始。

Verse · Verse 语言

UE6 的官方脚本语言,一门「函数式逻辑语言」:一切皆表达式,失败取代布尔值,并发原语直接长在语言里。由 Tim Sweeney 构思十年、Simon Peyton Jones 等 Haskell 名宿参与落地,自 2023 年起已在数万张 Fortnite 岛屿上实战。

UE6 的官方脚本语言,和你在蓝图里连节点是同一件事的两种写法。它有三个跟蓝图不太一样的脾气:每个东西都能算出一个值(连 Branch 那样的判断都有值);它不太用 True/False,而爱问「这条线能不能走通」;还有一批「同时跑好几条线」的玩法(spawn、race 这些)直接长在语言里,不用额外插件。由 Tim Sweeney 构思十年、Haskell 名宿参与落地,2023 年起已在数万张 Fortnite 岛屿上实战。

第 1 课 · 认识 Verse →

UEFN · Fortnite 虚幻编辑器

Unreal Editor for Fortnite,UE6 中面向 Fortnite 的创作环境,也是 Verse 代码的运行舞台。工作流:Verse Explorer 建 .verse 文件、VS Code 写码、回编辑器 Build Verse Code、启动会话看效果。

第 1 课 · 认识 Verse →

expression · 表达式

Verse 没有「语句」,一切皆表达式,一切都有值:if 有值、for 求值为数组、代码块的值是最后一个表达式。这是理解隐式返回、用 if 顶替三元运算符等一票特性的总钥匙。

在蓝图里,有些节点只是「做件事」(执行节点),有些节点会「吐出一个值」(带数据引脚)。Verse 把这条界线抹掉了:这里没有「只做事、不给值」的东西,凡是写下的都会算出一个值——连判断(if)都吐值、循环(for)整跑完会吐出一个数组。正因如此,函数不用专门放 Return 节点(最后算出的值自动就是结果),也能直接拿 if 当「二选一」用,不必像别处那样另找一个三元问号。

第 8 课 · if 与失败上下文 →

module · 模块

可再分发、可被依赖的原子代码单元。项目里每个文件夹自动成为同名模块,也可以用 MyModule := module: 显式定义;成员默认 internal,要跨模块使用,模块和成员得双双标 <public>

一包能单独打包、给别人依赖的代码,类比蓝图里「启用一个插件」或「引用另一个文件夹里的资产」。项目里每个文件夹会自动变成一个同名模块;里面的东西默认只有自己人能用,想让别的文件夹也引用得上,得给模块和成员都挂上 <public>(相当于把资产设成公开可引用)。

第 3 课 · 注释与模块 →

using · 导入

using { /Fortnite.com/Devices } 把模块的 public 成员请进当前文件,花括号不能省。路径长得像网址:域名反映 API 的归属,路径里的 Temporary 段表示这是将来会搬家的临时 API。

using { /Fortnite.com/Devices } 的意思是「先把那个抽屉拉开」——把某个模块里公开的东西请进当前文件,这样才用得上它们,花括号不能省。那串路径长得像网址:域名表示这套 API 归谁管,路径里的 Temporary 段是提醒你「这个 API 以后会搬家」。

第 3 课 · 注释与模块 →

implicit return · 隐式返回

函数体最后一个表达式的值就是返回值,不用写 return。想提前退出也可以显式 return;但失败上下文(如 <decides> 函数体)里禁止 return,这是语言层面的硬规定。

函数体里最后算出的那个值,自动就是这个蓝图函数的返回值,不用像蓝图那样专门拖一个 Return 节点把值接上去。想中途就退出也行,显式写 return;但在「可能走不通」的地方(带 <decides> 的可失败函数体)里禁止 return,这是语言写死的规矩。

第 15 课 · 函数 →

effect / specifier · 效果与说明符

specifier 是尖括号里的语义标注,如 <suspends><decides><override><public>,由编译器强制执行。其中「效果」说明符描述函数会做什么:transacts / varies / computes / converges 四选一,decides 与 suspends 可叠加;什么都不标时默认 no_rollback——不可回滚,因此进不了失败上下文。

说明符(specifier)是写在尖括号里的小标签,比如 <suspends><decides><override><public>,一按 Compile 编译器就照着它们检查你。其中「效果」这一类标签是在申报「这个函数会干什么」:能不能回滚、会不会读写数据等,transacts / varies / computes / converges 四选一,可失败(decides)和会等待(suspends)可以额外叠加;什么都不写时默认「不可回滚」,因此不能放进「可能走不通」的检定里。

第 18 课 · 说明符与属性 →

二、类型

从 logic 到 tuple,Verse 的每种类型都有自己的脾气,先认脸再交朋友。

logic · 逻辑值

Verse 的布尔类型,只有 true 和 false 两个值。但 if 吃的不是它——条件要的是可失败表达式,logic 想进 if 得先加查询运算符:if (Flag?)

就是蓝图里的 Boolean,只有 true 和 false。但要注意:Verse 的 if 不像 Branch 那样直接收 Boolean——它问的是「能不能走通」。想拿一个 logic 去当条件,得在后面补个问号 if (Flag?),把「真 / 假」翻译成「走得通 / 走不通」。

第 7 课 · logic 与 enum →

int / float / rational · 数值三件套

int 是 64 位有符号整数;float 是 IEEE 双精度浮点,字面量必须带小数点(写 1.0 不写 1);两者不做隐式转换,混算直接编译错误。int 除以 int 得到第三种类型 rational(精确分数),要用 Floor 或 Ceil 换回 int。

两种数:int 是整数(蓝图里的 Integer 引脚),float 是小数(Float 引脚)。写小数时必须带小数点(写 1.0,别写 1),否则会被当成整数。两者不会像蓝图那样自动帮你转换,直接混在一起算会亮红报错。还有个冷知识:整数 ÷ 整数得到的是第三种精确分数类型 rational,想变回整数得用 Floor(向下取整)或 Ceil(向上取整)。

第 5 课 · int 与 float →

string · 字符串

本质是 []char 的类型别名,字面量用双引号。花括号做插值:"Score: {Score}";想输出字面花括号要转义 \{。既然是数组,Length、索引、Slice 这套数组操作全部照用。

文字串,写在双引号里(蓝图里的 String)。想把一个变量的值嵌进文字,用花括号:"Score: {Score}"——花括号里的东西会被换成实际值(就像蓝图里把变量接进 Format Text 的占位符);真要打出花括号本身就转义写 \{。它底层其实是一串字符排成的数组,所以数组那套(取长度、按序号取字符、切片)对它照样管用。

第 6 课 · 字符串与插值 →

enum · 枚举

一组具名值:card_suit := enum{Clubs, Diamonds, Hearts, Spades},访问写 card_suit.Clubs。成员背后没有整数,不能转 int 或 string;分支匹配交给 case 表达式。

一组起好名字的选项,就是你在蓝图里右键新建的那个 Enumeration 资产,比如「梅花 / 方块 / 红桃 / 黑桃」。用「类型名.成员名」来取某一项(像从枚举下拉里选一个)。它背后没有藏整数,不能转成数字或文字;要按不同取值分头处理,交给 case(相当于蓝图里的 Switch on Enum)。

第 7 课 · logic 与 enum →

array · 数组

同类型元素的有序列表:类型 []int,字面量 array{1, 2, 3}。下标从 0 开始且是可失败表达式——越界不崩溃,只是失败;数组本身是不可变值,所谓「修改」其实是整体换新。

一串同类型的东西按顺序排好,就是蓝图里的 Array。按序号取(从 0 开始数),但这一步是「可能走不通」的——序号越界不会让游戏崩,只是这一步失败、走 else。还有个和蓝图不同的脾气:Verse 的数组是「只读值」,所谓「改数组」其实是整包换成一个新的。逐个处理用 ForEach。

第 12 课 · array 数组 →

map · 映射

键值对登记表:类型 [string]int,字面量 map{"a" => 1}。键必须是 comparable 的子类型;读和写都可失败,连 set M[K] = V 都得包进 if。

一张「键 → 值」的登记表(蓝图里的 Map):拿名字查分数、拿 ID 查血量。查和写都是「可能走不通」的——查一个不存在的键会失败,所以连往里写 set M[K] = V 都得包进一个 if。当键用的类型必须出自「能比较相等」的那一类。

第 13 课 · map 映射 →

weak_map · 弱映射

map 的特殊近亲:不能遍历、没有 Length,只能按键逐条读写。模块作用域的 var 必须是 weak_map 类型;以 player 为键、值可持久化的模块级 weak_map,正是跨会话存档的载体。

map 的一个特殊亲戚:不能整体遍历、也问不出总数,只能拿着键一条一条读写。它最重要的用途是存档——放在「类的外面」(模块层)、以玩家为键、值声明成可持久化,就成了每个玩家一份、退出重进还在的 SaveGame。

第 22 课 · 持久化数据 →

option · 选项类型

「可能有值也可能是空」的盒子:类型写 ?int,空值就是 false,装值用 option{42}(花括号,不是圆括号)。解包用后缀 ?,是可失败表达式;option{...} 本身还是个失败上下文,能把失败变成一只空盒子。

一个「可能装了东西、也可能是空」的小盒子。类型写成 ?int,空盒子就是 false,装东西用 option{42}(注意是花括号)。想把里面的东西拿出来,在后面加个问号,这一步「可能走不通」(盒子是空的就走 else)。而 option{...} 本身也是个「能容错的地方」:里面那步失败了,它不报错,只是给你一个空盒子。

第 14 课 · option 与 tuple →

tuple · 元组

定长、可混类型的一组值:类型 tuple(int, string),字面量 (1, "a")。取元素用圆括号 T(0)——位数编译期就确定,所以不可失败,但下标必须是编译期常量。函数要返回多个值,靠的就是它。

把固定几个、可以不同类型的值捆成一小包,比如「一个整数 + 一段文字」。装的时候用圆括号 (1, "a"),取第几个也用圆括号 T(0)——因为包里有几样、每样什么类型在编译时就定死了,所以取值不会失败,但序号必须是写死的常数。一个蓝图函数想一次交出好几个返回值,靠的就是它。

第 14 课 · option 与 tuple →

class · 类

引用类型的复合结构:my_class := class: 定义字段与方法,方法体内用 Self 指代当前实例。单继承一个类、可同时实现多个接口;重写成员必须标 <override>,调父类实现写 (super:)Method()

就是蓝图类本身——把数据(字段)和行为(方法)打包成一个可以反复生成实例的东西,方法里用 Self 指「我这个实例」。只能认一个父类(像 Child Blueprint Class 那样继承),但可以同时答应多个接口;重写从父类继承来的成员必须挂 <override>(对应蓝图里那个 Override 下拉),想调父类的原版实现写 (super:)Method()

第 16 课 · 类与继承 →

archetype · 原型

Verse 实例化对象的方式:没有 new,直接写 my_class{Field := 3},花括号里必须给所有没有默认值的字段赋值。想封装更复杂的初始化逻辑,另有 <constructor> 函数可选。

Verse 里「造一个实例」的方式:没有别处那种 new 关键字,直接写 my_class{Field := 3} 就造出一个,花括号里得把所有没给默认值的字段填上(有点像在 Details 面板里把必填项填好才能 Spawn)。想封装更复杂的初始化步骤,还能另外写一个 <constructor> 函数。

第 16 课 · 类与继承 →

struct · 结构体

值语义的纯数据聚合:赋值、传参、进容器都是整体拷贝。不能有 var 字段、体内不能定义方法(想加行为用扩展方法);小型数据组合与持久化的常客。

就是你右键新建的那个 Structure 结构体资产,一小包纯数据。它是「值」语义:赋给别人、当参数传、放进容器,都是整包复制一份(改副本不影响原件)。里面不能放会变的 var 字段、也不能写方法(想加行为用扩展方法);小数据打包和存档时的常客。

第 17 课 · struct 与 interface →

interface · 接口

只有函数签名、没有字段和实现的行为契约。类可以同时实现多个接口,每个接口方法都要用 <override> 给出实现;跨继承树的多态就靠它。

这个你已经会了——就是蓝图接口(Blueprint Interface,BPI):只列出「要有哪些函数」,不写具体实现,谁答应这个接口谁就得自己把这些函数补齐(每个都要挂 <override>)。Verse 的 interface 就是 BPI 的代码形态。一个类能同时答应好几个接口,跨不同继承树也能统一调用。

第 17 课 · struct 与 interface →

comparable · 可比较

能用 = 和 <> 判等的类型家族:int、float、logic、string、char、枚举,以及元素全部可比较的 array / map / option / tuple。map 的键必须是 comparable 的子类型,这是入场资格审查。

「能判断相等」的那一类类型:整数、小数、Boolean、文字、字符、枚举,以及里面元素都可比较的数组 / 映射 / 选项 / 元组。为什么在意它?因为 map 的键必须出自这一类——这是能不能当键的入场资格。判等用 =、判不等用 <>

第 13 课 · map 映射 →

unique · 唯一身份

类的说明符:<unique> 类的每个实例构造时获得唯一身份,可用 = 按身份判等,因此成为 comparable——player、agent 能当 map 键正是因为它。普通类实例没有身份,压根不能比较。

一个给类挂的标签 <unique>:挂上之后,每次造出来的实例都带一张「独一无二的身份证」,可以按身份判断是不是同一个,因此也就能当 map 的键——玩家(player、agent)之所以能当键,正是这个原因。没挂这标签的普通实例没有身份证,压根不能互相比较。

第 16 课 · 类与继承 →

三、流程与失败

「失败」是 Verse 最独特的发明:不是事故,而是一等公民的控制流。

failure context · 失败上下文

允许执行可失败表达式的位置:if 条件、for 的过滤子句、not 与 or 的操作数、<decides> 函数体、option{...} 初始化块。在失败上下文之外写可失败表达式,编译器直接打回。

专指那些「允许放会走不通的检定」的地方:if 的条件括号、for / ForEach 的取值和过滤条件、not 和 or 的操作数里、可失败函数(带 <decides> 的)体内、还有 option{...} 的初始化块。在这些地方之外硬塞一个「可能失败」的检定,一按 Compile 就会被直接拦下报错。

第 8 课 · if 与失败上下文 →

failable expression · 可失败表达式

要么成功产出值、要么失败的表达式:比较运算、数组和 map 的索引、方括号调用 Foo[]、查询 X?、类型转换都在此列。失败不是异常,是正常控制流——程序安安静静走进 else。

「要么成功给值、要么走不通」的操作:大小 / 相等比较、按序号或键取元素、方括号调用可失败函数 Foo[]、后缀查询 X?、还有类型转换。类型转换你最熟——它就是蓝图里的 Cast 节点,成功走一条线、失败走那根红色的 Cast Failed 引脚。失败不是报错、不会崩,是正常的另一条出路,程序安安静静走进 else。

第 8 课 · if 与失败上下文 →

decides · 判定效果

把函数变成可失败函数的效果说明符:调用必须换方括号 Foo[Args],且调用处必须身处失败上下文。它必须与 <transacts> 成对出现——失败了得能回滚。

给函数挂上它,这个蓝图函数就变成「可能走不通」的:调用时得改用方括号 Foo[Args](方括号就是「小心,这一步可能失败」的记号),而且只能在「允许失败的地方」调用。它必须和 <transacts> 一起出现——既然可能失败,就得能把失败前做的事一笔勾销。

第 18 课 · 说明符与属性 →

transacts · 事务效果

声明函数的动作可以整体回滚:读、写、分配全都能撤销。失败上下文只放行带可回滚效果的函数;不带它的函数默认 no_rollback,一进 if 条件就是那句经典报错「effects that are not allowed by its context」。

给函数申报「我做的事全都能撤销」:读、写、生成,统统能反悔。为什么重要?因为「允许失败的地方」只放行「能反悔」的函数;不带这标签的函数默认「泼出去的水收不回」,一旦塞进 if 的条件里,就会撞上那句经典报错「effects that are not allowed by its context」。

第 18 课 · 说明符与属性 →

speculative execution / rollback · 投机执行与回滚

失败上下文的执行策略:先假设会成功、放手执行,一旦失败就把期间的所有效果(包括 set 修改)整体撤销,如同从未发生。这是语言级的事务机制,也是官方那句「Verse 把 C++ 事务化」的字面含义。

「允许失败的地方」是这么跑的:Verse 先在一张「影子图」上大胆把线连一遍试试,一旦整条检定最后走不通,就把这期间做的所有改动(包括 Set 节点改过的变量)全部作废,像那几个节点从没连过。这是语言自带的「后悔药」机制,不用你写任何插件——官方把这套「先试连、不行就当没连过」的事务能力直接内建进了语言里。

第 8 课 · if 与失败上下文 →

query operator ? · 查询运算符

后缀问号,一符两用:接 logic 值,true 则成功、false 则失败;接 option,非空则解包、空则失败。两种用法都是可失败表达式,都离不开失败上下文。

后缀的一个问号,一物两用:接在 logic(Boolean)后面,true 就走得通、false 就走不通;接在 option(盒子)后面,装了东西就把它拆出来、空盒子就走不通。两种用法都是「可能走不通」的,所以都只能待在「允许失败的地方」。

第 7 课 · logic 与 enum →

set · 赋值指令

Verse 铁律三分::= 是定义,set ... = 是赋值,裸 = 是可失败的相等比较(== 根本不存在)。少喊一声 set,编译器就把你的「赋值」当比较处理,报错还看不太懂。

「给变量换个新值」就用 set,相当于蓝图里的 Set 节点。这里三个符号别搞混::= 是第一次定义、set ... = 是之后改值(接 Set)、光一个 = 其实是「判断相等」的检定(而且根本没有 ==)。少写了 set,编译器就把你的「改值」当成「判等」,报的错还挺费解。

第 4 课 · 常量与变量 →

for · 遍历表达式

遍历有限集合的迭代表达式:for (X : Arr)、带下标 for (I -> X : Arr)、范围 for (I := 0..10)。括号里的子句是每轮独立的失败上下文——过滤失败只跳过该轮;整个 for 求值为数组,天生就是列表推导。

就是 ForEach Loop:把一串东西逐个跑一遍——for (X : Arr) 逐个取,for (I -> X : Arr) 连序号一起取,for (I := 0..10) 按数字范围跑。括号里的条件是每一轮各算各的「可失败检定」,某一轮没通过就只跳过那一轮。而且整个 for 跑完会攒出一个新数组,天生就能拿来「边跑边收集结果」。

第 10 课 · for 遍历 →

loop / break · 循环与跳出

loop 无条件重复,出口只有 break 或 return;Verse 没有 while 也没有 continue。同步 loop 不 break 会触发 ErrRuntime_InfiniteLoop,全设备 Verse 停摆;异步游戏循环的标配是 loop 加 Sleep。

loop 就是一根自己绕回去、无条件重复的执行线(相当于蓝图的 While Loop),想出来只能靠 break 或 return;Verse 没有单独的 while,也没有 continue。要命的是:同步的 loop 里要是既不 break 又不歇一下,会触发死循环错误 ErrRuntime_InfiniteLoop,整台设备的 Verse 全罢工;所以游戏循环的标配是 loop 里配一个 Delay(Sleep)。

第 11 课 · loop 与 defer →

defer · 延迟执行

「离开当前作用域时再执行」的收尾块:控制流经过 defer 完成登记后,无论从哪条路退出都会执行;没走到 defer 就退出则不算数。开门关门、显示隐藏这类成对操作的清理,交给它最稳。

「离开这段之前,最后一定会补跑的一根线」。只要控制流经过了 defer 把它登记上,那之后不管从哪个出口离开,这根收尾线都会执行;要是压根没走到 defer 就退出了,就不算数。开门 / 关门、显示 / 隐藏这种「有始有终」的成对操作,收尾交给它最稳。

第 11 课 · loop 与 defer →

四、并发与时间流

Verse 把「时间」当成和控制流同级的语言概念:这些词就是时间维度上的 if 和 for。

Verse 把「时间」也当成一种正经的语言概念,和判断、循环平起平坐:下面这组词,你可以理解成「时间维度上的 Branch 和 ForEach」——照样是分头走、逐个跑,只不过跑的是几条同时进行的时间线。

immediate / async expression · 立即与异步表达式

Verse 把表达式按时间分两类:立即表达式在当前 tick 内完成;异步表达式(Sleep、Await、并发块)可能跨越多个模拟更新。失败上下文里只许立即表达式——挂起跨帧的事没法回滚。

Verse 把动作按「要不要花时间」分两种:立即动作在这一帧(tick)里当场干完;异步动作(Delay/Sleep、等事件、并发块)可能要跨好几帧才完成。「允许失败的地方」里只准放立即动作——一件事要是挂在半空等好几帧,就没法干净地一笔勾销回滚了。

第 19 课 · suspends 与时间流 →

suspends · 挂起效果

函数的异步执照:带 <suspends> 的函数可以在多个模拟更新之间挂起,协作式地让出控制权。它只能在异步上下文中直接调用;同步代码想启动它,得请 spawn 出面。

函数的「异步执照」:带 <suspends> 的函数体里有会等待的节点(就是蓝图里那种带小钟表的 latent 节点,比如 Delay),它能在多帧之间挂起、礼貌地把控制权先让出去。这种函数只能在「同样会等待的上下文」里直接调用;想在普通同步代码里启动它,得请 spawn 出面。

第 19 课 · suspends 与时间流 →

Sleep · 挂起等待

Sleep(Seconds:float)<suspends>:void,来自 /Verse.org/Simulation:挂起当前协程指定秒数——只睡自己,不睡游戏。Sleep(0.0) 恰好等一帧;Sleep(Inf) 永远等待,专等被取消。

就是 Delay 节点:Sleep(Seconds:float) 让当前这条线原地等若干秒——只等它自己,游戏照跑不误。Sleep(0.0) 刚好等一帧(常用来「下一帧再继续」);Sleep(Inf) 则永远等下去,专门用来等着被别人取消。

第 19 课 · suspends 与时间流 →

simulation update · 模拟更新

与 tick、帧同义:Verse 时间流的最小刻度,Fortnite 服务器约每秒 30 次。Sleep 的精度以它为底,亚 tick 的高精度计时是不存在的。

第 19 课 · suspends 与时间流 →

spawn · 派生任务

spawn{ Fn() } 启动一个独立运行的异步任务,自己立刻继续——花括号里只能放单个异步函数调用。它是唯一的非结构化并发表达式,也是唯一允许出现在同步上下文的:Subscribe 回调里想做异步,全靠它。

spawn{ Fn() } 从当前这条执行线里岔出一条新线去跑,自己不等它、立刻往下走(花括号里只能放单个异步函数调用)。它是唯一一种「没人管生命周期」的并发写法,也是唯一能在同步代码里用的——所以想在 Subscribe(绑定的事件回调)里做点需要等待的事,全靠它开路。

第 20 课 · 并发表达式 →

sync · 全员等待

并发运行块内所有异步子表达式,全部完成才继续;结果按书写顺序打包成 tuple 返回。一句话:等所有人到齐再开饭。

几条线一起跑,全部到齐了才继续往下——等所有人到齐再开饭。各条线的结果会按书写顺序打成一小包(tuple)交还给你。

第 20 课 · 并发表达式 →

race · 竞速

并发运行各分支,第一个完成者获胜,其余分支在各自下一个挂起点被自动取消,race 的值就是赢家的结果。「超时逻辑」的标准写法:一臂干活,一臂 Sleep 计时。

几条线同时跑,谁先到终点算谁的,其余的线在各自下一个等待点被当场剪断,race 最后给的就是冠军的结果。「超时」的标准写法就是它:一条线干正事,另一条线 Delay 计时,谁先到听谁的。

第 20 课 · 并发表达式 →

rush · 抢先

和 race 一样取最先完成者的结果,但输家不被取消,继续跑到所在异步作用域结束为止。要「先到先得,但大家都得做完」时用它。

和 race 一样取最先跑完那条线的结果,区别是:落后的线不被剪断,会继续跑到它们所在的这段结束为止。想要「先到先得,但大家的活都得干完」时用它。

第 20 课 · 并发表达式 →

branch · 分支执行

启动一个异步块,后续代码立刻继续;所在异步作用域退出时,branch 的任务被自动撤销。像 spawn,但生命周期有人管——这正是它比 spawn 更受官方推荐的原因。

岔出一条线去跑,主线立刻继续;等主线所在的这段结束时,这条岔线会被自动清场。就像 spawn,但生命周期有人替你管着——这正是官方更推荐它、而不是裸用 spawn 的原因。

第 20 课 · 并发表达式 →

task · 任务

spawn 的返回值:task(t) 代表一个进行中的异步计算,提供 Await() 等它完成、拿回结果。注意它没有公开的 Cancel 方法——想取消 spawn 出去的任务,惯用法是让函数内部 race 一个取消事件。

spawn 岔出去那条线的「把手」:拿着它可以 Await() 等这条线跑完、把结果取回来。注意它没有现成的 Cancel 按钮——想中途叫停一条 spawn 出去的线,惯用法是让函数内部拿一个「取消事件」去 race。

第 20 课 · 并发表达式 →

structured concurrency · 结构化并发

sync / race / rush / branch 四兄弟的共同信条:任务的生命周期被所在作用域约束,作用域退出即自动取消,不留孤儿任务。官方指导原则:能用结构化,就不用 spawn。

sync / race / rush / branch 四兄弟共同的信条:岔出去的线,寿命都被它所在的这段管着,这段一结束,里面没跑完的线自动清场,绝不留下没人管的「孤儿线」。官方的建议是:能用这四个,就别裸用 spawn。

第 20 课 · 并发表达式 →

event · 事件

event(t) 是 Verse 自带的信号器:一端 Signal(Payload) 触发,另一端 Await() 挂起等待,或 Subscribe 注册回调。声明要「类型 = 原型」成对写:MyEvent : event(agent) = event(agent){};设备间解耦通信的主力。

event(t) 就是事件分发器(Event Dispatcher):一头 Signal(Payload) 相当于 Call(广播一声,还能捎带数据),另一头 Await() 挂起等它响,或者 Subscribe 注册一个回调(相当于 Bind)。声明时要「类型 = 原型」成对写。设备之间不想写死耦合、要隔空通信,主力就是它。

第 21 课 · 设备与事件 →

五、设备与数据

代码最终要落进关卡:设备是 Verse 在游戏世界里的身体,数据是它的记忆。

creative_device · 创意设备

Verse 自制设备的基类,来自 /Fortnite.com/Devices:my_device := class(creative_device):。编译成功后,得把设备从内容浏览器拖进关卡才会运行,每个实例各跑一份自己的代码。

你自己做的设备的「基础蓝图类」,写 my_device := class(creative_device): 就是从它继承。编译通过后,还得把这个设备从内容浏览器拖进关卡里才会真正运行;拖几个就有几份,每份各跑各的代码(和放多个设备实例一个道理)。

第 2 课 · 第一个设备 →

OnBegin / OnEnd · 生命周期入口

设备的一头一尾:OnBegin<override>()<suspends>:void 在游戏体验开始时执行,是承载游戏循环的长命协程;OnEnd 在体验结束时执行——官方特别警告,在 OnEnd 里 spawn 的任务可能永远没机会跑。

设备的一头一尾。OnBegin 就是 Event BeginPlay:游戏体验一开始就跑,通常用它挂一整个游戏循环(所以它是根长命的、会等待的线);OnEnd 在体验结束时跑——官方特别提醒,别指望在 OnEnd 里 spawn 出去的线还有机会跑完。

第 2 课 · 第一个设备 →

@editable · 可编辑属性

写在字段上一行的 @ 属性,把字段暴露到 UEFN 细节面板:改数值、连设备引用都不用重新编译。字段必须有默认值,设备引用用空原型初始化:Button : button_device = button_device{}——代码全对但面板忘了连线,是新手第一大坑。

就是变量上那个 Instance Editable 的小眼睛:把字段暴露到 UEFN 的 Details 面板,放进关卡后直接在面板里改数值、连设备引用,都不用重新编译。前提是字段得有默认值,设备引用先用一个空的占位初始化好:Button : button_device = button_device{}。代码全写对、面板却忘了把线连上,是新手第一大坑。

第 21 课 · 设备与事件 →

agent / player · 代理与玩家

引擎的两层身份:agent 是「能触发行为的存在」的抽象,player 是它的子类型(真人玩家)。设备事件的载荷多为 agent;转 player 用失败上下文里的类型转换 if (P := player[Agent]),再取角色实体则 Agent.GetFortCharacter[]

引擎里两层「谁干的」身份:agent 是「能触发行为的家伙」这个抽象(类似事件里那根 Instigator 引脚),player 是它更具体的一种(真人玩家)。设备事件递给你的多半是 agent;想当成 player 用,得在「允许失败的地方」做一次转换 if (P := player[Agent])(就像 Cast to Player),要拿到角色本体再 Agent.GetFortCharacter[]

第 21 课 · 设备与事件 →

Subscribe / cancelable · 订阅与退订

设备事件的回调式监听:Button.InteractedWithEvent.Subscribe(OnPressed),回调签名必须与事件载荷匹配。Subscribe 返回 cancelable,调用 Cancel() 即退订;订阅只做一次——重复订阅会叠加回调,一次交互触发 N 遍。

给设备事件挂回调,就是蓝图里的 Bind Event:Button.InteractedWithEvent.Subscribe(OnPressed),回调函数的参数必须和事件递来的数据对得上。Subscribe 会回一个 cancelable 把手,调它的 Cancel() 就解绑;切记只绑一次——重复绑会叠加,按一下按钮触发好几遍。

第 21 课 · 设备与事件 →

persistable · 可持久化

类型的「存档资格」:基础类型天生合格,自定义类要写成 class<final><persistable> 且只含常量字段。配上模块级 var X : weak_map(player, t) = map{},数据就按玩家跨会话自动保存——排行榜、金币、进度全从这里起步。

一个类型「够不够格进存档」的资格:基础类型天生够格,自定义的类要写成 class<final><persistable> 且里面只放不可变字段。配上放在类外面(模块层)、以玩家为键的 weak_map(player, t),数据就每个玩家一份、退出重进自动还在——排行榜、金币、进度都从这儿起步(就是 Verse 版的 SaveGame)。

第 22 课 · 持久化数据 →