技巧 · EXTRA
for 即列表推导:一行完成 map + filter
生成器、过滤器、变换主体三件套,让一个 for 顶别的语言两个函数。
供货、筛选、加工三件套,让一个 for 顶别的工具「先筛再逐个转换」两步的活。
进入拓展 →结算画面上,战利品一件一件滚出来——那正是 for 的节奏。这一课学会 for (Item : Items) 点名集合、用 0..N 数回合、在括号里顺手过滤,最后解锁隐藏技能:for 本身是表达式,能直接产出一个新数组。
结算画面上,战利品一件一件滚出来——那正是 ForEach Loop 的节奏。这一课学会用 ForEach Loop 把一个数组(Array)逐个点名、用 0..N 数固定回合、在节点上顺手加一道筛选,最后解锁隐藏技能:Verse 的 for 跑完还能直接吐出一个新数组,像给 ForEach 接了个「自动收集每轮结果」的出口。
打开结算界面,战利品清单从上往下滚:木剑、药水、金币袋……每件东西被点到名,都走一遍同样的流程——展示、入库、计数。「对集合里的每个元素,做一遍同样的事」,这就是 for 的全部使命。
Verse 的 for 只有一种形态:生成器形式。写法是 for (Item : Collection): 加缩进的循环体——冒号左边的 Item 是你给「本轮元素」起的临时名字,只在循环体里有效;右边是要遍历的集合。没有 C 风格的 for (i = 0; i < n; i++),也没有 Python 的 for x in xs——那两套写法在 Verse 里都是语法错误,别客气,直接忘掉。
Verse 的 for 你就当成蓝图里的 ForEach Loop 节点:把一个数组(Array)接到它的 Array 引脚上,它就从 Loop Body 逐个吐出元素,每吐一个,就把下游那串节点跑一遍,全部吐完自动从 Completed 引脚往下走。你在括号里给「本轮这个元素」起的名字(这里叫 Item),相当于 ForEach 的 Array Element 输出引脚——只在循环体里连得到。别的语言那种要自己写计数器、手动加一的老式循环,Verse 里根本不存在,你也用不着——正如蓝图 ForEach 从不用你手动数数。
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
loot_device := class(creative_device):
# 战利品清单:一个字符串数组
Loot:[]string = array{"木剑", "药水", "金币袋"}
OnBegin<override>()<suspends>:void =
for (Item : Loot):
Print("拾取:{Item}")
读图翻译:这段就是把 Loot 这个字符串数组接进 ForEach 的 Array 引脚,Loop Body 里摆一个 Print String 节点,把每轮的 Item 拼进「拾取:__」打到屏幕左上角;数组里有三件,ForEach 就把这个 Print String 跑三遍。
三件战利品,循环体跑三轮,每轮 Item 换一件宝贝。记住 for 的性格:它是有界迭代——集合有多少元素,它就跑多少轮,跑完自动收工,绝不多转一圈。想无限转圈?那是 loop 的地盘,下一课见。另外,循环体除了缩进块,也可以写大括号:for (Item : Loot) { Print("拾取:{Item}") },单行小循环用它很顺手。另外,蓝图里不存在「用缩进还是用大括号」这种纠结——ForEach 的 Loop Body 引脚往哪几个节点连,循环体就是哪几个节点;这点纯粹是写 Verse 文本时才要操心的排版细节。
光拿到元素还不够,有时你还想知道「这是第几件」。数组遍历有个升级形态:for (Index -> Item : Loot):——箭头左边的 Index 是从 0 开始的 int 下标,右边照旧是元素本身,一次点名双倍情报。
光拿到元素还不够,有时你还想知道「这是第几件」。好消息:蓝图 ForEach Loop 你早认识——它除了 Array Element,还有个 Array Index 输出引脚,从 0 开始数。Verse 里对应的写法,就是在元素名字前面多加一个下标名(写成 Index -> Item),这个 Index 就等于 ForEach 的 Array Index 引脚,右边照旧是元素本身,一次点名拿双份情报。
另一种常见需求是「固定跑 N 轮」,比如回合数。这时用范围遍历:for (Round := 0..3):。注意两个细节:第一,范围含两端,0..3 是 0、1、2、3 共 4 轮,0..10 是 11 轮,数错一轮是老牌经典 bug;第二,范围绑定用 :=,而数组、map 遍历用 :——官方示例就是这么分工的,照着记不吃亏。范围只支持 int、步长固定为 1、只能升序,写 10..0 不报错,但一轮都不会跑(想倒数怎么办?文末拔高页有完整答案)。
另一种常见需求是「固定跑 N 轮」,比如回合数。蓝图里这有专门的节点:普通的 For Loop 节点,填上 First Index 和 Last Index,它就从头数到尾,每轮给你一个 Index。Verse 里写成 0..3 这样的范围。有两个坑要记牢:第一,范围含两端——0..3 是 0、1、2、3 共 4 轮,0..10 是 11 轮(跟蓝图 For Loop 的 Last Index 一样,是数到并且包含它),数错一轮是老牌经典 bug;第二,范围只能数整数、每次加 1、只能从小往大数,写成 10..0 这种倒着的不报错,但一轮都不跑(想倒数怎么办?文末拔高页有完整答案)。
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
roll_call_device := class(creative_device):
Loot:[]string = array{"木剑", "药水", "金币袋"}
OnBegin<override>()<suspends>:void =
# 带下标遍历:Index 从 0 开始
for (Index -> Item : Loot):
Print("格子{Index}:{Item}")
# 范围遍历:0..3 含两端,共 4 轮
for (Round := 0..3):
Print("第{Round}回合开始")
顺手记一个安全常识:范围搭配 .Length 写 for (I := 0..Loot.Length - 1) 时,就算数组是空的也不会炸——0..-1 是空范围,循环体一次都不执行,for 安静地跳过。空集合同理。
顺手记一个安全常识:蓝图里对空数组跑 ForEach 不会出事,Verse 这边同样安全。就算你用「数组长度减一」来当数到几(.Length 相当于蓝图取 Array 长度的节点),当数组是空的,这个范围会变成「从 0 数到 -1」——起点比终点大,于是一轮都不跑,for 安静跳过、不报错。空数组一律这个待遇。
现在把「带下标遍历」装上,亲眼看一台清点机跑三轮。点「运行下一步」,注意第 10、11 行被反复经过——同样两行代码,每轮的 Index 和 Item 都不一样:
现在把「带下标遍历」装上,亲眼看一台清点机跑三轮。点「运行下一步」,注意标着 Index、Item 的那两行被反复经过——就像 ForEach 的 Loop Body 被一遍遍重跑,同一串节点,每轮的 Index 和 Item 都换了人:
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
loot_counter_device := class(creative_device):
Loot:[]string = array{"木剑", "药水", "金币袋"}
OnBegin<override>()<suspends>:void =
Print("开始清点战利品……")
for (Index -> Item : Loot):
Print("格子{Index}:{Item}")
Print("清点完毕,共 {Loot.Length} 件。")
点「运行下一步」,看代码怎么一行行执行。
结算时只想展示「值钱的」战利品?其他语言的套路是循环里套一个 if。Verse 更直接:把条件写进 for 的括号里。规则是——括号里第一个子句必须是生成器(集合或范围),后面可以跟任意个过滤器(可失败表达式)或内联定义:
结算时只想展示「值钱的」战利品?蓝图里你会在 ForEach 的 Loop Body 里接一个 Branch 节点,把不合格的挡在 False 那侧。Verse 更省事:直接把筛选条件写进 for 的括号里,连单独的 Branch 都省了。规则是——括号里打头的必须是「供货的那位」(一个数组或一段范围),后面可以跟任意条筛选条件,或者顺手起个中间变量:
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
filter_device := class(creative_device):
Prices:[]int = array{5, 120, 48, 300, 9}
OnBegin<override>()<suspends>:void =
# 过滤器:只处理价格超过 100 的战利品
for (P : Prices, P > 100):
Print("贵重品入库:{P} 金币")
# 内联定义 + 过滤:先算 Taxed,再筛选
for (P : Prices, Taxed := P * 2, Taxed > 200):
Print("翻倍计税后过 200:{Taxed}")
读图翻译:第一段等于「ForEach 遍历 Prices 数组,循环头内建一道 P > 100 的筛选(相当于把 Branch 塞进了循环头),只有价格大于 100 的那几件才轮到 Print String」。第二段多了个中间变量 Taxed(先把价格乘 2),再拿它跟 200 比大小当筛选——像在 ForEach 里先接一个乘法节点、再接 Branch。
关键语义来了,官方规则是:for 的迭代与过滤子句为每次迭代创建一个失败上下文。也就是说,某一轮里 P > 100 失败,只是「跳过这一轮」(相当于其他语言的 continue)——绝不会中止整个 for,更不会让 for 整体失败。上面第一个循环,5、48、9 三轮被静默跳过,120 和 300 正常入库。上一课学的可失败表达式在这里全能用:比较、索引 Arr[I]、方括号调用,随便堆叠。
关键规则来了:for 括号里的筛选是一轮一判——某一轮 P > 100 没通过,就只是跳过这一轮(相当于 Branch 走了 False 引脚,这一件不处理),绝不会掀翻整个循环,更不会让循环「整体失败」。上面第一个循环,5、48、9 三件被静默跳过,120 和 300 正常入库。上一课讲的那些「能通过 / 通不过」的判断(比较大小、按下标取数组元素等)在这里全能用,想叠几条叠几条。
但失败上下文的老规矩也跟着来了:子句必须可回滚,所以不能在过滤子句里调用 Print 这类有副作用的普通函数,否则领一条 This invocation calls a function that has effects that are not allowed by its context。副作用请放循环体——循环体不是失败上下文,Print 随便打;反过来,体内想用 Prices[I] 这类可失败表达式,就得自己包一层 if。括号管筛选,体内管干活,分工明确。
但这道筛选有个规矩:它必须能被干净地「试一下再收回」,所以不能在筛选里放 Print 这类「做了就改屏幕、收不回」的节点,否则 Compile 会亮红报错:This invocation calls a function that has effects that are not allowed by its context。要打印、要开门这类「做了就算数」的动作,放进循环体——循环体里没有「能不能走通」的限制,Print 随便打;反过来,循环体里想用「可能取不到」的操作(比如按下标去数组捞 Prices[I],越界就捞空),得自己先加个 Branch 兜一下。一句话:括号管筛选,循环体管干活。
现在揭晓本课最帅的一招。在 Verse 里 for 不只是「执行几轮」的语句——它是表达式,整体求值为一个数组。官方的措辞是:「循环求值为值的数组,因此可以基于现有数组快速创建新数组」。每次成功迭代的循环体结果会被依序收进结果数组;被过滤掉的轮次不产出元素。
现在揭晓本课最帅的一招。在 Verse 里,for 跑完不只是「跑了几轮就完事」——它还顺手把每轮的产出收集成一个新数组交给你,像给蓝图 ForEach 自动接了个「把每轮结果 Add 进一个新 Array」的出口,而且不用你手动建那个数组、手动 Add。官方原话是:「循环求值为值的数组,因此可以基于现有数组快速创建新数组」。每成功跑通一轮,循环体算出来的那个值就被依次收进结果数组;被筛掉的那几轮不贡献元素。
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
forge_device := class(creative_device):
Prices:[]int = array{5, 120, 48, 300, 9}
OnBegin<override>()<suspends>:void =
# 范围 + 变换:产出 array{1, 4, 9, 16, 25}
Squares := for (X := 1..5) { X * X }
Print("平方数共 {Squares.Length} 个")
# 生成器 + 过滤器 + 变换:一行完成筛选加翻倍
Doubled := for (P : Prices, P > 100) { P * 2 }
Print("双倍返利清单共 {Doubled.Length} 件")
读图翻译:第一行 for (X := 1..5) { X * X } 相当于一个从 1 数到 5 的 For Loop,每轮把 Index 自乘,结果自动攒成数组 array{1, 4, 9, 16, 25} 存进 Squares。第二行是 ForEach 遍历 Prices、带一道大于 100 的筛选,通过的元素乘 2 后收进 Doubled——遍历、筛选、加工、建新数组,一口气全办了。
第二行里,生成器提供元素、过滤器筛掉便宜货、循环体做变换——合起来就是别的语言 map + filter 两个函数干的活,Verse 一个 for 全包。顺带一条分工提醒:如果变换本身是可失败的(比如 Arr[I] 索引、上一课的 int 除法),别把它塞进大括号循环体——循环体不是失败上下文;把它写成括号里的内联定义,让子句的失败上下文来兜底,失败的轮次自动被过滤掉。空集合也不慌:一轮都不跑,for 老老实实返回空数组 array{}。
第二行里,供货的数组管出元素、筛选条件筛掉便宜货、大括号里做加工——这一套等于别的工具要「先筛一遍、再逐个转换」两步才干得完的活,Verse 一个 for 全包。顺带一条提醒:如果加工这一步本身「可能算不出来」(比如按下标去数组捞 Arr[I]、或上一课那种会失败的整数除法),别硬塞进大括号——那里不兜底;把它挪到括号里当中间变量,让括号那套「一轮一判、通不过就跳过」帮你兜,算不出来的那几轮会自动被丢掉。空数组也不慌:一轮都不跑,for 老实交出一个空数组 array{}。
最后补一刀值语义:Verse 的数组是值,for 遍历的是循环开跑那一刻的集合快照。循环体内对同名 var 重新 set,本轮遍历完全不受影响——想「边遍历边改」,正确姿势是用 for 表达式先收集出新数组,循环结束后统一 set 回去。还有一个性能提醒:for 是同步执行的,所有轮次在一帧内跑完;对超大数组做重活可能触发运行时的死循环检测,分帧处理的手艺留到下一课。
最后补一刀「快照」概念:Verse 的数组是按值传递的,for 一开跑就先给集合拍了张快照,之后你在循环体里用 Set 节点把那个数组变量改了,本轮遍历照着快照走、完全不受影响——想「边遍历边改」,正确姿势是先用 for 表达式收集出一个新数组,等循环整个跑完,再用一个 Set 节点统一写回去。还有个性能提醒:for 是一口气跑完的(所有轮次挤在同一帧里,中途不喘气),对超大数组干重活可能触发引擎的死循环保护而报错;想把活分摊到好几帧慢慢做(蓝图里常用 Delay 或按 Tick 分批的那种手艺),留到下一课。
for 语法不多,坑却都很有个性,提前踩点排雷:
for (X = 0..10)(用了 =)直接报错;数组遍历用 :,范围遍历用 :=,一个萝卜一个坑。:= 绑定、数组遍历用 :,写错(比如只用了一个 =)会 Compile 报错。蓝图连线时你碰不到这些符号,可一旦动手写 Verse,就得认得它俩的分工:一个萝卜一个坑。10..0——不报错、不警告,循环体安静地一轮不跑,查这种 bug 最费头发。10..0——不报错、不警告,Loop Body 安静地一轮都不跑,查这种 bug 最费头发。雷排完了,上手练一把。下面这台巡逻机被挖掉两个符号:一个是范围遍历的绑定符,一个是「大于」的比较符。填回去,点「检查答案」:
雷排完了,上手练一把——这题练的是「读写 Verse」的手感。下面这台巡逻机被挖掉两个符号:一个是范围遍历前面用来绑定的符号,一个是「大于」的比较符。填回去,点「检查答案」:
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }
patrol_device := class(creative_device):
Scores:[]int = array{55, 72, 91, 38, 87}
OnBegin<override>()<suspends>:void =
# 范围遍历:0..2 共三轮(范围要用哪个符号绑定?)
for (Round ____ 0..2):
Print("第{Round}轮巡逻")
# 过滤子句:只报出大于 60 的分数
for (S : Scores, S ____ 60):
Print("高分:{S}")
清单滚完,来三道小关验收。答错零惩罚,可以一直重试。
for (S : Scores, S > 40) 遍历到 S = 12 这一轮,会发生什么?一个 ForEach 遍历 Scores、带一道「S 要大于 40」的筛选,当轮到 S = 12 这一件时,会发生什么?
Squares := for (X := 1..3) { X * X } 之后,Squares 是什么?一个从 1 数到 3 的 for,每轮把当前数字自乘、再把结果收集成数组(写作 Squares := for (X := 1..3) { X * X }),跑完 Squares 是什么?
想让 for 在找到目标后提前退出,正确做法是?