语法速查:一页扫完 Verse 的全部招式
对应官方 Verse Quick Reference 的中文速查页:23 课的核心语法浓缩在此,每个知识点一段最小代码、一句要点、一个详解课号。写代码卡壳时回来扫一眼,顺着课号跳回去补课。
一、变量、常量与类型标注
# 常量:定义即终身,不可再改;:= 省略类型让编译器推断
MaxHealth:int = 100
Nickname := "泡泡"
# 变量:var 声明,带类型标注;修改必须喊 set
var Health:int = 100
set Health = 80
set Health += 15
▢ 要点:三个符号各司其职——:= 只管首次定义,set … = 只管给 var 赋值,裸 = 是可失败的比较,没有 ==。详见 第 4 课。
▢ 读一下这段:上半截造了两个「定死不改」的常量(相当于建变量时不给它接 Set 节点);下半截 var Health 是个可改的变量,后面每次改都得用 set(就是接一个 Set 节点)。记住三个符号::= 只管第一次定义、set … = 才是改值(接 Set)、光一个 = 是「判断相等」的检定,而且根本没有 ==。详见 第 4 课。
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) # 元组
▢ 要点:标注一律「名字:类型」;int 与 float 不互转——int 变 float 乘 1.0,float 变 int 用可失败的 Floor[X]。数字详见 第 5 课,字符串详见 第 6 课,logic 详见 第 7 课。
▢ 读一下这段:每一行都是「名字:类型 = 值」,给一个变量定好它装什么(整数、小数、文字、Boolean、数组、映射、盒子、元组),对应蓝图里建变量时先选好变量类型。两个坑:int 和 float 不会自动互转——整数变小数要乘 1.0,小数变整数得用「可能失败」的 Floor[X](向下取整);写小数一定带小数点。数字详见 第 5 课,字符串详见 第 6 课,logic 详见 第 7 课。
二、运算符
if (A = B, B <> C): # 判等是 =,不等是 <>;没有 ==
Print("逗号就是 and")
if (X > 0 and Y > 0): # 逻辑词 and / or / not,没有 && || !
Print("比较全家都是可失败表达式")
if (Ready?): # 后缀 ?:logic 为 true 才成功
Print("裸写 if (Ready) 编译不过")
▢ 要点:比较运算符不返回布尔值,它们以「成功/失败」参与控制流,所以只能住在失败上下文里。详见 第 9 课。
▢ 读一下这段:三段 if 都像 Branch,但注意它们问的不是「Boolean 是不是 true」,而是「括号里这件事走不走得通」。所以判等用一个 =、判不等用 <>(没有 ==),逻辑连接用 and / or / not(没有 && || !),逗号也当 and 用;想拿 Boolean 当条件得补个 ?。一句话:这些比较不吐 true/false,而是以「走通 / 走不通」参与流程,所以只能待在「允许失败的地方」。详见 第 9 课。
| 符号 | 作用 | 备注 |
|---|---|---|
:= |
定义并初始化 | 只用于首次定义,可省类型 |
set X = … |
给 var 赋值 | 复合赋值 += -= *= /=(int 的 /= 不行) |
= <> < <= > >= |
比较,可失败 | 只能出现在失败上下文 |
+ - * / |
算术 | int / int 可失败且得 rational:if (Half := Floor(X / 2)): |
Mod[A, B] |
求余 | 没有 %;方括号 = 可失败调用 |
and or not |
组合成功/失败 | 没有 && || !;not 内的效果永远回滚 |
X? |
查询 | logic 为 true / option 非空才成功 |
▢ 这张表按蓝图直觉认一遍::= 第一次建变量并赋初值;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 是表达式,可直接取值
▢ 要点:失败上下文是事务——条件里的 set 在失败时整体回滚,像没发生过;所以条件里也禁止调用 Print 等不可回滚的函数。详见 第 8 课。
▢ 读一下这段:上面这个 if 像一串接龙的 Cast——先把 Agent 转成 player、再从它身上取到角色实体,任何一环走不通就整体走 else。下面 if (A > B) then A else B 则是把 if 当「二选一」用(相当于 Select 节点),直接算出较大值。关键脾气:「允许失败的地方」是可回滚的——条件里用 Set 改过的值,一旦失败就整体撤销、像没改过,所以条件括号里也不许放 Print String 这种收不回的动作。详见 第 8 课。
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 是表达式,收集成新数组
▢ 要点:for 里没有 break/continue;「跳过」用过滤子句表达,「列表推导」直接取 for 的值。详见 第 10 课。
▢ 读一下这段:四种 ForEach 写法——逐个取元素、连序号一起取、按数字范围跑(还能顺手加个过滤条件,不满足就跳过这一轮)、以及把每轮算出的值收集成一个新数组。注意 Verse 的 ForEach 里没有 break / continue:想「跳过某轮」就写成过滤条件,想「边跑边收集」直接拿整个 for 的结果当数组用。详见 第 10 课。
var Round:int = 0
OnBegin<override>()<suspends>:void =
loop:
set Round += 1
if (Round > 3):
break # break 只属于 loop,for 里不能用
Sleep(1.0) # 每轮至少一个挂起点,否则判死循环
▢ 要点:Verse 没有 while,一律 loop + 条件 break;同步 loop 既不 break 又不挂起会触发 ErrRuntime_InfiniteLoop,整台设备的 Verse 全部停摆。详见 第 11 课。
▢ 读一下这段:loop 是一根自己绕回去、无限重复的执行线(相当于 While Loop),每轮把回合数 +1,超过 3 就 break 跳出,末尾放一个 Sleep(1.0)(Delay)让它歇一帧。两个铁律:break 只归 loop 用、ForEach 里不能用;而且同步 loop 里要是既不 break 又不放个 Delay,就会触发死循环错误 ErrRuntime_InfiniteLoop,整台设备的 Verse 全罢工。详见 第 11 课。
Countdown()<suspends>:void =
Print("倒计时开始")
defer:
Print("离开作用域时才执行,提前退出也会跑")
Sleep(3.0)
Print("三、二、一")
▢ 要点:defer 必须先被控制流「经过」才算登记;写在没走到的分支里就不会执行。详见 第 11 课。
▢ 读一下这段:defer 里的那行是「离开这个函数之前,最后一定补跑」的收尾线——所以哪怕它写在中间,真正执行也排在末尾。关键:必须先让执行流「经过」defer 把它登记上才算数;要是它藏在一条根本没走到的岔线里,就不会补跑。详见 第 11 课。
game_phase := enum{Lobby, Battle, Podium}
PhaseLabel(Phase:game_phase):string =
case (Phase):
game_phase.Lobby => "大厅"
game_phase.Battle => "战斗"
game_phase.Podium => "颁奖台"
▢ 要点:对默认(closed)的 enum,case 写全所有成员就不需要兜底分支,漏了成员编译器会点名。详见 第 7 课。
▢ 读一下这段:case 就是蓝图里的 Switch on Enum——按枚举当前是哪一项,分头走对应的线,这里把三种游戏阶段各翻译成一句中文交出去。好处:只要把枚举的每一项都写全了,就不用再留一个「其它」兜底分支;漏写了哪一项,一按 Compile 编译器会点名。详见 第 7 课。
四、容器: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 不会失败
▢ 要点:数组是不可变值,没有 .Add/.Push;越界不崩溃,只是「失败」走 else。详见 第 12 课。
▢ 读一下这段:建一个数组;按序号取第 0 个,但这一步「可能走不通」,所以包进 if;连改某个元素、往后追加,也都是这个套路(追加其实是拼出一个新数组);.Length 取总数才是稳的、不会失败。要点:Verse 数组是「只读值」,没有 .Add / .Push 这种就地改;序号越界不会崩,只是这一步失败、走 else。详见 第 12 课。
var Scores:[string]int = map{"泡泡" => 10}
if (Score := Scores["泡泡"]): # 查询可失败:查无此键走 else
Print("查到 {Score} 分")
if (set Scores["阿奇"] = 0) {} # 连写入都要包 if
for (Name -> Score : Scores): # 按写入顺序遍历
Print("{Name}: {Score}")
▢ 要点:字面量配对用 =>,遍历读出用 ->;没有 Remove,删除靠「过滤重建」。详见 第 13 课。
▢ 读一下这段:建一张「名字 → 分数」的登记表(Map);拿名字查分数「可能查不到」,所以包进 if;连写入一个新键都要包 if;最后用 ForEach 按写入顺序把每对键值逐个读出来。注意两个箭头别搞混:建表配对用 =>,遍历读出用 ->;没有 Remove,想删只能「过滤出想留的、重建一张」。详见 第 13 课。
var MaybeScore:?int = false # false 就是「空盒子」
set MaybeScore = option{42} # 装入值用 option{},不是 option()
if (Score := MaybeScore?): # 后缀 ? 解包,空则失败
Print("盒子里是 {Score}")
MaybeFirst := option{Numbers[0]} # 把可失败表达式装罐成 option
▢ 要点:option{…} 本身就是失败上下文——内部失败得到空盒子,是「失败」与「缺值」之间的官方桥梁。详见 第 14 课。
▢ 读一下这段:?int 是个「可能装了整数、也可能是空」的盒子,false 就是空盒;装东西用 option{42};想取出来在后面加 ?,空盒就走不通、走 else。最后一行把一个「可能失败」的取值直接塞进 option{…}——因为 option{…} 本身就是个「允许失败的地方」:里面那步失败了不报错,只是给你一个空盒。它正是「走不通」和「没有值」之间的官方桥梁。详见 第 14 课。
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)}")
▢ 要点:tuple 用圆括号 T(0)、array 用方括号 A[0];tuple 下标必须是编译期常量,不能拿变量当索引。详见 第 14 课。
▢ 读一下这段:把「一个整数 + 一个小数」捆成一小包,用圆括号装、也用圆括号 Point(0) 取第几个——因为包里几样、每样啥类型都是编译时定死的,取值不会失败。下面的 GetXY() 演示了元组最大的用处:让一个函数一次交出好几个返回值。记住:元组取值用圆括号 T(0)、数组取值用方括号 A[0],而且元组的序号必须写死,不能拿变量当序号。详见 第 14 课。
五、函数定义
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)) # 函数也能当参数传
▢ 要点:参数是不可变的,想改先拷进局部 var;写 Foo 不带括号拿到的是函数本身而不是调用。详见 第 15 课。
▢ 读一下这段:Add 是个最普通的蓝图函数,最后算出的值自动就是返回值(不用专门放 Return 节点)。Award 演示带默认值的可选参数(名字前加 ?),调用时可以不传、也可以按名字传。Twice 更进一步:函数还能当参数塞给另一个函数。两个坑:传进来的参数是「只读」的,想改先拷进一个局部 var;写一个函数名 Foo 不加括号,拿到的是「这个函数本身」而不是它的运行结果。详见 第 15 课。
# <decides> 必须与 <transacts> 成对出现:失败要能回滚
IsBig(X:int)<transacts><decides>:void =
X > 100
if (IsBig[200]): # 可失败函数用方括号调用
Print("圆括号调普通函数,方括号调可失败函数")
▢ 要点:decides 函数体内禁止显式 return;想「找到就带值退出」,用 var Ret:?t = false 收集,最后一行 Ret? 解包。详见 第 15 课 与 第 18 课。
▢ 读一下这段:给函数挂上 <decides> 就把它变成「可能走不通」的蓝图函数,里面 X > 100 这一步没过、整个函数就算失败;这种函数必须同时挂 <transacts>(可回滚),而且调用时改用方括号 IsBig[200](圆括号调普通函数、方括号调可失败函数)。一个坑:这种函数体内不许写 return;想「找到就带着值退出」,惯用法是先备一个空盒子 var Ret:?t = false 收集,最后一行用 Ret? 把它拆出来。详见 第 15 课 与 第 18 课。
六、类型定义:class / struct / interface / enum
enemy := class:
Name:string # 无默认值 → 实例化时必填
var Health:int = 100
TakeDamage(Amount:int):void =
set Health -= Amount # 方法里用 Self 指自己
Grunt := enemy{Name := "小兵"} # 原型语法实例化,没有 new
▢ 要点:继承写 child := class(base):,重写成员必加 <override>,调父类实现是 (super:)Method();类是引用语义。详见 第 16 课。
▢ 读一下这段:这就是定义一个蓝图类——列出字段(没给默认值的,造实例时必须填)、写方法(方法里用 Self 指「我自己」),最后 enemy{Name := "小兵"} 造一个实例(没有 new,直接花括号填字段)。继承写成 child := class(base):(相当于 Child Blueprint Class),重写父类成员必须挂 <override>(对应那个 Override 下拉),调父类原版写 (super:)Method();类是「引用」语义(传的是同一个实例,不是拷贝)。详见 第 16 课。
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 不许 var 字段、不许写方法——想加行为用扩展方法 (S:point).Norm():float = …。详见 第 17 课。
▢ 读一下这段:struct 就是右键新建的那个 Structure 结构体资产,一小包纯数据。它是「值」语义:Q := point{X := P.X, …} 这种「改一下」其实是照着旧的造一个全新实例,赋值、传参都是整包拷贝、互不影响。规矩:结构体里不许放会变的 var 字段、也不许写方法——想给它加行为,用「扩展方法」(S:point).Norm():float = … 从外面挂上去。详见 第 17 课。
rideable := interface:
Mount():void # 只有签名,没有实现
bicycle := class(rideable):
Mount<override>():void = # 实现每个接口方法都要 <override>
Print("上车,出发")
▢ 要点:类只能单继承一个类,但可同时实现多个接口 class(base, iface1, iface2);向下转型 if (C := child_type[Base]): 是可失败表达式。详见 第 17 课。
▢ 读一下这段:interface 就是蓝图接口(BPI)——只列函数签名、不写实现;下面这个类答应了这个接口,就得把 Mount 补齐,而且实现接口方法也要挂 <override>。规矩:一个类只能认一个父类,但能同时答应好几个接口 class(base, iface1, iface2);想把父类引用「向下转」成具体子类型,用 if (C := child_type[Base]):——这一步「可能走不通」,就是蓝图里的 Cast。详见 第 17 课。
card_suit := enum{Clubs, Diamonds, Hearts, Spades}
MySuit := card_suit.Hearts # 成员通过类型名访问
if (MySuit = card_suit.Hearts): # 只支持 = 与 <> 判等
Print("是红桃") # 想转文字?自己写 case 映射
▢ 要点:enum 成员没有底层整数,不能转 int/string、不能比大小;定义要放在模块作用域。详见 第 7 课。
▢ 读一下这段:enum 就是右键新建的 Enumeration 枚举资产,列几个起好名的选项;用「类型名.成员名」取某一项,判断是不是某一项用 =(只支持 = 和 <>)。要点:枚举成员背后没有藏整数,不能转成数字或文字、也不能比大小;想变成显示文字得自己写一段 case 映射。定义要放在类的外面(模块层)。详见 第 7 课。
七、修饰符速查表
尖括号的叫说明符(specifier),编译器强制执行;@ 开头的叫属性(attribute),写在声明的上一行,影响编辑器行为。
尖括号 <…> 里的叫说明符(specifier),一按 Compile 编译器就照着它检查你;@ 开头的叫属性(attribute),写在声明的上一行,主要影响 UEFN 编辑器里的表现(比如 @editable 让字段冒到 Details 面板)。下面这张表挑你用得上的记。
| 修饰符 | 一句话 | 写在哪 |
|---|---|---|
<public> <internal> <protected> <private> |
访问权限;默认是 internal(模块内可见),不是 public | 标识符后;加在 var 上可读写分权:var<protected> Ammo<public>:int |
<override> |
重写父类成员,必加 | 成员名后 |
<abstract> |
不能实例化,可含无实现方法 | class<abstract> |
<final> |
类不许继承 / 成员不许重写 | class 或成员 |
<concrete> |
全字段有默认值,可 my_class{} 空原型实例化;@editable 自定义类必备 |
class<concrete> |
<unique> |
实例有身份,可用 = 比较、可作 map 键(player 就是这么来的) | class<unique> |
<transacts> |
动作可回滚;失败上下文里只欢迎它 | 效果:参数表后、返回类型前 |
<decides> |
可失败,方括号调用;必须搭 <transacts> |
效果 |
<suspends> |
异步,可跨帧挂起;只能在异步上下文直接调用 | 效果 |
<computes> <varies> <converges> |
无副作用系互斥效果;什么都不标 = no_rollback(不可回滚) | 效果 |
<persistable> |
可进存档;类须 <final> 且只能有常量字段 |
class / struct 后 |
<localizes> |
把插值字符串包装成 message(UI 文本要的类型) | 函数 / 常量 |
@editable |
把字段暴露到细节面板,改值不用重编译 | 属性:字段声明的上一行 |
▢ 要点:效果说明符是 Verse 把「这个函数会干什么」写进类型签名的机制——报错 no_rollback / decides / suspends 时,先看这张表。详见 第 18 课。
▢ 这张表全是一按 Compile 可能亮红的关键词。按蓝图直觉挑几条:<override> = 重写父类成员(那个 Override 下拉);<concrete> = 所有字段都有默认值、能空手 {} 造实例,自定义 @editable 类必备;<unique> = 实例带身份证、能当 map 键(玩家就这么来的);<transacts> = 动作可回滚,只有它受「允许失败的地方」欢迎;<suspends> = 会跨帧等待(体内有 Delay 那类节点);@editable = 就是 Instance Editable 小眼睛,把字段亮到 Details 面板。「效果」这一类其实就是把「这个函数会干什么」写进签名——报错提到 no_rollback / decides / suspends 时,先回来看这张表。详见 第 18 课。
八、并发五表达式
| 表达式 | 语义 | 其余任务的命运 | 可用位置 |
|---|---|---|---|
sync: |
全部完成才继续,结果打包成 tuple | —— | 异步上下文 |
race: |
第一名完成即结束,值取冠军 | 在下个挂起点被取消 | 异步上下文 |
rush: |
第一名完成即返回 | 继续跑,随外层作用域收尾 | 异步上下文 |
branch: |
先启动,主线继续 | 随所在作用域退出被撤销 | 异步上下文 |
spawn{ Fn() } |
放出独立任务,返回 task | 不被取消,自己跑完 | 唯一可在同步代码里用 |
这五个都是「同时跑好几条线」的玩法,按蓝图直觉认:sync = 等所有线到齐再往下(结果打成一小包);race = 谁先到听谁的,其余的线当场剪断(超时就用它);rush = 也取最先到的,但落后的线不剪断、让它们跑完;branch = 岔出去跑、主线继续,等这段结束时自动清场(有人管的 spawn);spawn = 硬岔出一条没人管生命周期的线,是唯一能在普通同步代码里用的。前四个只能在「会等待的上下文」里用。
GateOnce()<suspends>:void =
race:
block:
Button.InteractedWithEvent.Await() # Button 是 @editable 引用
Print("及时按下!")
block:
Sleep(10.0)
Print("超时,自动继续")
▢ 要点:超时配方——事件与闹钟赛跑,谁先到听谁的;多行的臂用 block: 打包,各臂结果类型要兼容。详见 第 20 课。
▢ 读一下这段:race 让两条线赛跑——一条等按钮按下(Await() 挂起等事件,Button 是面板上连好的 @editable 引用),另一条 Sleep(10.0) 当闹钟计时;谁先到就走谁,输的那条当场被剪断。这就是「超时」的标准配方:事件和闹钟赛跑。每条臂里有好几行时用 block: 打包成一条线,各臂最后给的结果类型要能兼容。详见 第 20 课。
OnPressed(Agent:agent):void = # Subscribe 回调不是异步函数
spawn{ CloseAfterDelay() } # spawn 体只能是单个异步调用
CloseAfterDelay()<suspends>:void =
Sleep(5.0)
Print("五秒到,收尾")
▢ 要点:官方建议能用结构化(sync/race/rush/branch)就别用 spawn;Sleep(0.0) 挂起到下一帧,是「等一帧」的标准写法。suspends 与时间流详见 第 19 课,五表达式详见 第 20 课。
▢ 读一下这段:按钮的 Subscribe 回调 OnPressed 本身不是「会等待」的函数,想在里面做需要等待的事,就 spawn{ CloseAfterDelay() } 岔一条新线出去(花括号里只能放单个异步调用),自己立刻返回;那条新线先 Sleep(5.0)(Delay 五秒)再收尾。提醒:能用结构化的四个(sync/race/rush/branch)就别裸用 spawn;Sleep(0.0) 是「等一帧再继续」的标准写法。会等待与时间流详见 第 19 课,五表达式详见 第 20 课。
九、设备样板:@editable + Subscribe
using { /Fortnite.com/Devices }
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 连上实物才会生效;回调签名必须匹配事件载荷(按钮是 agent),想在回调里做异步就 spawn。第一台设备详见 第 2 课,事件与 @editable 详见 第 21 课,完整毕业项目详见 第 23 课。
▢ 读一下这段:这是一台最小设备的骨架——@editable 的 Button(面板上那个小眼睛,进关卡后连上真按钮);OnBegin 就是 Event BeginPlay,一开场就用 Subscribe(Bind Event)把按钮的「被交互」事件绑到自己的 OnPressed 上;按钮一按,OnPressed 就跑、打印一行字。要生效:设备得从内容浏览器拖进关卡、并在 Details 面板把 @editable 连上实物;回调的参数要和事件递来的数据对上(按钮给的是 agent);想在回调里做要等待的事就 spawn。第一台设备详见 第 2 课,事件与 @editable 详见 第 21 课,完整毕业项目详见 第 23 课。
十、持久化模式
# 模块作用域(类的外面)——位置本身就是持久化开关
var PlayerCoins:weak_map(player, int) = map{}
AddCoins(Player:player, Amount:int):void =
if (Old := PlayerCoins[Player], set PlayerCoins[Player] = Old + Amount):
Print("金币到账,跨局也不丢")
▢ 要点:读和写都是可失败操作;玩家首次进图先 if (set PlayerCoins[Player] = 0) {} 铺好初值,再谈累加。详见 第 22 课。
▢ 读一下这段:把存金币的 weak_map 放在「类的外面」(模块层)——这个位置本身就是存档开关,数据会每个玩家一份、跨局自动保留。AddCoins 里,读旧值和写新值都是「可能走不通」的,所以整句包进一个 if(读到旧值、当场把新值 set 回去,一步到位)。切记:玩家第一次进图,得先 if (set PlayerCoins[Player] = 0) {} 把初值铺好,才谈得上累加。详见 第 22 课。
player_profile := class<final><persistable>:
Level:int = 1
Coins:int = 0
var Profiles:weak_map(player, player_profile) = map{}
# 更新 = 读旧实例 → 用旧值构造新实例 → 整体 set 回去
▢ 要点:weak_map 三不能——没有 Length、不能遍历、键是弱引用;键必须是 player,拿到 agent 先 player[Agent] 转型。会演化的数据用 class 装(发布后只有 class 能加带默认值的新字段)。详见 第 22 课。
▢ 读一下这段:把要存的一堆数据(等级、金币)打包成一个 <persistable> 的类,再用一张以玩家为键的 weak_map 存这些实例;更新的套路是「读出旧实例 → 拿旧值造个新实例 → 整体 set 回去」。记住 weak_map 三不能:问不出总数、不能遍历、键是弱引用;键必须是 player,拿到的是 agent 就先 player[Agent] 转一下(Cast to Player)。以后还会加字段的数据用 class 装最稳(发布后只有 class 能加带默认值的新字段)。详见 第 22 课。