Verse 语法速查
一页扫完 Verse 的全部招式:声明、运算符、流程、容器、函数、类型、说明符、并发、设备样板、Scene Graph 组件骨架与持久化。每个速查块一段最小代码、一张「蓝图里是什么」对照表、一个详解课号。写代码卡壳时回来扫一眼,顺着课号跳回去补课。
怎么用这一页
这一页是「写代码时开着的那一屏」。每一节的结构固定成三层:先给一段能编译的最小代码,再给一句 ▢ 读一下这段 的白话讲解,最后是一张 蓝图里是什么 的对照表——你已经会做那件事,只是不知道它在这边怎么写。
想查一个具体名词而不是语法块,去蓝图 ↔ Verse 术语对照表;想弄明白某条规则为什么长这样,顺着每节末尾的课号跳回课程地图。
一、声明:变量、常量与类型标注
# 常量:定义即终身,不可再改;:= 省略类型让编译器推断
MaxHealth:int = 100
Nickname := "泡泡"
# 变量:var 声明,带类型标注;修改必须喊 set
var Health:int = 100
set Health = 80
set Health += 15
▢ 读一下这段:上半截造了两个「定死不改」的常量(相当于建变量时不给它接 Set 节点);下半截 var Health 是个可改的变量,后面每次改都得用 set(就是接一个 Set 节点)。记住三个符号::= 只管第一次定义、set … = 才是改值(接 Set)、光一个 = 是「判断相等」的检定,而且根本没有 ==。详见 第 8 课。
Count:int = 42 # 整数
Speed:float = 3.5 # 浮点,字面量必须带小数点
Title:string = "UE6" # 字符串,插值写 "HP: {Count}"
var Ready:logic = false # 布尔:true / false
Names:[]string = array{"泡泡", "阿奇"} # 数组
Table:[string]int = map{"钥匙" => 1} # 映射
var MaybeN:?int = false # option,false 即空
Pair:tuple(int, float) = (1, 2.0) # 元组
▢ 读一下这段:每一行都是「名字:类型 = 值」,给一个变量定好它装什么(整数、小数、文字、Boolean、数组、映射、可空的 option 盒子、元组),对应蓝图里建变量时先选好变量类型。两个坑:int 和 float 不会自动互转——整数变小数要乘 1.0,小数变整数得用「可能失败」的 Floor[X](向下取整);写小数一定带小数点。数字详见 第 9 课,字符串详见 第 10 课,logic 详见 第 11 课。
| 这一节的 Verse | 蓝图里是什么 | 一句话差异 |
|---|---|---|
Name:type = 值 | 变量面板里新建一个变量并填 Default Value | 没有面板,声明写在哪一级缩进就属于哪个作用域 |
| 写出变量名 | Get 节点 | 读一个值不需要任何节点 |
set X = … | Set 节点 | 漏了 set,单独的 = 会被当成判等 |
| 什么都不写(默认不可变) | 蓝图没有「不可变变量」 | 默认值反了:Verse 要变才申请 var |
int / float / logic / string | Integer / Float / Boolean / String | Boolean 改名叫 logic,且不能直接当 if 条件 |
?int(option) | 可能为空的对象引用 | 「可能没有」被写进类型里,编译期就逼你处理 |
二、运算符
if (A = B, B <> C): # 判等是 =,不等是 <>;没有 ==
Print("逗号就是 and")
if (X > 0 and Y > 0): # 逻辑词 and / or / not,没有 && || !
Print("比较全家都是可失败表达式")
if (Ready?): # 后缀 ?:logic 为 true 才成功
Print("裸写 if (Ready) 编译不过")
▢ 读一下这段:三段 if 都像 Branch,但注意它们问的不是「Boolean 是不是 true」,而是「括号里这件事走不走得通」。所以判等用一个 =、判不等用 <>(没有 ==),逻辑连接用 and / or / not(没有 && || !),逗号也当 and 用;想拿 Boolean 当条件得补个 ?。一句话:这些比较不吐 true/false,而是以「走通 / 走不通」参与流程,所以只能待在「允许失败的地方」。详见 第 13 课。
| 符号 | 作用 | 蓝图里是什么 | 备注 |
|---|---|---|---|
:= |
定义并初始化 | 新建变量并填默认值 | 只用于首次定义,可省类型 |
set X = … |
给 var 赋值 | Set 节点 | 复合赋值 += -= *= /=(int 的 /= 不行) |
= <> < <= > >= |
比较,可失败 | Equal / Not Equal / Less / Greater 节点 | 只能出现在失败上下文;没有 == |
+ - * / |
算术 | Add / Subtract / Multiply / Divide 节点 | int / int 可失败且得 rational(分数类型):if (Half := Floor(X / 2)): |
Mod[A, B] |
求余 | Modulo 节点 | 没有 %;方括号 = 可失败调用 |
and or not |
组合成功/失败 | AND / OR / NOT 节点 | 没有 && || !;not 内的效果永远回滚,也不能拿来翻转一个 logic 变量 |
X? |
查询 | 把 Boolean 接进 Branch 的 Condition / Is Valid | logic 为 true / option 非空才成功 |
X[…] |
可失败调用与取值 | Cast 的那根 Cast Failed 引脚 | 取下标、查表、调 <decides> 函数、类型转换都用它 |
▢ 这张表按蓝图直觉认一遍::= 第一次建变量并赋初值;set X = … 是 Set 节点改值;那排比较符号都只在「允许失败的地方」用;算术里整数除整数会「可能失败」且得到分数,要用 Floor 收回整数;求余没有 %、写成 Mod[A, B];逻辑连接只有 and / or / not 三个词;末尾的 X? 把 Boolean 或盒子翻译成「走不走得通」。
三、控制流:if / for / loop / defer / case
# 条件区是失败上下文:绑定链任何一步失败,整体走 else
if (Player := player[Agent], Fort := Player.GetFortCharacter[]):
Print("拿到角色,继续干活")
else:
Print("链上某一步失败了")
Max := if (A > B) then A else B # if 是表达式,可直接取值
▢ 读一下这段:上面这个 if 像一串接龙的 Cast——先把 Agent 转成 player、再从它身上取到角色实体,任何一环走不通就整体走 else。下面 if (A > B) then A else B 则是把 if 当「二选一」用(相当于 Select 节点),直接算出较大值。关键脾气:「允许失败的地方」是可回滚的——条件里用 Set 改过的值,一旦失败就整体撤销、像没改过,所以条件括号里也不许放 Print String 这种收不回的动作。详见 第 12 课。
for (Item : Items): # 遍历数组元素
Print("{Item}")
for (Index -> Elem : Items): # 带下标,从 0 起
Print("{Index} 号:{Elem}")
for (N := 1..5, N > 2): # 范围含两端;过滤失败只跳过本轮
Print("{N}")
Squares := for (X := 1..5) { X * X } # for 是表达式,收集成新数组
▢ 读一下这段:四种 ForEach 写法——逐个取元素、连序号一起取、按数字范围跑(还能顺手加个过滤条件,不满足就跳过这一轮)、以及把每轮算出的值收集成一个新数组。注意 Verse 的 ForEach 里没有 break / continue:想「跳过某轮」就写成过滤条件,想「边跑边收集」直接拿整个 for 的结果当数组用。详见 第 14 课。
var Round:int = 0
OnBegin<override>()<suspends>:void =
loop:
set Round += 1
if (Round > 3):
break # break 只属于 loop,for 里不能用
Sleep(1.0) # 每轮至少一个挂起点,否则判死循环
▢ 读一下这段:loop 是一根自己绕回去、无限重复的执行线(相当于 While Loop),每轮把回合数 +1,超过 3 就 break 跳出,末尾放一个 Sleep(1.0)(Delay)让它歇一秒再跑下一轮。两个铁律:break 只归 loop 用、ForEach 里不能用;而且同步 loop 里要是既不 break 又不放个 Delay,就会触发死循环错误 ErrRuntime_InfiniteLoop,整台设备的 Verse 全罢工。详见 第 15 课。
Countdown()<suspends>:void =
Print("倒计时开始")
defer:
Print("离开作用域时才执行,提前退出也会跑")
Sleep(3.0)
Print("三、二、一")
▢ 读一下这段:defer 里的那行是「离开这个函数之前,最后一定补跑」的收尾线——所以哪怕它写在中间,真正执行也排在末尾。关键:必须先让执行流「经过」defer 把它登记上才算数;要是它藏在一条根本没走到的岔线里,就不会补跑。详见 第 15 课。
game_phase := enum{Lobby, Battle, Podium}
PhaseLabel(Phase:game_phase):string =
case (Phase):
game_phase.Lobby => "大厅"
game_phase.Battle => "战斗"
game_phase.Podium => "颁奖台"
▢ 读一下这段:case 就是蓝图里的 Switch on Enum——按枚举当前是哪一项,分头走对应的线,这里把三种游戏阶段各翻译成一句中文交出去。好处:只要把枚举的每一项都写全了,就不用再留一个「其它」兜底分支;漏写了哪一项,一按 Compile 编译器会点名。要按整数或字符串分支也用 case,但那种情况没法穷尽,必须留一条 _ => 兜底。详见 第 11 课。
| 这一节的 Verse | 蓝图里是什么 | 一句话差异 |
|---|---|---|
if (…): … else: … | Branch | 收的不是 Boolean,是「这一步走不走得通」;Cast 失败、越界、查不到键全走同一个 else |
if (…) then A else B | Select 节点 | if 本身是表达式,直接吐值 |
| 顺序缩进的几行 | Sequence 节点 | 没有对应节点,缩进本身就是顺序 |
for (X : Arr) / for (I -> X : Arr) | ForEach Loop(带不带 Index) | 整个 for 跑完会攒出一个新数组 |
for (I := 0..9) | For Loop | 和 ForEach 是同一个关键字,范围含两端 |
for (X : Arr, X > 0) | 循环体里套一个 Branch | 这是 Verse 版的 continue;for 里没有 break |
loop: + break | While Loop | 没有 while,条件搬进循环体;同步 loop 不歇会判死循环 |
defer: | 没有对应节点 | 登记一次,从哪个出口离开都会补跑 |
case (X): | Switch on Enum / Switch on Int | 枚举必须覆盖全,否则要一条 _ => |
四、容器:array / map / option / tuple
var Numbers:[]int = array{1, 2, 3}
if (First := Numbers[0]): # 下标可失败,必须住进 if
Print("第一个是 {First}")
if (set Numbers[0] = 9) {} # 改元素同样可失败
set Numbers += array{4} # 追加:拼接出新数组
Print("共 {Numbers.Length} 个") # .Length 不会失败
▢ 读一下这段:建一个数组;按序号取第 0 个,但这一步「可能走不通」,所以包进 if;连改某个元素、往后追加,也都是这个套路(追加其实是拼出一个新数组);.Length 取总数才是稳的、不会失败。要点:Verse 数组是「只读值」,没有 .Add / .Push 这种就地改;序号越界不会崩,只是这一步失败、走 else。详见 第 16 课。
var Scores:[string]int = map{"泡泡" => 10}
if (Score := Scores["泡泡"]): # 查询可失败:查无此键走 else
Print("查到 {Score} 分")
if (set Scores["阿奇"] = 0) {} # 连写入都要包 if
for (Name -> Score : Scores): # 按写入顺序遍历
Print("{Name}: {Score}")
▢ 读一下这段:建一张「名字 → 分数」的登记表(Map);拿名字查分数「可能查不到」,所以包进 if;连写入一个新键都要包 if;最后用 ForEach 按写入顺序把每对键值逐个读出来。注意两个箭头别搞混:建表配对用 =>,遍历读出用 ->;没有 Remove,想删只能「过滤出想留的、重建一张」。详见 第 17 课。
var MaybeScore:?int = false # false 就是「空盒子」
set MaybeScore = option{42} # 装入值用 option{},不是 option()
if (Score := MaybeScore?): # 后缀 ? 解包,空则失败
Print("盒子里是 {Score}")
MaybeFirst := option{Numbers[0]} # 把可失败表达式装罐成 option
▢ 读一下这段:?int 是个「可能装了整数、也可能是空」的盒子,false 就是空盒;装东西用 option{42};想取出来在后面加 ?,空盒就走不通、走 else。最后一行把一个「可能失败」的取值直接塞进 option{…}——因为 option{…} 本身就是个「允许失败的地方」:里面那步失败了不报错,只是给你一个空盒。它正是「走不通」和「没有值」之间的官方桥梁。详见 第 18 课。
Point:tuple(int, float) = (1, 2.0) # 混合类型定长打包
X := Point(0) # 圆括号访问,编译期检查,不会失败
GetXY():tuple(float, float) = (3.0, 4.0) # 多返回值 = 返回 tuple
Result := GetXY()
Print("x = {Result(0)}, y = {Result(1)}")
▢ 读一下这段:把「一个整数 + 一个小数」捆成一小包,用圆括号装、也用圆括号 Point(0) 取第几个——因为包里几样、每样啥类型都是编译时定死的,取值不会失败。下面的 GetXY() 演示了元组最大的用处:让一个函数一次交出好几个返回值。记住:元组取值用圆括号 T(0)、数组取值用方括号 A[0],而且元组的序号必须写死,不能拿变量当序号。详见 第 18 课。
| 这一节的 Verse | 蓝图里是什么 | 一句话差异 |
|---|---|---|
[]int / array{…} | Array | 不可变值,没有 Add;取下标可失败,越界不崩 |
[string]int / map{…} | Map(TMap) | 查和写都可失败;没有 Remove,删要重建 |
| (没有) | Set(TSet) | Verse 不提供集合,惯用替代是 [t]logic |
?t / option{…} | 可能为空的对象引用 + Is Valid | 「空」写进类型里,Accessed None 崩溃不存在 |
tuple(int, float) | 函数的多个输出引脚 | 取第几个用圆括号,序号必须是写死的常数 |
五、函数定义
Add(A:int, B:int):int = A + B # 隐式返回:最后一个表达式即结果
Award(?Points:int = 1):void = # ? 前缀 = 命名可选参数
Print("加 {Points} 分")
# 调用:Award() 用默认值;Award(?Points := 10) 按名传参
Twice(F(X:int):int, N:int):int = F(F(N)) # 函数也能当参数传
▢ 读一下这段:Add 是个最普通的蓝图函数,最后算出的值自动就是返回值(不用专门放 Return 节点)。Award 演示带默认值的可选参数(名字前加 ?),调用时可以不传、也可以按名字传。Twice 更进一步:函数还能当参数塞给另一个函数。两个坑:传进来的参数是「只读」的,想改先拷进一个局部 var;写一个函数名 Foo 不加括号,拿到的是「这个函数本身」而不是它的运行结果。详见 第 19 课。
# <decides> 必须与 <transacts> 成对出现:失败要能回滚
IsBig(X:int)<transacts><decides>:void =
X > 100
if (IsBig[200]): # 可失败函数用方括号调用
Print("圆括号调普通函数,方括号调可失败函数")
▢ 读一下这段:给函数挂上 <decides> 就把它变成「可能走不通」的蓝图函数,里面 X > 100 这一步没过、整个函数就算失败;这种函数必须同时挂 <transacts>(可回滚),而且调用时改用方括号 IsBig[200](圆括号调普通函数、方括号调可失败函数)。一个坑:这种函数体内不许写 return;想「找到就带着值退出」,惯用法是先备一个空盒子 var Ret:?t = false 收集,最后一行用 Ret? 把它拆出来。详见 第 19 课 与 第 22 课。
| 这一节的 Verse | 蓝图里是什么 | 一句话差异 |
|---|---|---|
Foo(A:int):int = … | Function(蓝图函数) | 最后一个表达式自动就是返回值,不用 Return 节点 |
?Points:int = 1 | 输入引脚的默认值 | 只有加了 ? 的命名参数才能有默认值 |
| 函数当参数传 | 蓝图基本做不到 | 函数在 Verse 里是一等值 |
<decides> + Foo[] | 没有对应物,最接近的是 Cast 的双出口 | 「可能失败」是函数签名的一部分,编译器强制 |
<computes> / <converges> | Pure 复选框 | Verse 会检查你申报得对不对 |
模块层的 <public> 函数 | Blueprint Function Library | 不用专门建「库」资产,文件夹自动是模块 |
| (没有) | Macro(蓝图宏) | 写普通函数就够了 |
| (没有) | 参数的 By Ref 勾选 | 参数一律只读,想改先拷进局部 var |
六、类型定义:class / struct / interface / enum
enemy := class:
Name:string # 无默认值 → 实例化时必填
var Health:int = 100
TakeDamage(Amount:int):void =
set Health -= Amount # 方法里用 Self 指自己
Grunt := enemy{Name := "小兵"} # 原型语法实例化,没有 new
▢ 读一下这段:这就是定义一个蓝图类——列出字段(没给默认值的,造实例时必须填)、写方法(方法里用 Self 指「我自己」),最后 enemy{Name := "小兵"} 造一个实例(没有 new,直接花括号填字段)。继承写成 child := class(base):(相当于 Child Blueprint Class),重写父类成员必须挂 <override>(对应那个 Override 下拉),调父类原版写 (super:)Method();类是「引用」语义(传的是同一个实例,不是拷贝)。详见 第 20 课。
point := struct:
X:float = 0.0
Y:float = 0.0
P := point{X := 3.0, Y := 4.0} # 值语义:赋值、传参都是拷贝
Q := point{X := P.X, Y := 9.9} # 「修改」= 构造一个新实例
▢ 读一下这段:struct 就是右键新建的那个 Structure 结构体资产,一小包纯数据。它是「值」语义:Q := point{X := P.X, …} 这种「改一下」其实是照着旧的造一个全新实例,赋值、传参都是整包拷贝、互不影响。规矩:结构体里不许放会变的 var 字段、也不许写方法——想给它加行为,用「扩展方法」(S:point).Norm():float = … 从外面挂上去。详见 第 21 课。
rideable := interface:
Mount():void # 只有签名,没有实现
bicycle := class(rideable):
Mount<override>():void = # 实现每个接口方法都要 <override>
Print("上车,出发")
▢ 读一下这段:interface 就是蓝图接口(BPI)——只列函数签名、不写实现;下面这个类答应了这个接口,就得把 Mount 补齐,而且实现接口方法也要挂 <override>。规矩:一个类只能认一个父类,但能同时答应好几个接口 class(base, iface1, iface2);想把父类引用「向下转」成具体子类型,用 if (C := child_type[Base]):——这一步「可能走不通」,就是蓝图里的 Cast。详见 第 21 课。
card_suit := enum{Clubs, Diamonds, Hearts, Spades}
MySuit := card_suit.Hearts # 成员通过类型名访问
if (MySuit = card_suit.Hearts): # 只支持 = 与 <> 判等
Print("是红桃") # 想转文字?自己写 case 映射
▢ 读一下这段:enum 就是右键新建的 Enumeration 枚举资产,列几个起好名的选项;用「类型名.成员名」取某一项,判断是不是某一项用 =(只支持 = 和 <>)。要点:枚举成员背后没有藏整数,不能转成数字或文字、也不能比大小;想变成显示文字得自己写一段 case 映射。定义要放在类的外面(模块层)。详见 第 11 课。
| 这一节的 Verse | 蓝图里是什么 | 一句话差异 |
|---|---|---|
class | Blueprint Class 资产 | 不是资产,是文本文件里的一段声明;引用语义、单继承 |
class(base) | Child Blueprint Class | 重写成员必须挂 <override> |
my_class{Field := 3} | Spawn + Expose on Spawn | 没有 new;没默认值的字段是必填项 |
struct | Structure 资产 | 更「纯」:不能有 var 字段、不能写方法 |
interface | Blueprint Interface(BPI) | 没有「Message 调用 vs 直接调用」的分裂 |
enum | Enumeration 资产 | 背后不藏整数,不能转数字或文字 |
child_type[Base] | Cast To … | 方括号 = 可能失败,失败走 else |
七、说明符与属性速查表
尖括号 <…> 里的叫说明符(specifier),一按 Compile 编译器就照着它检查你;@ 开头的叫属性(attribute),写在声明的上一行,主要影响 UEFN 编辑器里的表现(比如 @editable 让字段冒到 Details 面板)。下面这张表挑你用得上的记。
| 修饰符 | 一句话 | 蓝图里是什么 | 写在哪 |
|---|---|---|---|
<public> <internal> <protected> <private> |
访问权限;默认是 internal(模块内可见),不是 public | 变量/函数的 Private / Protected / Public 下拉 | 标识符后;加在 var 上可读写分权:var<protected> Ammo<public>:int |
<override> |
重写父类成员,必加 | 函数的 Override 下拉 | 成员名后 |
<abstract> |
不能实例化,可含无实现方法 | Abstract 类勾选 | class<abstract> |
<final> |
类不许继承 / 成员不许重写 | 把函数标成不可 Override | class 或成员 |
<concrete> |
全字段有默认值,可 my_class{} 空原型实例化;@editable 自定义类必备 |
「这个类能直接摆进面板」 | class<concrete> |
<unique> |
实例有身份,可用 = 比较、可作 map 键(player 就是这么来的) | 对象引用的「是不是同一个」 | class<unique> |
<transacts> |
动作可回滚;失败上下文里只欢迎它 | 蓝图没有:Set 泼出去就收不回 | 效果:参数表后、返回类型前 |
<decides> |
可失败,方括号调用;必须搭 <transacts> |
最接近的是 Cast 的双出口 | 效果 |
<suspends> |
异步,可跨帧挂起;只能在异步上下文直接调用 | Latent 节点(带小钟表那种) | 效果 |
<computes> <varies> <converges> |
三档「无副作用」承诺,只能挑一个;什么都不标 = no_rollback(不可回滚) | Pure 复选框 | 效果 |
<persistable> |
可进存档;类须 <final> 且只能有常量字段 |
SaveGame Object 的字段 | class / struct 后 |
<localizes> |
把插值字符串包装成 message(UI 文本要的类型) | Text(可本地化文本) | 函数 / 常量 |
<final_super> |
Scene Graph 要求:自定义组件必须直接派生自 component。Beta,以官方文档为准。 |
蓝图没有对应概念 | class<final_super>(component) |
@editable |
把字段暴露到细节面板,改值不用重编译 | Instance Editable 的小眼睛 | 属性:字段声明的上一行 |
▢ 这张表全是一按 Compile 可能亮红的关键词。按蓝图直觉挑几条:<override> = 重写父类成员;<concrete> = 所有字段都有默认值、能空手 {} 造实例,自定义 @editable 类必备;<unique> = 实例带身份证、能当 map 键;<transacts> = 动作可回滚,只有它受「允许失败的地方」欢迎;<suspends> = 会跨帧等待;@editable = 就是 Instance Editable 小眼睛。「效果」这一类其实就是把「这个函数会干什么」写进签名——报错提到 no_rollback / decides / suspends 时,先回来看这张表。详见 第 22 课。
八、并发五表达式
| 表达式 | 语义 | 其余任务的命运 | 蓝图里是什么 |
|---|---|---|---|
sync: |
全部完成才继续,结果打包成 tuple | —— | 没有:蓝图要自己数「都回来了没有」 |
race: |
第一名完成即结束,值取冠军 | 在下个挂起点被取消 | 没有:「超时」在蓝图里要靠 Tick 或 Timeline 手拼 |
rush: |
第一名完成即返回 | 继续跑,随外层作用域收尾 | 没有 |
branch: |
先启动,主线继续 | 随所在作用域退出被撤销 | 最接近「另起一条执行线」,但有人管清场 |
spawn{ Fn() } |
放出独立任务,返回 task | 不被取消,自己跑完 | 「另起一条执行线」,且没人管它什么时候结束 |
这五个都是「同时跑好几条线」的玩法,按蓝图直觉认:sync = 等所有线到齐再往下;race = 谁先到听谁的,其余的线当场剪断(超时就用它);rush = 也取最先到的,但落后的线不剪断;branch = 岔出去跑、主线继续,等这段结束时自动清场;spawn = 硬岔出一条没人管生命周期的线,是唯一能在普通同步代码里用的。前四个只能在「会等待的上下文」里用。
GateOnce()<suspends>:void =
race:
block:
Button.InteractedWithEvent.Await() # Button 是 @editable 引用
Print("及时按下!")
block:
Sleep(10.0)
Print("超时,自动继续")
▢ 读一下这段:race 让两条线赛跑——一条等按钮按下(Await() 挂起等事件,Button 是面板上连好的 @editable 引用),另一条 Sleep(10.0) 当闹钟计时;谁先到就走谁,输的那条当场被剪断。这就是「超时」的标准配方:事件和闹钟赛跑。每条臂里有好几行时用 block: 打包成一条线,各臂最后给的结果类型要能兼容。详见 第 24 课。
OnPressed(Agent:agent):void = # Subscribe 回调不是异步函数
spawn{ CloseAfterDelay() } # spawn 体只能是单个异步调用
CloseAfterDelay()<suspends>:void =
Sleep(5.0)
Print("五秒到,收尾")
▢ 读一下这段:按钮的 Subscribe 回调 OnPressed 本身不是「会等待」的函数,想在里面做需要等待的事,就 spawn{ CloseAfterDelay() } 岔一条新线出去(花括号里只能放单个异步调用),自己立刻返回;那条新线先 Sleep(5.0)(Delay 五秒)再收尾。提醒:能用结构化的四个(sync/race/rush/branch)就别裸用 spawn;Sleep(0.0) 是「等一帧再继续」的标准写法。会等待与时间流详见 第 23 课,五表达式详见 第 24 课。
九、设备样板:@editable + Subscribe(今天在 UEFN)
今天 Verse 唯一能真正跑起来的地方是 UEFN,那里「能放进关卡的东西」叫 creative_device。下面这台设备是所有 UEFN Verse 代码的最小骨架。
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
my_device := class(creative_device):
@editable
Button:button_device = button_device{} # 面板里连上真设备
OnBegin<override>()<suspends>:void =
Button.InteractedWithEvent.Subscribe(OnPressed)
OnPressed(Agent:agent):void =
Print("有玩家按下了按钮")
▢ 读一下这段:这是一台最小设备的骨架——@editable 的 Button(面板上那个小眼睛,进关卡后连上真按钮);OnBegin 就是 Event BeginPlay,一开场就用 Subscribe(Bind Event)把按钮的「被交互」事件绑到自己的 OnPressed 上;按钮一按,OnPressed 就跑、打印一行字。要生效:设备得从内容浏览器拖进关卡、并在 Details 面板把 @editable 连上实物;回调的参数要和事件递来的数据对上(按钮给的是 agent);想在回调里做要等待的事就 spawn。第一台设备详见 第 6 课,事件与 @editable 详见 第 25 课,完整毕业项目详见 第 29 课。
| 这一节的 Verse | 蓝图里是什么 | 一句话差异 |
|---|---|---|
class(creative_device) | Blueprint Actor | 不是资产,是一段声明;拖进关卡才会跑 |
@editable 字段 | Details 面板上的一栏 | 面板是代码的镜子,没标就没有那一栏 |
OnBegin<override>()<suspends> | Event BeginPlay | 自带 <suspends>,能挂一整个游戏循环 |
.Subscribe(F) | Bind Event | 回一个 cancelable;只绑一次,重复绑会叠加 |
Print("…") | Print String | 要先 using Diagnostics 模块 |
十、Scene Graph:entity 与 component(UE6 的方向)
状态,先说清楚:Scene Graph 是 UE6 的新玩法框架,从零基于 Verse 构建,用 entity(实体)装 component(组件)。它目前在 UEFN 中以 Beta 提供,Epic 官方文档明确写着「谨慎用于正式发布」,部分工具还要在项目设置的 Beta Access 里开启。与此同时:Actor 与 Blueprint 在 UE6 Early Access(目标 2027 年底)及早期版本中完整支持,弃用要等 Scene Graph 足够成熟,时间未定;Epic 已承诺在弃用前提供转换工具,但尚未发布。
下面这段骨架取自 Epic 官方文档的写法。Beta 阶段签名会变,写代码时以官方文档为准(本节末尾附来源链接)。
using { /Verse.org }
using { /Verse.org/Native }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/SceneGraph }
# 自定义组件必须挂 <final_super>,并直接派生自 component
my_component := class<final_super>(component):
@editable
WaitSeconds:float = 3.0 # 和设备一样,冒到 Details 面板
# 开始模拟时跑一次(同步),相当于组件版的 Event BeginPlay
OnBeginSimulation<override>():void =
(super:)OnBeginSimulation()
# 长命的异步逻辑写这里:能等时间、等事件、挂循环
OnSimulate<override>()<suspends>:void =
Sleep(WaitSeconds)
# 结束模拟时收尾
OnEndSimulation<override>():void =
(super:)OnEndSimulation()
▢ 读一下这段:这是一个自定义组件的最小骨架。<final_super> 是 Epic 的硬性要求——组件必须直接派生自 component,少了它这个类就加不到 entity 上(在它之上再派生子类则不需要重复写)。@editable 的用法和设备一模一样,这一点从设备迁到组件不用重新学。三个生命周期函数分别对应「开始模拟 / 长命异步逻辑 / 结束模拟」,官方示例里都是先调一次 (super:) 的同名实现再写自己的。
# 组件天生知道自己挂在哪个 entity 上:直接写 Entity
if (Mesh := Entity.GetComponent[mesh_component]):
# 方括号 = 可能走不通:这个 entity 未必装了网格组件
Print("拿到了同一个 entity 上的网格组件")
Parent := Entity.GetParent() # 往上找
Kids := Entity.GetEntities() # 往下找子实体
Comps := Entity.GetComponents() # 这个 entity 上的组件
▢ 读一下这段:在 Scene Graph 里,「拿到别的东西」不再是蓝图里的 Get All Actors of Class,而是沿着 entity 的层级找。组件里的 Entity 是一个现成的数据成员,指向「我挂在的那个实体」;GetComponent[…] 用方括号,因为那个组件未必在。层级本身就是数据结构——这是 Scene Graph 和 World Outliner 最大的区别。
| Scene Graph 里的写法 | 蓝图里是什么 | 说明 | 状态 |
|---|---|---|---|
entity |
Actor | 几乎空的容器,靠装组件获得功能;继承 entity 的类官方称为 prefab |
Beta |
class<final_super>(component) |
Actor Component | 行为和数据唯一的落脚点 | Beta |
OnAddedToScene |
组件被添加到世界 | 官方列出的生命周期之一;完整签名以官方文档为准 | Beta |
OnBeginSimulation<override>():void |
Event BeginPlay(组件版) | 同步的一次性初始化 | Beta |
OnSimulate<override>()<suspends>:void |
没有对应物 | 组件的长命异步逻辑,能挂循环、等事件 | Beta |
OnEndSimulation<override>():void |
Event EndPlay(组件版) | 收尾 | Beta |
OnRemovingFromScene |
组件即将被移除 | 官方列出的生命周期之一;完整签名以官方文档为准 | Beta |
Entity |
GetOwner() |
组件的数据成员,指向它挂在的实体 | Beta |
TickEvents.PrePhysics / .PostPhysics |
Event Tick | 把回调挂上去拿到每帧时机;签名以官方文档为准 | Beta |
Entity.GetComponent[t] |
Get Component by Class | 方括号 = 可能走不通 | Beta |
Entity.AddComponents(…) / RemoveFromEntity() |
Add Component / Destroy Component | 同一类组件在一个 entity 上只能有一个 | Beta |
GetParent() / AddEntities() / RemoveFromParent() |
Attach To / Detach | 层级即数据结构,不只是摆放关系 | Beta |
AddTag() / ContainsTag() / RemoveTag() |
Actor Tag | entity 自带一套标签接口 | Beta |
SendUp() / SendDown() / OnReceive() |
沿层级广播一个通知 | 把场景事件顺着 entity 树送出去,沿途组件接 | Beta |
IsInScene() / IsSimulating() |
Is Valid 之类的状态查询 | 官方描述为「成功即代表是」,即可失败调用 | Beta |
本节所有名称与骨架取自 Epic 官方文档。entity 的 API 参考页把它标在 /Verse.org/SceneGraph 模块下,而官方教程页示例的 using 清单是上面那四行——两者以官方文档为准。OnAddedToScene 与 OnRemovingFromScene 的完整签名官方页面未给出,这里只列概念占位,不要照抄猜测的签名。Scene Graph 的完整世界观见 第 26 课,写一个组件见 第 27 课。
来源
十一、持久化模式
# 模块作用域(类的外面)——位置本身就是持久化开关
var PlayerCoins:weak_map(player, int) = map{}
AddCoins(Player:player, Amount:int):void =
if (Old := PlayerCoins[Player], set PlayerCoins[Player] = Old + Amount):
Print("金币到账,跨局也不丢")
▢ 读一下这段:把存金币的 weak_map 放在「类的外面」(模块层)——这个位置本身就是存档开关,数据会每个玩家一份、跨局自动保留。AddCoins 里,读旧值和写新值都是「可能走不通」的,所以整句包进一个 if(读到旧值、当场把新值 set 回去,一步到位)。切记:玩家第一次进图,得先 if (set PlayerCoins[Player] = 0) {} 把初值铺好,才谈得上累加。详见 第 28 课。
player_profile := class<final><persistable>:
Level:int = 1
Coins:int = 0
var Profiles:weak_map(player, player_profile) = map{}
# 更新 = 读旧实例 → 用旧值构造新实例 → 整体 set 回去
▢ 读一下这段:把要存的一堆数据(等级、金币)打包成一个 <persistable> 的类,再用一张以玩家为键的 weak_map 存这些实例;更新的套路是「读出旧实例 → 拿旧值造个新实例 → 整体 set 回去」。记住 weak_map 三不能:问不出总数、不能遍历、键是弱引用;键必须是 player,拿到的是 agent 就先 player[Agent] 转一下(Cast to Player)。以后还会加字段的数据用 class 装最稳(发布后只有 class 能加带默认值的新字段)。详见 第 28 课。
| 这一节的 Verse | 蓝图里是什么 | 一句话差异 |
|---|---|---|
模块层的 weak_map(player, t) | SaveGame / Save Game Object | 「放在类的外面」这个位置本身就是存档开关 |
class<final><persistable> | SaveGame Object 的结构 | 只能放不可变字段,且类必须 <final> |
player[Agent] | Cast to Player | 方括号 = 可能走不通,要包进 if |
| (没有) | Save Game to Slot / Load Game from Slot | 没有显式存读盘调用,读写 weak_map 就是存读 |