Verse Wiki — 写给 UE6 时代的 Verse 互动手册
拓展 · EXTRA

设备生命周期:OnBegin、OnEnd 与被吞掉的协程

正课里 OnBegin 只是「游戏开始时被调用的函数」。这一页把镜头拉远,看清一台设备的完整一生:从 OnBegin 醒来,到 OnEnd 谢幕,以及官方文档里那句耐人寻味的警告——「在 OnEnd 中 spawn 的协程可能永远不会执行」。

正课里 OnBegin 只是「游戏开始时自动触发的函数」——也就是设备的 Event BeginPlay。这一页把镜头拉远,看清一台设备的完整一生:从 OnBegin 醒来,到 OnEnd 谢幕,以及官方文档里那句耐人寻味的警告——「在 OnEnd 中 spawn 的协程可能永远不会执行」(spawn = 临时岔出一条新执行线;协程 = 一条能随时暂停再续、跟着会话存活的执行线)。

一、一生两端:OnBegin 开场,OnEnd 谢幕

creative_device 的生命周期由两个可重写的函数框定:

creative_device 的生命周期由两个可以重写(在蓝图的 Override 菜单里改写)的函数框定:

把两个签名并排放,会发现一处刺眼的不对称:OnBegin 带 <suspends>,OnEnd 不带。这不是笔误,而是设计意图的直接表达——开场函数被设计成可以从容地等待、循环、贯穿整场游戏;谢幕函数则被设计成「说完就走」:游戏正在关灯锁门,没有时间留给任何异步等待。

把两行写法并排放,会发现一处刺眼的不对称:OnBegin 带 <suspends>(等待徽章),OnEnd 不带。这不是笔误,而是设计意图的直接表达——开场函数被设计成可以从容地等待(放 Delay)、循环、贯穿整场游戏;谢幕函数则被设计成「说完就走」:游戏正在关灯锁门,没有时间留给任何等待。

lifecycle_device.verse
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /UnrealEngine.com/Temporary/Diagnostics }

# 观察设备的一生:开场、心跳、谢幕
lifecycle_device := class(creative_device):

    var Beats:int = 0

    OnBegin<override>()<suspends>:void =
        Print("OnBegin:体验开始,设备上岗。")
        loop:
            Sleep(5.0)
            set Beats += 1
            Print("心跳 #{Beats}:OnBegin 还活着。")
            if (Beats >= 3):
                break

    OnEnd<override>():void =
        Print("OnEnd:体验结束,设备谢幕。")

读图翻译:这张蓝图先在变量面板建一个整数变量 Beats = 0。Event BeginPlay 触发后,先 Print String 打「OnBegin:体验开始…」;接着进一个循环(像一个 While Loop):每圈先 Delay 5 秒,再用一个 Set 节点把 Beats 加 1,Print 出「心跳 #…」,然后一个 Branch 判断 Beats 是否 ≥ 3——走通就 Break 跳出循环。下面 OnEnd 那格是设备的收场事件,只 Print 一句「OnEnd:体验结束…」就完事。

示例里的 loop: 每 5 秒打一次「心跳」,跳满 3 次用 break 退出。如果去掉计数与 break,这个循环就会伴随整场会话一直跳下去——完全合法,而且是写周期逻辑(刷怪、倒计时、定时检查)的常见形态。能这么写的前提,正是 OnBegin 的 <suspends>

示例里的循环每 5 秒打一次「心跳」,跳满 3 次用 break 跳出。如果去掉计数与 break,这个循环就会伴随整场会话一直跳下去——完全合法,而且是写周期逻辑(刷怪、倒计时、定时检查)的常见形态。能这么写的前提,正是 OnBegin 的 <suspends>(等待徽章)。

二、心智模型:OnBegin 是一条跟随会话存活的协程

拆掉「函数被调用一下就结束」的旧观念,换上这个心智模型:设备的 OnBegin 是一条协程(coroutine)——一条可以随时挂起、随时恢复、跟随会话存活的执行线

拆掉「函数触发一下、跑完就结束」的旧观念,换上这个心智模型:设备的 OnBegin 是一条协程(coroutine)——一条可以随时暂停、随时从暂停点接着跑、跟随会话一直存活的执行线(想象一根永不断电、随时能按暂停/继续的蓝图执行线)。

它 Sleep 时不是「卡死」,而是把舞台让出来,时间一到再从暂停点原地续播;它可以用 loop 无限循环,因为它本来就被允许活到会话结束。你在正课 stepper 里看到的「Sleep 期间日志一动不动」,微观上就是这条协程被挂起的样子。

它 Sleep 时不是「卡死」,而是把舞台让出来,时间一到再从暂停点原地续播——完全是 Delay 节点的手感;它可以无限循环下去(像一个条件永远为真的 While Loop),因为它本来就被允许活到会话结束。你在正课 stepper 里看到的「Sleep 期间日志一动不动」,微观上就是这条执行线被按下暂停的样子。

这个模型是后面一切进阶玩法的地基:事件订阅、计时器、游戏主循环,全都长在「OnBegin 是长寿协程」这个认知之上。Verse 还提供了 spawnsyncracerushbranch 等并发表达式,用来在一条执行线里再分出多条——其中除 spawn 之外都是结构化的:子任务的生死被外层作用域管着,作用域退出,任务跟着收工。并发是后面章节的重头戏,这里先埋个种子。

这个模型是后面一切进阶玩法的地基:事件订阅(Bind Event)、计时器、游戏主循环,全都长在「OnBegin 是一条长寿执行线」这个认知之上。Verse 还提供了 spawnsyncracerushbranch并发写法,用来在一条执行线里再岔出好几条同时跑:spawn 是岔出一条新线、不等它;sync 是几条线一起跑、全到齐才继续;race 是几条线抢先、第一条到终点的算数、其余当场剪断。除 spawn 外它们都是结构化的——岔出去的线归外层管,外层一收尾,它们就跟着被清场(有点像把一组节点圈进一个受控的分支里,主线一结束、整组一起停)。并发是后面章节的重头戏,这里先埋个种子。

三、被吞掉的协程:OnEnd 的官方警告

现在可以读懂官方 OnEnd API 页上那句警告了:「在 OnEnd 中 spawn 的任何协程可能永远不会执行」

现在可以读懂官方 OnEnd API 页上那句警告了:「在 OnEnd 中 spawn 的任何协程可能永远不会执行」(spawn 出去的那条新执行线,可能一次都轮不上跑)。

原因不神秘:OnEnd 运行时,游戏体验正在收尾——调度器忙着拆台,不再保证给新生的异步任务安排档期。你在 OnEnd 里 spawn 出一个协程,相当于在剧院熄灯散场时才递上节目单:台子都拆了,这个节目大概率永远排不上。它不会报错,只会无声地消失——比报错更难排查。

原因不神秘:OnEnd 运行时,游戏体验正在收尾——引擎的任务调度忙着拆台,不再保证给新冒出来的等待型任务安排档期。你在 OnEnd 里 spawn 出一条新执行线,相当于在剧院熄灯散场时才递上节目单:台子都拆了,这个节目大概率永远排不上。它不会报错,只会无声地消失——比报错更难排查。

实践守则:

官方文档为什么警告「不要指望在 OnEnd 里 spawn 的协程」?官方文档为什么警告「不要指望在 OnEnd 里 spawn(临时岔出的新执行线)」?

OnBegin 带 <suspends> 而 OnEnd 不带,这个设计传达了什么意图?

四、出处

本文整理自 Epic 官方文档: