技巧 · EXTRA
读写分离的变量权限:var<private> Ammo<public>
类里的变量可以「谁都能读、只有自己能接 Set」——一个小设置管「谁能读」、一个管「谁能改」,不写多余节点就白拿一个对外只读的变量。
进入拓展 →你在变量面板里新建的每一个变量,在 Verse 里都是一行代码。第三章从数据开始,这一课发两件基础装备:刻好就不换的名字牌(常量),和随时能换内容的背包格子(变量 var)。学会怎么给数据贴类型标签、怎么用 := 新建、怎么用 set(也就是蓝图里的 Set 节点)改值,再认得两条最常见的 Compile 报错,你的变量面板就搬进代码了。
游戏里的数据分两种脾气:一种从开局到结束都不该变——竞技场的名字、生命值上限、每回合的时长;另一种时时刻刻在变——金币数、分数、剩余弹药。Verse 给这两种数据发了两套装备,先看第一套:常量。
声明一个常量的完整写法是 名字:类型 = 值,三段式:名字是刻在牌子上的字,:类型 规定这块牌子只能配哪种值,= 值 是当场刻上去的内容。注意——没有任何额外关键字。在 Verse 里,「不可变」是默认待遇,不用申请。
using { /Fortnite.com/Devices }
arena_device := class(creative_device):
# 完整写法:名字:类型 = 值
MaxHealth:int = 100
ArenaName:string = "新手竞技场"
# 简写::= 让 Verse 自己推断类型
# 12 是整数字面量,推断出 int
MaxPlayers := 12
读图翻译:这段等于打开 arena_device 蓝图的「变量面板」,新建三个变量——MaxHealth(类型 Integer、默认 100)、ArenaName(类型 String、默认「新手竞技场」)、MaxPlayers(类型 Integer、默认 12)。三个都是常量:意味着整张蓝图里没有任何一个 Set 节点会碰它们。:= 这个写法相当于「新建变量并填好默认值」一步到位,而且类型那一栏 Verse 会照着默认值替你选好。
第三个例子用了简写 :=:名字和初始值一步到位,类型不用你手填,Verse 照着右边的值自己猜——12 是整数,于是 MaxPlayers 就是整数型(蓝图变量面板里的 Integer)。眼熟吗?我们前几课一直在写的 arena_device := class(creative_device):,其实就是给一个蓝图类挂名字牌,和给数字挂牌是同一个写法。
常量有两条铁律:第一,函数和模块里声明的常量必须当场给初始值,不存在「先挂块空牌子、以后再刻字」(类的字段是例外——不给默认值时,谁把这个蓝图放进关卡、谁就得在 Details 面板里补上,后面讲类再展开);第二,刻上去就焊死了,谁敢给常量接一个 Set 节点,一按 Compile 就亮红报错(第四节会看到它长什么样)。经验法则:先按常量写,真需要改再加 var 升级成可变——能不变的值就让它不变,别人打开你的蓝图一眼就知道哪些是死的、哪些是活的。
轮到会变的数据了。每个角色都有背包:一格放药水、一格放金币。程序也需要这样的「格子」来存会变的数据,这就是变量。开格子要在声明前面加一个词:var。
var Health:int = 100 一口气干了四件事:var 是「我要开一个可变格子」的申请;Health 是格子上的标签;:int 规定格子只能装整数;= 100 是开箱自带的初始物品。和常量一样,函数和模块里声明的变量也必须给初始值——Verse 不允许空格子出厂(类字段的例外同上一节)。官方文档里 var 声明总是把类型写全(就像在蓝图里新建变量时,Variable Type 那一栏一定要选好),本教程也这么写:变量是要被反复改的,把它装什么类型标明白,改的人心里才有数。
格子开好了,怎么换里面的东西?这是 Verse 最有个性的地方:必须先喊口令 set。
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
backpack_device := class(creative_device):
# var:开一个可变格子,初始 100
var Health:int = 100
OnBegin<override>()<suspends>:void =
set Health = 80
set Health -= 30
set Health += 5
Print("Health: {Health}")
set Health = 80 是整个换掉;set Health -= 30 和 set Health += 5 是在原值基础上加减(还有 *= 乘)。三行跑完,格子里躺着 55。放到蓝图里,这就是三个连着的 Set 节点,依次把 Health 改成 80、再减 30、再加 5。为什么 Verse 对「改值」这么较真?因为改状态是 bug 的重灾区:逼你每次改值都亲手接一个 Set,你就永远不会「手滑连错一根线、把某个变量悄悄改了还不自知」。这种在别处要盯着调试半天的坑,在 Verse 里根本挖不出来——少写一个 set,一按 Compile 就被拦下,连运行的机会都不给。
把常量和变量放进同一台设备,亲眼看它们各司其职:名字牌全程不动,背包格子被 Set 两次,最后用 Print String 把结果打到屏幕上。那句 {RoomName}、{GoldCoins} 的写法叫「插值」,就是把大括号里的变量名换成它当前的值,相当于蓝图里给 Print String 的文字引脚接上变量再拼起来。点「运行下一步」。
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
loot_room_device := class(creative_device):
# 常量:名字牌,挂上就不换
RoomName:string = "宝物室"
# 变量:背包格子,里面的东西可以换
var GoldCoins:int = 0
OnBegin<override>()<suspends>:void =
set GoldCoins = 30
set GoldCoins += 20
Print("{RoomName} 金币:{GoldCoins}")
点「运行下一步」,看代码怎么一行行执行。
注意第 13、14 行:格子还是那个格子,里面的东西换了两次。而 RoomName 从头到尾没人动它——也动不了。这就是本课的全部世界观:该变的就新建变量、用 Set 节点改,不该变的就挂个名字牌、不给它接 Set。
本课出场了三个长得像亲戚的符号,一不留神就会看花眼、拿错。先上速查表:
| 写法 | 作用 | 例子 |
|---|---|---|
:= |
声明并初始化(挂牌 / 开格子) | MaxPlayers := 12 |
set … = |
修改变量(只对 var 有效) | set Score = 100 |
= |
比较两边是否相等 | Score = 100(这是提问,不是赋值!) |
报错一:忘了 set。想改值,直接写了 Score = 100?Verse 里根本没有 ==,单个 = 的活儿就是「比较」——而且它是一个「可能走不通的判断」(相等就算走通,不等就走不通,后面讲 Branch 那种判断时会正式认识它)。所以一按 Compile,报错不会贴心地说「你忘了 set」,而是甩给你一句新手看不懂的话,大意是:这个可能走不通的判断必须摆在「允许走不通的地方」。以后 Compile 报错里蹦出「failure context」,先回头检查是不是把 set 漏掉了。
报错二:给常量接 Set。对一个没写 var 的名字用 set MaxHealth = 50,一按 Compile 直接被拒——左边根本不是变量,没有格子可换。这条报错倒很直白,它其实在提醒你做个设计决定:这个值真需要变吗?需要,就回到声明处补上 var(把它变成能接 Set 的变量);不需要,就删掉这行 set。
顺手补一刀:新建就得给默认值。只写 var Score:int 却不填初始值,一样过不了 Compile——函数里、模块里新建的变量都必须当场给个默认值。下面这台「药水商店」被挖掉了两个关键词,把它们填回去:
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
potion_shop_device := class(creative_device):
# 常量:药水单价,全场不变
PotionPrice:int = 25
# 库存会变——开可变格子的关键字是?
____ Stock:int = 10
OnBegin<override>()<suspends>:void =
# 卖出一瓶——改格子前要喊的口令是?
____ Stock -= 1
Print("单价 {PotionPrice},库存 {Stock}")
最后一个问题:名字牌挂上之后,在哪些地方管用?答案是:跟着「代码块」走。Verse 靠缩进把代码圈成一块块,越往里缩进就是越小的一圈——整个蓝图类是最大一圈,一个函数图是里面小一圈,以后学到的 Branch、ForEach 的身体又是更里面的一圈。就像蓝图里的 Local Variable 只在自己那个函数图里认得,Verse 里在某一圈中新建的常量或变量,也只在这一圈(和它内部更小的圈)里认得;这一圈一结束,名字就作废了。
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
scope_demo_device := class(creative_device):
OnBegin<override>()<suspends>:void =
Reward:int = 100
if (Reward > 50):
# Bonus 声明在 if 的代码块里
Bonus:int = 10
Print("块内可以用:{Bonus}")
# 到这里 Bonus 已经失效
# Print("{Bonus}") ← 取消注释就是编译错误
(这里的 if 先混个脸熟,它就是蓝图里的 Branch 判断,后面的章节细讲——现在只要知道:它冒号后面、往里缩进的那几行,是被圈进去的一小块。)读图翻译:Reward 在函数这一圈里新建;Bonus 是在 Branch 走通的那条分支里才新建的,所以只在那一小圈里认得。想在圈外用 Bonus,一按 Compile 就说不认识这个名字。解法很朴素:要么把用它的节点挪进那一圈里,要么把 Bonus 提到外面那一圈去建。
还有一条 Verse 的独门规矩:禁止同名遮蔽(shadowing)。很多语言允许在里面一圈再建一个和外面同名的变量,里面那个会悄悄「盖住」外面那个——经典的 bug 温床。Verse 干脆一刀切:外面那圈已经有 Count,里面再建一个 Count := 7 就直接 Compile 报错。解法是换个名字,让每块名字牌都独一无二。
顺带埋个彩蛋:作用域这个洋葱剥到最外层,是「模块」(可以理解成一整个资产文件夹)。你可能已经在盘算「那我在最外层放一个 var TotalKills:int = 0 当全局计数器」——很遗憾,一按 Compile 会报错:今天在 UEFN 里跑 Verse 时,模块级的可变变量有一条特殊要求(必须是 weak_map 类型,类似「每个玩家一份、退出重进还在的 SaveGame」)。为什么有这条规矩、以及它如何顺手送你一套跨对局存档,见文末拓展阅读。
这一课讲的每件事,你在蓝图的变量面板里都做过。把两边并排放,差异那一列才是要背下来的部分。
| 蓝图里的做法 | Verse 里的写法 | 差异 |
|---|---|---|
| 变量面板 → 新建变量,全程不接 Set 节点 | MaxHealth:int = 100 |
蓝图变量天生可变;Verse 反过来,不可变是默认,可变才要申请 |
| 把变量拖进图里,得到一个 Get 节点 | 直接写名字:MaxHealth |
Verse 没有「读」这个动作,名字本身就是值,不占一个节点 |
| Set 节点(连一根线把新值送进去) | set Health = 80 |
蓝图靠连线,Verse 靠关键字;漏写 set 会被 Compile 当成「比较」拦下 |
| 变量面板的 Variable Type 下拉框 | 冒号后的类型名::int / :string / :logic |
用 := 可以让 Verse 照着右边的值替你选好类型,省掉这一栏 |
| Details 面板里填 Default Value | 声明同一行的 = 100 |
Verse 把默认值和声明写在一起;函数里新建的常量和变量必须当场给值 |
| 勾选 Instance Editable(变量旁边那个眼睛) | @editable |
效果一样:拖进关卡后能在 Details 面板改。Verse 写成一行属性,不是一个勾选框 |
| 函数图里新建 Local Variable | 函数体里的 var Count:int = 0 |
作用范围同样只在本函数;但 Verse 还多一条:禁止和外层同名(不许遮蔽) |
差异都指向同一件事:蓝图把「变量」做成了一件家具——你在面板上摆好它,再用节点去读、去写,读写各占一个节点。Verse 把变量做成了一行文字:名字就是读,set 就是写,类型和默认值挤在同一行里。少了节点这层中介,代价是每一次「改」都必须被你亲手写出来,好处是打开一个 .verse 文件,谁是死的、谁是活的,一眼就能看完。
还有一处不对称值得留意:蓝图里没有「不可变变量」这个概念——最接近的是「我自己约定不给它接 Set 节点」,而这条约定编译器不帮你盯。Verse 把这条约定升级成了类型系统的一部分,于是「不小心改了不该改的值」这类 bug,在 Verse 里根本编译不过去。
背包整理完毕,来闯三道小关卡验收战利品。答对拿星星 ★,答错可以一直重试,零惩罚。
想声明一个整场游戏都不变的最大生命值,哪种写法才是 Verse 的常量?
写了 Score = 100(前面没有 set),编译器会怎么理解这行代码?
在 Branch(蓝图的 if 判断)那一小圈里新建的常量 Bonus,出了这一圈之后还能用吗?
技巧 · EXTRA
类里的变量可以「谁都能读、只有自己能接 Set」——一个小设置管「谁能读」、一个管「谁能改」,不写多余节点就白拿一个对外只读的变量。
进入拓展 →拓展 · EXTRA
为什么不能在最外层建一个全局可变变量?模块级变量的特殊规矩,以及它附赠的「退出重进还在」的跨对局存档系统。
进入拓展 →拔高 · EXTRA
常量优先、改值必须显式接 Set、= 是比较不是赋值——这些「怪癖」全部来自同一篇论文:Verse Calculus。
进入拓展 →