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

接口驱动设计:官方社区教程走读

正课学会了 interface 的语法,这篇回答更难的问题:什么时候该抽接口?我们走读 Epic 社区教程「Implementing an Interface in Verse Code」的设计思路,对照官方文档给出的三大价值,最后看看引擎 API 里那些你天天在用、却没意识到的接口。

一、教程在做什么:一份合同,多种履约

Epic 社区教程的套路可以浓缩成三步:定义契约 → 各自实现 → 统一持有。先用 interface 写下「签约者必须会做什么」;再让若干个彼此无关的类各自 <override> 履约;最后调用方用接口类型统一持有它们,根本不关心背后是谁。用一个「可受击物」的例子复刻这个骨架:

Epic 社区教程的套路可以浓缩成三步:定义合同 → 各自实现 → 统一持有。先用 interface(就是蓝图接口 BPI)写下「签约的人必须会做什么」;再让若干个彼此无关的蓝图各自用 <override> 把合同实现掉;最后用接口类型的一根引用统一拿着它们,根本不问背后是谁。用一个「可受击物」的例子复刻这个骨架:

damageable.verse
using { /UnrealEngine.com/Temporary/Diagnostics }

# 第一步:契约——凡可受击物,必须会「吃伤害」
damageable := interface:
    TakeHit(Amount:int):void

# 第二步:各自履约——木人桩和油桶毫无血缘关系
training_dummy := class(damageable):
    TakeHit<override>(Amount:int):void =
        Print("木人桩稳稳吃下 {Amount} 点伤害")

exploding_barrel := class(damageable):
    TakeHit<override>(Amount:int):void =
        Print("油桶被击中,轰然爆炸!")

# 第三步:统一持有——调用方只认合同,不认具体类型
HitAll(Targets:[]damageable, Amount:int):void =
    for (T : Targets):
        T.TakeHit(Amount)

读图翻译:三段对应三步:先建接口 damageable(合同:会 TakeHit 挨伤害);再让两个毫无血缘的蓝图 training_dummy(木人桩)和 exploding_barrel(油桶)各自签约、把 TakeHit<override> 实现成自己的反应(一个稳稳吃下、一个当场爆炸);最后 HitAll 收一个 []damageable 数组,用 ForEach 挨个喊 TakeHit——它只认「你会挨打」这份合同,不管你具体是谁。

留意 HitAll 的参数类型:[]damageable。木人桩、油桶,以后再加的靶子、稻草人、镜像分身……只要签了约,就能进这个数组、被这个函数处理。新增一种受击物,HitAll 一个字都不用改——这就是接口驱动设计的核心收益:把「会变的部分」(各类的实现)和「不变的部分」(调用逻辑)干净地切开。

留意 HitAll 的参数类型:[]damageable——数组每一格的类型写的是接口。木人桩、油桶,以后再加的靶子、稻草人、镜像分身……只要签了这份约,就能进这个数组、被这个函数处理。新增一种受击物,HitAll 一个字都不用改——这就是接口驱动设计的核心收益:把「会变的部分」(各蓝图各自的实现)和「不变的部分」(调用逻辑)干净地切开。

二、官方文档划的重点:接口的三大价值

官方「Interface in Verse」文档为接口总结了三条价值,值得逐条翻译成人话:

▢ 安全性。接口只暴露契约,不暴露实现:拿着 damageable 的调用方,只能调 TakeHit,碰不到油桶内部的引信状态。暴露面越小,别人能误用的方式就越少。

▢ 安全性。接口只亮出合同、不亮出内部做法:手里拿着一根 damageable 引用的人,只能调合同答应的 TakeHit,碰不到油桶内部的引信状态。暴露面越小,别人能误用的方式就越少。

▢ 多继承。Verse 的类只能继承一个基类,但可以同时实现任意多个接口:class(base, iface1, iface2)。一个 boss 可以既是 damageable 又是 pickup(打死掉落自己?为什么不),两份能力互不干扰地拼在同一个类上——这是继承树给不了的自由。

▢ 多继承。Verse 的一个类只能有一个父类,却能同时实现任意多个接口:class(base, iface1, iface2)——就是蓝图 Class Settings 里那个能勾一长串的 Implemented Interfaces。一个 boss 可以既是 damageable 又是 pickup(打死掉落自己?为什么不),两份能力互不干扰地拼在同一个蓝图上——这是单纯靠父类继承给不了的自由。

▢ 复用性。针对接口写的每一段逻辑(HitAll、掉落结算、UI 列表……)自动适用于所有现在和未来的实现类。写一次,吃到饱。

▢ 复用性。针对接口写的每一段逻辑(HitAll、掉落结算、UI 列表……)自动适用于所有现在和将来签了这份约的蓝图。写一次,吃到饱。

三、什么时候该抽接口:一张检查清单

接口不是越多越好——为只有一个实现的东西抽接口,是给代码穿雨衣洗澡。对照这张清单再动手:

一句话总结:行为的「共同点」用接口表达,数据和实现的「共同点」用基类表达。两者可以同时上阵——基类管血缘,接口管技能。

一句话总结:行为的「共同点」用接口表达,数据和实现的「共同点」用父类表达。两者可以同时上阵——父类管血缘,接口(BPI)管技能。

四、你天天在用的接口:官方 API 巡礼

接口驱动不是教程里的玩具,Epic 自己的 API 就是这么写的。翻开 Verse API Reference 能看到不少熟面孔:fort_character——角色能力(血量、传送、移动)以接口形式定义,所以不同形态的「角色」都能统一操作;listenable(payload)——你从第 11 课就开始写的 InteractedWithEvent.Subscribe(...),背后正是「可监听」这份带参数的契约;enableable——大量设备的 Enable / Disable 来自同一份「可启停」合同,所以你可以用一段代码统一开关一批不同种类的设备。

接口驱动不是教程里的玩具,Epic 自己的 API 就是这么写的。翻开 Verse API Reference 能看到不少熟面孔:fort_character——角色能力(血量、传送、移动)以接口形式定义,所以各种形态的「角色」都能被统一操作;listenable(payload)——你从第 11 课就在写的 InteractedWithEvent.Subscribe(...)(把设备事件绑到自己的处理上,像蓝图里 Bind Event),背后正是「可监听」这份带参数的合同;enableable——大量设备的 Enable / Disable 来自同一份「可启停」合同,所以你能用一段代码统一开关一批不同种类的设备。

下次调 API 时不妨多看一眼类型签名:当参数类型是个接口,官方是在告诉你「这里认合同不认人」——你自己的类,签了约同样能递进去。读懂了这一层,你看 API 文档的方式会从「查函数」升级成「读设计」。

下次调 API 时不妨多看一眼参数类型:当某个参数的类型是个接口,官方其实在告诉你「这里认合同不认人」——你自己写的蓝图,只要签了这份约,同样能递进去。读懂这一层,你看 API 文档的方式就会从「查函数」升级成「读设计」。

下面哪件事是 interface 做得到、而 <abstract> 抽象类做不到的?

来源与延伸

本文整理自 Epic 官方社区教程与官方文档:

社区教程:Implementing an Interface in Verse Code ↗

官方文档:Interface in Verse ↗