Verse Wiki — 写给蓝图作者的 Verse 手册
技巧 · EXTRA

订阅的生命周期:什么时候订、什么时候退订

蓝图里 Bind Event 之后你很少想起 Unbind,因为 Actor 一销毁,绑定跟着没了。Verse 的 Subscribe 会交回一张 cancelable 凭证,这张凭证该收在哪、什么时候必须 Cancel、为什么重复订阅会让一次按下触发 N 次——这一页把订阅的整条生命周期讲透。

一、一次订阅,是一根一直挂着的线

先把心智模型摆正。Subscribe 不是「调用一次函数」,而是在事件那边留下一条记录:「以后你响的时候,顺便叫一下我这张图」。这条记录会一直挂着,直到两件事之一发生——你主动 Cancel(),或者持有它的那个对象整体退场。

蓝图作者对这件事往往没什么感觉,因为在蓝图里绑定的生命周期通常被 Actor 的生命周期兜住了:Actor 一 Destroy,它身上的绑定也就没了意义。Verse 里绝大多数场景同样如此——一台设备在整局游戏里活着,它在 OnBegin 里订阅一次,绑到游戏结束,完全不需要退订。

所以第一条结论就很朴素:大多数订阅根本不需要退订。真正需要你操心的,是下面三种「订阅活得比它该活的时间长」的情况。

二、订阅写在哪:OnBegin,而且只写一次

蓝图里你不会把 Bind Event 放进 Tick,因为一眼就能看出这是在反复绑。Verse 里同样的错误更隐蔽——它可能藏在一个「每次开新回合都会跑一遍」的函数里:

double_subscribe.verse
# ✗ 反例:每开一局订阅一次,handler 会叠加
StartRound()<suspends>:void =
    Bell.InteractedWithEvent.Subscribe(OnRing)
    Sleep(60.0)

# 第一局:按一次响一次
# 第二局:按一次响两次
# 第三局:按一次响三次……

这和蓝图里对同一个 Event Dispatcher 反复 Bind 的后果一模一样:处理函数会叠加,而不是覆盖。症状也一样容易误判——你会以为是「事件触发了多次」,其实是同一次触发调了 N 张图。

两条修法。要么把订阅提到只跑一次的地方(OnBegin),回合逻辑只读状态不重绑;要么每次重绑前先退订:

resubscribe.verse
var RingSub:?cancelable = false

StartRound()<suspends>:void =
    # 先退掉上一局的那根线(如果有)
    if (Old := RingSub?):
        Old.Cancel()
    set RingSub = option{ Bell.InteractedWithEvent.Subscribe(OnRing) }
    Sleep(60.0)

这里的 ?cancelable第 18 课的 option:「可能有、也可能还没有」的那种字段。if (Old := RingSub?) 就是「如果这里面装着东西,取出来叫 Old」——取不出来(第一局)就跳过这一段,不会炸。这正是 Verse 用来替代蓝图 Is Valid 判断的写法。

三、cancelable:凭证收在哪

官方文档写得很明确:对设备事件调用 Subscribe() 会返回一个 cancelable,对它调用 Cancel() 即可退订,之后该处理函数不再被调用。绑得多了,把凭证攒进一个数组是最省事的做法——这相当于给自己造一个蓝图里现成的 Unbind All Events:

subscription_bag.verse
using { /Fortnite.com/Devices }
using { /UnrealEngine.com/Temporary/Diagnostics }

console_device := class(creative_device):

    @editable
    Buttons:[]button_device = array{}

    # 把所有凭证收在一个「口袋」里
    var Subs:[]cancelable = array{}

    OnBegin<override>()<suspends>:void =
        for (B : Buttons):
            set Subs += array{ B.InteractedWithEvent.Subscribe(OnAny) }

    # 一键清场:相当于蓝图的 Unbind All Events
    UnsubscribeAll():void =
        for (S : Subs):
            S.Cancel()
        set Subs = array{}

    OnAny(Agent:agent):void =
        Print("有按钮被按下")

读图翻译:Buttons 是一个按钮引用的数组插槽(在 Details 面板里一次拖进一批);OnBegin 用一个 ForEach 给每个按钮订阅同一张处理图,并把每张凭证追加进 Subs;UnsubscribeAll 再用一个 ForEach 挨个 Cancel(),最后把口袋清空。

注意最后那句 set Subs = array{}:凭证已经作废了,继续留在数组里只会让你下次误以为还绑着。清空它,和蓝图里 Unbind 完顺手把引用变量置空是同一个卫生习惯。

四、什么时候必须退订

场景 要不要退订 为什么
设备在 OnBegin 订阅一次,活到整局结束 不用 订阅和对象同生共死,和蓝图里 BeginPlay 绑一次是同一回事
「只响一次就作废」的一次性逻辑(教程提示、开场引导) 否则玩家第二次触发时又弹一遍。也可以干脆改用 Await(),天然只等一次
每回合重新配置一次的订阅 不退订就会叠加,一次触发跑 N 张图 —— 本页第二节那个反例
处理函数会去碰某个「可能已经不在了」的对象 对象没了、线还挂着,触发时就是运行时错误。这正是蓝图里 Accessed None 的同款事故

有一条经验法则可以少记很多规则:「只想听一次」就用 Await(),「要一直听」才用 Subscribe()Await 等到一次就往下走,不留任何记录,压根不存在退订这回事——蓝图里没有这种选项,所以这是一个值得主动养成的新习惯。

一台设备在每回合开始的函数里 Subscribe 同一个按钮事件,三回合之后,玩家按一次按钮会发生什么?

来源

本文整理自 Epic 官方文档: