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

自定义事件跨设备通信:论坛实战与 Dispatch 变体

第 21 课学会了 event(t) 的 Signal / Await;这一页深入论坛实战:声明报「抽象类」错误新建时报「抽象类」错误怎么破、Verse 事件为什么进不了原生设备的下拉框,以及当 Await 不够用时,社区怎么自制分发器。本文整理自 Epic 开发者论坛与官方社区教程。

一、声明的正确姿势:「类型 = 原型」缺一不可

论坛帖《How do I add custom events in verse creative devices?》里,楼主的第一次尝试是只写类型标注——直觉上很合理,却直接吃了编译错误:事件字段没有实例,编译器抱怨抽象类/抽象函数无法使用。正确写法必须让「类型标注」和「原型实例」成对出现:

论坛帖《How do I add custom events in verse creative devices?》里,楼主的第一次尝试是只写左半边(只声明它是个事件)——直觉上很合理,却直接 Compile 报红:这个事件没有初始值,引擎抱怨它是个抽象的、没法直接用的东西。正确写法必须让「左边的声明」和「右边的原型实例」成对出现:

custom_event_demo.verse
# 错误:只有类型,没有实例 → 编译器报抽象类相关错误
# BadEvent<public>:event()

# 正确:「类型 = 原型」成对出现
GoEvent<public>:event() = event(){}

# 要捎带数据就给 event 加载荷类型
ScoreEvent<public>:event(int) = event(int){}

加上 <public> 是为了让别的设备访问得到——通信的另一端往往在另一个类里。用法与第 21 课一致:一端 GoEvent.Signal(),另一端 GoEvent.Await();带载荷的版本 ScoreEvent.Signal(99),等待方 NewScore := ScoreEvent.Await() 直接收到数字。Signal / Await 来自 /Verse.org/Concurrency(不同版本模板的 using 路径可能有差异,以 Verse API Reference 为准)。

加上 <public> 是为了让别的设备也访问得到——通信的另一端往往在另一台设备(另一个蓝图)里。用法与第 21 课一致:一端 GoEvent.Signal()(Call 一下分发器),另一端 GoEvent.Await()(Bind 上去等它响);想捎带数据就用带类型的版本 ScoreEvent.Signal(99),等的那一端 NewScore := ScoreEvent.Await() 直接收到数字 99。Signal / Await 来自 /Verse.org/Concurrency(不同版本模板的 using 路径可能有差异,以 Verse API Reference 为准)。

二、一个硬限制:Verse 事件进不了原生设备的下拉框

帖子里澄清了一个高频误解:很多人希望自定义事件像原生设备事件那样,出现在 Storm Controller、Trigger 这类原生创意设备的事件绑定下拉菜单里,用「设备直连」的方式消费它。答案是:不行。原生设备的下拉框只认原生事件;你在 Verse 里声明新建event(t),只能被其他 Verse 代码通过 Signal / Await / Subscribe 消费。

这条限制决定了架构方向:如果链路的两端都是 Verse 设备,自定义事件是最顺手的胶水;如果一端是原生设备,你就得反过来——让 Verse 去订阅原生设备的事件,或调用原生设备的函数,由 Verse 这一侧充当翻译官。

三、Dispatch 变体:当你想要「广播给一群设备」

Await 的消费模式是「谁等谁收到」,每个等待方都得自己挂起在事件上。帖中有用户展示了另一种思路:自制一个分发器(dispatcher)——内部维护一份回调(或监听者)清单,触发时逐个通知。官方社区教程《Use this to Subscribe to Events with Verse in UEFN》也演示了类似的订阅式封装。下面是思路的「监听设备数组」变体,只用第 21 课学过的语法:

Await 的模式是「谁等谁收到」,每个等待方都得自己停在事件上等。帖中有用户展示了另一种思路:自制一个分发器(dispatcher)——它其实很像蓝图里的事件分发器:内部存一份「监听者名单」,触发时挨个通知。官方社区教程《Use this to Subscribe to Events with Verse in UEFN》也演示了类似的订阅式封装。下面是这个思路的「监听设备数组」变体,只用第 21 课学过的写法:

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

# 被通知方:暴露一个 public 方法当「收件口」
siren_device := class(creative_device):

    OnNotified<public>():void =
        Print("收到广播,警笛拉响!")

# 分发器:维护监听者清单,广播时逐个点名
notifier_device := class(creative_device):

    # 在面板里把所有警笛设备拖进这个数组
    @editable
    Listeners:[]siren_device = array{}

    Broadcast<public>():void =
        for (L : Listeners):
            L.OnNotified()

读图翻译:上半段 siren_device 暴露了一个 public 的 OnNotified 函数图当「收件口」。下半段 notifier_device 里那个 @editableListeners 是一个「装 siren 设备的数组」——进关卡后把所有警笛设备都拖进这个数组框。Broadcast 里用一个 ForEach 循环(for (L : Listeners))把数组里的每台警笛挨个取出来,运行它的 OnNotified——等于一次广播,全部点名。

论坛原帖的版本更进一步,存的不是设备引用而是回调函数数组,Dispatch() 时逐个调用——行为更接近原生设备事件的 Subscribe。函数类型的书写细节这里不展开,建议直接读原帖源码。

论坛原帖的版本更进一步,数组里存的不是设备引用,而是一串「函数」,Dispatch() 时逐个运行——行为更接近原生设备事件的 Subscribe(Bind)。这种「把函数当数据存」的写法细节这里不展开,建议直接读原帖源码。

四、三种设备间通信方式,怎么选?

方式 耦合度 多接收方 适合场景
@editable 引用直调 <public> 方法@editable 拖进来,直接调它开放的(<public>)函数 高(甲认识乙) 手动拖数组 一对一、关系固定的设备对
共享 event(t) Signal / Await 低(只认事件) 所有 Await 方都被唤醒 一对多广播、跨模块解耦
自制 dispatcher 清单里逐个通知 需要订阅式回调、可控通知顺序

经验法则:先试事件,事件是 Verse 并发模型的原生公民,和 racespawn 无缝配合;只有当你明确需要「按清单顺序逐个通知」或「运行时增删监听者」时,才值得自己造 dispatcher。

GoEvent<public>:event() 只写这一行,为什么编译失败Compile 失败?

来源

本文整理自 Epic 开发者论坛与官方社区教程: