对动态数量的任务做 race:事件聚合模式
在蓝图里等「N 个按钮里最先被按下的那个」,你会给每个按钮 Bind 一次事件,再用一个共享标志位判断谁先到。Verse 的 race 更直接,但它的臂数在写代码时就定死了——到底几个,得等游戏跑起来才知道。本页整理社区实战出的标准解法:spawn 哨兵 + event 聚合,顺便讲清一个会把编辑器搞崩的语法陷阱。
一、问题:race 不吃运行时数组
正课里的 race,每条臂都是你亲手连在图里的:两条臂、三条臂,还没运行就数得一清二楚。可假设你的关卡里摆着一排抢答按钮,数量由关卡设计师随意增减(那个数组变量点亮了 Instance Editable 小眼睛,能在 Details 里改)——「等这一排按钮里最先被按下的那个」,到底几条臂,得游戏跑起来才揭晓,race 的写法立刻卡壳。
论坛帖《How to (race) await an array of tasks?》里,楼主试了一个看起来非常合理的写法:race(T : Tasks){ T.Await() }——想像 ForEach 那样,把数组里每个任务都自动摊成 race 的一条臂。结果更刺激:编译器负责检查写法的前半段居然放行了,可真正把它变成可运行逻辑的后半段压根没实现,于是直接把编辑器搞崩(24.30 版实测崩溃)。Epic 引擎工程师 Andrew Scheidecker 在帖子里确认:这个语法「意图支持,但目前只做了前半段」。结论很干脆:这种写法眼下别碰,等官方实装。同一系列的雷还有一个:把一个范围写法(比如 0..10,表示 0 到 10 这一串数)当参数塞给 sync / race / rush,项目必崩——同样绕开为妙。
二、解法:每人一个哨兵,谁被按了谁报数
既然 race 的臂数定死,那就反过来想:不让主流程去 race N 个任务,而是给每个按钮派一名 spawn 哨兵。哨兵的工作只有两步:Await(等)自己盯的那个按钮;按钮一被按下,就把自己的编号 Signal 到一个共享的「事件分发器」event(int) 上——Signal 相当于 Call 一次这个分发器、还捎带一个整数(event 及其 Signal / Await 来自 /Verse.org/Concurrency)。主流程完全不关心有几个哨兵——它只要 Await(等)这个分发器,就像 Bind 好事件之后等它响一样,第一声 Signal 一到,它就醒了,手里还攥着「是几号按钮」。
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }
which_button_device := class(creative_device):
@editable
Buttons:[]button_device = array{}
FirstPressed:event(int) = event(int){}
WatchButton(Index:int, Button:button_device)<suspends>:void =
Button.InteractedWithEvent.Await()
FirstPressed.Signal(Index)
AwaitAnyButton()<suspends>:int =
for (Index -> Button : Buttons):
spawn{ WatchButton(Index, Button) }
FirstPressed.Await()
OnBegin<override>()<suspends>:void =
Winner := AwaitAnyButton()
Print("最先被按下的是 {Winner} 号按钮")
读图:Buttons 是一个按钮数组变量,点亮了 Instance Editable(@editable),关卡里摆几个就往里拖几个。FirstPressed 是一个「带整数负载的事件分发器」(event(int))——谁 Signal 它,就等于朝它喊一嗓子并附带一个编号。WatchButton 是每个按钮的哨兵函数:先 Await(等)自己盯的那个按钮,一旦被按,就把自己的编号 Signal(喊)给 FirstPressed。AwaitAnyButton 用一个 ForEach 给每个按钮 spawn 一名哨兵,然后自己只 Await 一次 FirstPressed——第一个哨兵一喊,它就醒,手里攥着编号。OnBegin(Event BeginPlay)调用它,再 Print String 打出结果。
逐段拆一下。for (Index -> Button : Buttons) 是个 ForEach Loop,一圈同时拿到下标和按钮本体,给每个按钮 spawn 一名哨兵——注意 spawn 体里只有一个函数调用,完全合规。FirstPressed.Await() 是 AwaitAnyButton 里的最后一个节点,它等来的编号(哨兵 Signal 出来的 Index)会直接接到这个函数的输出引脚上——这就是 Verse 的「隐式返回」:最后一个节点的输出即返回值,不用专门连一个 Return。别忘了在 UEFN 的 Details 面板里把关卡中的按钮拖进 Buttons 数组(那只 Instance Editable 小眼睛,下一课细讲),否则数组是空的,哨兵一个也派不出去。
三、模式的边角与改进
有两处边角值得留意。第一,哨兵是 spawn 出来的——按正课的说法,它们是「放出去的野马」,不会随那段图结束被取消:主流程拿到第一名之后,其余哨兵还各自卡在 Await 上等着。多数场景无伤大雅(之后再有按钮被按,Signal 一声也没人接);但若想干净收场,可以给哨兵内部加一场 race:一条线等按钮,另一条线等一个「收工事件」,主流程拿到结果后朝这个事件 Signal 一声——这正是拔高页「取消与 defer」里讲的自制取消模式。
第二,同样的思路稍作变形,就能做出「动态 sync」:想等全部任务完成,就让每个哨兵干完后把一个计数器变量加一,加满时朝一个「完工事件」Signal 一声,主流程照样只 Await 一次。event 就像一座广播塔(其实就是事件分发器):任务数量随便变,主流程永远只用听一个频道。这也是 Verse 社区处理「图连好时数目定死、真实数目却要跑起来才知道」这对矛盾的通用套路。
想等「任意数量按钮中最先被按下的那一个」,当前版本的正确做法是?
本文整理自 Epic 开发者论坛:How to (race) await an array of tasks? ↗(含 Epic 工程师对 race 数组语法实现状态的确认)。