Verse Wiki — 写给蓝图作者的 Verse 手册
拓展 · EXTRA

把这台设备做成可复用的组件

主课把 BP_PressurePlate 翻成了一台 Verse 设备,能跑,但它一个人干了三份活。第 26、27 课讲过 Scene Graph 的组合模型:entity 是容器,能力由挂在上面的 component 提供。这一页把同一段逻辑按那套模型再拆一次——先看清三份活各是什么,再看拆开之后什么变好了、什么变贵了。

一、一台设备里其实塞了三件事

把主课那 43 行代码眯着眼看,它由三块互不相干的东西拼成:

三块之间几乎没有交集,却被硬塞进同一个类里。为什么?因为在继承模型下,一个能被放进关卡、能收到 Event BeginPlay 的东西就是一整块——蓝图里你也一样,BP_PressurePlate 是一个 Actor,它身上的碰撞盒、变量、事件图必须待在一起。想复用其中一块?蓝图作者的老办法是「新建一个父类,把公共部分提上去」,于是继承树越长越深,直到没人说得清 BP_PressurePlate_Timed_Locked_V2 是从哪儿继承来的。

组合模型换了个思路:entity 是一个空容器,能力靠往上挂 component 获得。同一块「定时开门」的能力,可以挂在压力板上,也可以挂在拉杆上、按钮上、甚至挂在一个纯粹由计时器驱动的空 entity 上——不需要任何继承关系。Scene Graph 就是这套模型,而且它是从零基于 Verse 构建的:组件本身就是 Verse 类,不存在「蓝图写一半、代码写一半」的接缝。

二、拆法对照:一台 device → 三份职责

照着上面那三块切,对照表长这样:

主课设备里的这一段 拆开之后归谁 拆开的好处
@editable Plate + Plate.InteractedWithEvent 感知件:只做一件事——发生了就喊一声 换触发源(按钮 → 拉杆 → 计时器)只换这一个件,下游一行不动
var IsOpen:logic 状态件:门自己的属性,谁想知道就问它 状态跟着门走,而不是跟着「触发它的那个机关」走——一扇门被两个机关控制时,不会出现两份账本
TargetDoor.Disable() / Enable() 与两个小函数 驱动件:只负责「把这扇门开上 / 关上」 极性陷阱被永久封在一个件里;全项目只有这一处知道 Disable 才是开门
loop + Sleep(OpenDuration) 那段编排 规则件:把「谁触发」和「驱动谁」接起来,并管时间 「踩一下开 5 秒」这条规则本身变成可复用的东西,换一扇门、换一个触发源都不用重写
@editable OpenDuration:float 跟着规则件走 秒数属于规则,不属于门,也不属于压力板——放对地方,面板上就不会到处都是重复的 Duration

注意第二行那个变化:IsOpen 从「压力板的变量」变成了「门的属性」。这不只是搬个位置——它把一个真实的设计问题摆到了台面上:如果同一扇门被两个机关控制,状态该由谁记?在主课那版里,两台压力板各记一份 IsOpen,迟早对不上账。拆开之后答案是唯一的:门自己记。组件化最大的收益往往不是复用,而是逼你把「这个数据到底属于谁」想清楚。

三、今天就能做的中间形态

Scene Graph 的组件基类名、生命周期函数与具体写法仍在演进,以官方文档为准;但切分思路今天就能练——用你已经会的东西:三台各司其职的 Verse 设备,靠自定义事件(第 25 课,也就是蓝图的 Event Dispatcher)串起来。

plate_sensor.verse(感知件)
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Concurrency }

# 只回答一件事:有人触发了。至于接下来干嘛,不关它的事。
plate_sensor := class(creative_device):

    @editable
    Plate:button_device = button_device{}

    # 自定义事件 = 蓝图的 Event Dispatcher(第 25 课)
    TriggeredEvent<public>:event(agent) = event(agent){}

    OnBegin<override>()<suspends>:void =
        Plate.InteractedWithEvent.Subscribe(OnPlateHit)

    OnPlateHit(Agent:agent):void =
        TriggeredEvent.Signal(Agent)
door_actuator.verse(驱动件 + 状态件)
using { /Fortnite.com/Devices }

# 只回答一件事:这扇门怎么开、怎么关、现在是什么状态。
door_actuator := class(creative_device):

    @editable
    TargetDoor:barrier_device = barrier_device{}

    # 状态跟着门走,不跟着触发它的机关走
    var IsOpen:logic = false

    Open<public>():void =
        set IsOpen = true
        TargetDoor.Disable()      # 极性陷阱只在这一处出现

    Close<public>():void =
        set IsOpen = false
        TargetDoor.Enable()
timed_open_rule.verse(规则件)
using { /Fortnite.com/Devices }
using { /Verse.org/Simulation }
using { /Verse.org/Concurrency }

# 只回答一件事:触发之后,开多久再关。
# @editable 也能引用你自己写的 Verse 设备(第 25 课)
timed_open_rule := class(creative_device):

    @editable
    Sensor:plate_sensor = plate_sensor{}

    @editable
    Actuator:door_actuator = door_actuator{}

    @editable
    OpenDuration:float = 5.0

    OnBegin<override>()<suspends>:void =
        loop:
            Sensor.TriggeredEvent.Await()
            Actuator.Open()
            Sleep(OpenDuration)
            Actuator.Close()

读图翻译:三台设备摆进关卡,在 Details 面板上把它们互相指一指就配好了。plate_sensor 听按钮、拉响自己的事件分发器;timed_open_rule 在自己的主循环里等那声响,然后调 door_actuatorOpen()、睡 OpenDuration 秒、再调 Close();door_actuator 从头到尾不知道有压力板这回事。

换个触发源试试:再写一台 lever_sensor,同样对外暴露一个 TriggeredEvent,规则件那边一行都不用改——只要在面板上换个指向。这就是组合模型许诺的东西,而你现在用今天的工具已经拿到了一半。

四、搬到 entity + component 上会变成什么

上面那份中间形态和真正的组件模型,差别主要在两处:

其一,归属关系变实了。三台设备现在是靠面板上的引用互相认识的,谁挂在谁身上并没有物理意义;在 entity + component 模型里,驱动件和状态件是挂在门这个 entity 上的,感知件是挂在压力板那个 entity 上的。「这个组件属于哪个东西」不再是一根可以随便乱指的线,而是结构本身。第 27 课讲写一个 component,26-x3 那页专门练「把一个 Actor 蓝图拆成 components」的手感。

其二,配置的粒度变细了。今天你要在关卡里摆三台设备、连三根引用;组件模型下,「一扇会定时开合的门」可以打包成一个 prefab,拖一次就带齐所有组件和默认参数。这也是 Epic 换骨架的动机之一:组合优于继承,不是因为它更时髦,而是因为继承树到了一定深度就没人敢改了。

关于时间线,事实照官方口径摆一次:Scene Graph 是 UE6 的新玩法框架,从零基于 Verse 构建;Actor 与 Blueprint 在 UE6 Early Access(目标 2027 年底)及早期版本中完整支持,弃用要等 Scene Graph 足够成熟,时间未定。所以这一页不是催你今天就重构,而是让你在下一次动手写新东西的时候,先问一句:这三份活,有必要塞在同一个类里吗?

五、拆分不是免费的

诚实地说完好处,也得说代价。三个件比一个类多出了这些东西:三份 @editable 要在面板上连、一条事件链要在脑子里跟、一次触发要多绕两跳才走完全程。对一扇门来说,这笔账不一定划算。

判断标准很朴素:看这三份活会不会各自被复用。整张地图只有一扇门、一个机关,主课那版 43 行就是正确答案,拆开纯属自找麻烦;地图里有十扇门、五种触发源、三条不同的开合规则,那么每多一种组合,单体写法就要多写一个类,而组件写法只是多连一根线。组合的收益随组合数增长,拆分的成本是固定的——这条曲线在哪儿交叉,只有你自己的项目知道。

六、小测验

拆分之后,IsOpen 这个状态为什么被挪到「门」那一侧,而不是留在压力板那一侧?