设备生命周期:OnBegin、OnEnd 与被吞掉的协程
正课里 OnBegin 只是「游戏开始时被调用的函数」。这一页把镜头拉远,看清一台设备的完整一生:从 OnBegin 醒来,到 OnEnd 谢幕,以及官方文档里那句耐人寻味的警告——「在 OnEnd 中 spawn 的协程可能永远不会执行」。
正课里 OnBegin 只是「游戏开始时自动触发的函数」——也就是设备的 Event BeginPlay。这一页把镜头拉远,看清一台设备的完整一生:从 OnBegin 醒来,到 OnEnd 谢幕,以及官方文档里那句耐人寻味的警告——「在 OnEnd 中 spawn 的协程可能永远不会执行」(spawn = 临时岔出一条新执行线;协程 = 一条能随时暂停再续、跟着会话存活的执行线)。
一、一生两端:OnBegin 开场,OnEnd 谢幕
creative_device 的生命周期由两个可重写的函数框定:
creative_device 的生命周期由两个可以重写(在蓝图的 Override 菜单里改写)的函数框定:
OnBegin<override>()<suspends>:void =—— 游戏体验开始时运行。官方 API 页的描述是:「Override 此函数以在游戏体验开始时加入自定义逻辑」;OnEnd<override>():void =—— 游戏体验结束时运行,适合做收尾:打一条结算日志、记录最终状态等。
把两个签名并排放,会发现一处刺眼的不对称:OnBegin 带 <suspends>,OnEnd 不带。这不是笔误,而是设计意图的直接表达——开场函数被设计成可以从容地等待、循环、贯穿整场游戏;谢幕函数则被设计成「说完就走」:游戏正在关灯锁门,没有时间留给任何异步等待。
把两行写法并排放,会发现一处刺眼的不对称:OnBegin 带 <suspends>(等待徽章),OnEnd 不带。这不是笔误,而是设计意图的直接表达——开场函数被设计成可以从容地等待(放 Delay)、循环、贯穿整场游戏;谢幕函数则被设计成「说完就走」:游戏正在关灯锁门,没有时间留给任何等待。
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 还提供了 spawn、sync、race、rush、branch 等并发表达式,用来在一条执行线里再分出多条——其中除 spawn 之外都是结构化的:子任务的生死被外层作用域管着,作用域退出,任务跟着收工。并发是后面章节的重头戏,这里先埋个种子。
这个模型是后面一切进阶玩法的地基:事件订阅(Bind Event)、计时器、游戏主循环,全都长在「OnBegin 是一条长寿执行线」这个认知之上。Verse 还提供了 spawn、sync、race、rush、branch 等并发写法,用来在一条执行线里再岔出好几条同时跑:spawn 是岔出一条新线、不等它;sync 是几条线一起跑、全到齐才继续;race 是几条线抢先、第一条到终点的算数、其余当场剪断。除 spawn 外它们都是结构化的——岔出去的线归外层管,外层一收尾,它们就跟着被清场(有点像把一组节点圈进一个受控的分支里,主线一结束、整组一起停)。并发是后面章节的重头戏,这里先埋个种子。
三、被吞掉的协程:OnEnd 的官方警告
现在可以读懂官方 OnEnd API 页上那句警告了:「在 OnEnd 中 spawn 的任何协程可能永远不会执行」。
现在可以读懂官方 OnEnd API 页上那句警告了:「在 OnEnd 中 spawn 的任何协程可能永远不会执行」(spawn 出去的那条新执行线,可能一次都轮不上跑)。
原因不神秘:OnEnd 运行时,游戏体验正在收尾——调度器忙着拆台,不再保证给新生的异步任务安排档期。你在 OnEnd 里 spawn 出一个协程,相当于在剧院熄灯散场时才递上节目单:台子都拆了,这个节目大概率永远排不上。它不会报错,只会无声地消失——比报错更难排查。
原因不神秘:OnEnd 运行时,游戏体验正在收尾——引擎的任务调度忙着拆台,不再保证给新冒出来的等待型任务安排档期。你在 OnEnd 里 spawn 出一条新执行线,相当于在剧院熄灯散场时才递上节目单:台子都拆了,这个节目大概率永远排不上。它不会报错,只会无声地消失——比报错更难排查。
实践守则:
- OnEnd 里只做同步的、立刻完成的事:打日志、记状态;
- OnEnd 里只做立刻就能做完、不用等的事:打日志、记状态;
- 不要在 OnEnd 里 spawn 协程,也别指望任何「等一下再做」的逻辑能在谢幕后兑现;
- 不要在 OnEnd 里 spawn(临时岔出新执行线),也别指望任何「等一下再做」的逻辑能在谢幕后兑现;
- 需要长期运行的逻辑,一律安置在 OnBegin 这条长寿协程里。
- 需要长期运行的逻辑,一律安置在 OnBegin 这条长寿执行线里。
官方文档为什么警告「不要指望在 OnEnd 里 spawn 的协程」?官方文档为什么警告「不要指望在 OnEnd 里 spawn(临时岔出的新执行线)」?
OnBegin 带 <suspends> 而 OnEnd 不带,这个设计传达了什么意图?
四、出处
本文整理自 Epic 官方文档: