技巧 · EXTRA
@editable 与 Details 面板:让美术也能改你的组件
能暴露哪些类型、滑块和数值范围怎么限、分组和悬浮提示怎么写,以及默认值与关卡覆盖值的关系。
进入拓展 →上一课的结论只有一句:entity 是空容器,component 是往里装的能力。这一课不再谈世界观,直接动手——把一个 component 的骨架逐段拆开看:类型怎么声明、字段怎么写、怎么用 @editable 把属性送进 Details 面板、生命周期回调分别在什么时候被叫醒。写完之后,你会发现「Add Component」这个动作在两边完全是同一件事。
先把上一课的结论压缩成一句话带走:entity 是一个什么都不会的容器,它能做什么,取决于你往里装了哪些 component。一个能被看见的方块,是「entity + mesh_component」;一个会发光的方块,是「entity + mesh_component + light_component」。没有继承树,没有「XxxActor 的子类」,只有拼装。
那问题就变成了:能力从哪来?内置的那些(网格、灯光、碰撞、粒子)Epic 已经写好了,直接挑就行。而当你要的行为清单里没有——比如「这块平台每隔两秒升上去、再降下来」——你就得自己写一个 component。
好消息是,写一个 component 就是写一个 Verse 类。第 20 课学的 class 语法在这里一字不改地复用,只有三处是 Scene Graph 特有的:继承谁、挂什么说明符、覆写哪几个生命周期函数。下面这段就是一个能跑的最小骨架,先整体看一眼,下一节逐行拆:
using { /Verse.org }
using { /Verse.org/Native }
using { /Verse.org/SceneGraph }
using { /Verse.org/Simulation }
using { /Verse.org/SpatialMath }
hello_component<public> := class<final_super>(component):
@editable
Greeting<public>:string = "hello"
OnBeginSimulation<override>():void =
(super:)OnBeginSimulation()
Print("{Greeting}")
十来行,一个自定义组件就成型了。把它跟你熟悉的东西对上:这相当于在蓝图里新建一个父类为 Actor Component 的蓝图,在变量面板里加一个勾了 Instance Editable 的 String 变量 Greeting,再在它的 Event BeginPlay 上接一个 Print String。区别只是:那边是三张面板加连线,这边是一个文本文件。
▢ 第一段:using。和第 7 课讲的一样,using 是「把某个抽屉拉开」。Scene Graph 这套 API 住在 /Verse.org/SceneGraph 里;Sleep 这类和时间有关的东西住在 /Verse.org/Simulation;vector3、transform 住在 /Verse.org/SpatialMath。编辑器用模板给你新建组件文件时,这几行是自动写好的,你只要知道少了哪一行会导致哪一批名字「不认识」。
▢ 第二段:类型声明。这一行是整节课信息密度最高的地方:
hello_component<public> := class<final_super>(component):
# ▲ ▲ ▲ ▲
# 名字 谁能用它 Scene Graph 继承自组件基类
# 的硬性要求
hello_component:类名,snake_case。这个名字会原样出现在 UEFN 的 Add Component 下拉列表里。<public>:可见性说明符,让这个类在整个项目里都能被引用(第 22 课讲过这一族说明符)。class<final_super>:<final_super> 是 Scene Graph 的硬性要求——官方文档的原话是,它用来保证 entity/component 这套体系的完整性。写自定义组件时照抄即可,不写会编译不过。(component):继承自组件基类 component。等于蓝图里新建蓝图时,父类那一栏选了 Actor Component。▢ 第三段:字段。就是普通的 Verse 字段,写法和第 8 课的常量/变量一模一样。区别只在于头上多一行 @editable——加了它,这个字段就会出现在 Details 面板里;不加,它就是纯代码内部的数据。这一节先记住结论,下一节展开。
▢ 第四段:生命周期回调。这是自定义组件唯一「新」的东西。蓝图里的 Component 只有 Event BeginPlay / Event EndPlay 那么两三个入口,Verse 把它拆得更细:一个 component 从被创建到被销毁,要依次经过若干个阶段,每个阶段都有一个可以覆写的回调。官方文档列出的阶段是:Initialized(创建完成)→ AddedToScene(进入场景)→ BeginSimulation(开始模拟)→ EndSimulation(结束模拟)→ RemovingFromScene(离开场景)→ Uninitializing(销毁)。对应到 Verse API 里最常覆写的是这几个:
| 回调 | 什么时候被叫醒 | 拿它干什么 |
|---|---|---|
OnAddedToScene() |
组件进入场景之后 | 从这一刻起,对其它组件的查询才是有效的 |
OnBeginSimulation() |
组件开始模拟时,立即跑完 | 一次性的准备工作:找一次别的组件、订阅事件、缓存引用 |
OnSimulate()<suspends> |
紧跟在 OnBeginSimulation 之后 | 需要「等」的异步逻辑:循环、Sleep、长期运行的行为 |
OnEndSimulation() |
模拟结束时 | 收尾:取消订阅、清掉缓存的回调 |
OnRemovingFromScene() |
组件即将离开场景前 | 只有当 OnAddedToScene 跑过,它才会被调用 |
两条顺序保证值得记牢:OnBeginSimulation 一定跑在 OnSimulate 前面;OnSimulate 会在 OnEndSimulation 之前被取消。所以「准备」写在前者、「表演」写在后者,是官方推荐的分工。
还有一条容易踩的规矩:覆写时要用 <override> 说明符,并且在函数体第一行把父类的同名实现叫一遍——写作 (super:)OnBeginSimulation()。这就像蓝图里覆写事件时那个自动生成的 Parent 节点:你可以不接,但多数时候接上才是对的。
▢ 附赠:Entity。每个 component 身上都自带一个常量 Entity,指向装着它的那个 entity。等于蓝图里 Component 图表里那个随手可得的 GetOwner。想改自己所在物体的位置、想找同一个物体上的别的组件,都从它出发。
做过蓝图的人对这个需求太熟了:一个「升降平台」组件,策划想改升多高、隔多久升一次,总不能每改一个数就找程序重新编译一遍。蓝图的解法是在变量面板给变量勾上 Instance Editable(那个小眼睛),变量就出现在 Details 面板里,每个摆进关卡的实例都能填不同的值。
Verse 的解法是在字段上面单独起一行,写 @editable:
editable_demo<public> := class<final_super>(component):
# 会出现在 Details 面板里,每个实例可以填不同的值
@editable
Height<public>:float = 200.0
@editable
Loops<public>:logic = true
# 没有 @editable:纯内部数据,面板里看不到
InternalTag:string = "lift"
三条规矩:第一,@editable 是属性(attribute),以 @ 开头、写在字段上一行——别和 <public>、<final_super> 那种写在尖括号里的说明符(specifier)搞混,那是两套东西。第二,可编辑的字段必须有默认值,代码里写的那个值就是面板里显示的初始值。第三,在关卡里改动某个实例的值,只影响那一个实例——官方文档的原话是,改动只对该实例生效,别的实例互不干扰,而且不需要重新编译。这正是 Instance Editable 的语义。
顺便把另一个蓝图习惯对上:蓝图变量还有个 Expose on Spawn,让 Spawn Actor 节点上多出一个可以连线的输入引脚。Verse 这边没有一个同名开关——你要么在 Details 面板里填好,要么在代码里构造实例时直接给字段赋值(my_component{ Height := 300.0 }),后者就是「生成时传参」这件事的代码形态。
能标 @editable 的类型有一份官方清单,还有 @editable_slider、@editable_number 这类带范围限制的变体,以及给面板加分组和悬浮提示的写法——这些整理在本课的技巧拓展页里。
类写好了,怎么让它真的生效?答案会让你觉得亲切得过分:在 Details 面板里点 Add Component,从列表里挑它。
官方给的流程是这样的:选中场景里的一个 entity,在它的 Details 面板点 Add Component > New Verse Component,在 Verse 代码模板里选 Scene Graph Component,填一个组件名(比如 my_verse_component),点 Create。UEFN 会替你生成好那个 .verse 文件——就是上面那副骨架——同时把这个组件挂到当前 entity 上。之后你在 VS Code 里改代码、编译,行为就更新了。
组件一旦编译通过,它就永久地出现在 Add Component 的列表里:场景里任何一个 entity 都能挑它,和挑 mesh_component、light_component 没有任何区别。你自己写的能力和 Epic 内置的能力,在面板上是平权的。
| 你在蓝图里的动作 | 在 Scene Graph 里的动作 |
|---|---|
| 打开一个 Actor 蓝图,在 Components 面板点 +Add | 选中 entity,在 Details 面板点 Add Component |
| 从列表里挑 Static Mesh Component | 从列表里挑 mesh_component |
| 挑自己写的那个 Actor Component 子类 | 挑自己写的那个 Verse component |
| 选中这个 Component,在右边 Details 里填参数 | 选中这个 component,在右边 Details 里填 @editable 字段 |
还有一条代码路径:entity 上有 AddComponents(),可以在运行时把组件实例装上去。这对应蓝图里的 Add Component by Class 节点——同样是「跑起来之后再长出一个能力」。不过日常开发里,绝大多数组件是在编辑器里装好的,代码装配留给真正动态的场景。
最后提醒一条容易被忽略的规矩:一个 entity 上,同一个组件类(以及它的子类)只能有一个。想在一个物体上放两个网格?做不到——正确做法是给它加一个子 entity,把第二个网格装在子 entity 上。这和蓝图「一个 Actor 可以挂五个 Static Mesh Component」的习惯是明显不同的,第一次撞上时会愣一下。
把前四节拼起来,做一个能直接用的组件:一块平台,升到指定高度、停一会儿、再降回原位,如此往复。它只需要两个 @editable 参数(升多高、停多久)和两个生命周期回调。点「运行下一步」,看这个组件从被装上到开始表演的完整过程。
using { /Verse.org }
using { /Verse.org/Native }
using { /Verse.org/SceneGraph }
using { /Verse.org/Simulation }
using { /Verse.org/SpatialMath }
lift_platform_component<public> := class<final_super>(component):
@editable
Height<public>:float = 200.0
@editable
WaitTime<public>:float = 2.0
OnBeginSimulation<override>():void =
(super:)OnBeginSimulation()
Print("升降平台已就位")
OnSimulate<override>()<suspends>:void =
loop:
Entity.SetLocalTransform(transform{Translation := vector3{Up := Height}})
Sleep(WaitTime)
Entity.SetLocalTransform(transform{Translation := vector3{Up := 0.0}})
Sleep(WaitTime)
点「运行下一步」,看代码怎么一行行执行。
读图翻译:整个文件里没有一行「注册到引擎」「订阅 Tick」的样板代码。你只是覆写了两个函数,剩下的由 Scene Graph 负责——它知道什么时候该叫你,就像蓝图知道什么时候该触发你的 Event BeginPlay 一样。
把这个组件装到一个带 mesh_component 的 entity 上,你就得到了一块会动的平台;把同一个组件装到十个不同的 entity 上,并在各自的 Details 面板里填不同的 Height,你就得到了十块高度各异的平台——一份代码,十种行为。这正是「组合优于继承」在日常里的样子。
一条实践提醒:官方文档指出,组件逻辑在编辑模式和游玩模式下都会运行,也就是说你加的行为在启动会话时会立刻开始跑。这和蓝图 Construction Script 与 BeginPlay 的分工不太一样,第一次看到平台在编辑器里就自己动起来时,不要以为是 bug。
这一课的每个动作,在蓝图里都有一个你天天在做的对应物。差异那一列才是重点。
| 蓝图里的做法 | Verse 里的写法 | 差异 |
|---|---|---|
| 新建蓝图,父类选 Actor Component | my_component := class<final_super>(component): |
Verse 组件是一个 .verse 文本文件,不是二进制资产;可以 diff、可以 code review |
| Components 面板点 +Add,挑一个组件 | Details 面板点 Add Component,挑一个组件 | 几乎完全一致。差异是 Scene Graph 规定同一个组件类在一个 entity 上只能有一个 |
| 变量面板新建变量,勾 Instance Editable | 字段上一行写 @editable |
语义相同:默认值写在代码里,每个实例可在 Details 面板单独覆盖 |
| 变量勾 Expose on Spawn | 构造时直接给字段赋值:my_component{ Height := 300.0 } |
Verse 没有同名开关;「生成时传参」变成了构造表达式的一部分 |
| Component 的 Event BeginPlay | OnBeginSimulation<override>() |
Verse 把「进场景」和「开始模拟」拆成了两个回调,查询别的组件要等到进场景之后 |
| Event Tick + Timeline 做持续动作 | OnSimulate<override>()<suspends> 里 loop + Sleep |
不是每帧回调,而是一条能「等」的协程;想要真正的逐帧,组件另有 TickEvents 回调 |
Component 图表里的 GetOwner |
组件自带的 Entity 常量 |
拿到的是 entity 而不是 Actor:它没有类型,只有一堆组件 |
GetComponentByClass |
Entity.GetComponent[mesh_component] |
Verse 版是可能失败的表达式,必须写在 if 这样的失败上下文里——不存在「返回 null 然后 Accessed None」 |
| Add Component by Class 节点 | Entity.AddComponents(...) |
都是运行时装能力;日常开发里两边都更常在编辑器里装好 |
为什么会有这些差异?根子在第 26 课那个分岔:蓝图的 Actor 是一棵继承树,组件是挂在树上的配件;Scene Graph 的 entity 干脆没有树,它就是一只筐,所有语义都从筐里的组件来。既然一切都靠组合,引擎就必须把「组件什么时候可用」讲得非常清楚——于是生命周期被拆成六段;也必须把「找不到组件」当成正常情况处理——于是 GetComponent 是可能失败的。
另一层差异来自语言本身:蓝图的可视化编辑器天然承担了「哪些参数可以调」的表达,而在文本里,这件事必须由 @editable 这样的显式标注来完成。你会发现 Verse 的很多「多余的字」,换来的都是「在文本里也能一眼看懂」。
最后重复一条本站的事实纪律:Actor 与 Blueprint 在 UE6 Early Access(目标 2027 年底)及早期版本中完整支持;弃用要等 Scene Graph 足够成熟,时间未定。学 component,不是因为蓝图明天就没了,而是因为它是同一件事的另一种写法——而这种写法,是新框架的原生语言。
组件写完了,来验收三道。答错零惩罚,可以一直重试。
声明一个自定义 Scene Graph 组件时,类声明里哪个说明符是 Scene Graph 的硬性要求?
你想在组件里跑一段「升上去、等两秒、降下来、再等两秒」的循环,应该写在哪个回调里?
第 23 课讲过),哪个回调带着它?">
你想让策划在关卡里给每块平台填不同的升起高度,该怎么做?
技巧 · EXTRA
能暴露哪些类型、滑块和数值范围怎么限、分组和悬浮提示怎么写,以及默认值与关卡覆盖值的关系。
进入拓展 →拓展 · EXTRA
你没写过一行代码,mesh_component 是从哪冒出来的?asset-generated component 的来历、属性与限制。
进入拓展 →拔高 · EXTRA
GetComponent 与 GetComponents、官方的「找一次就缓存」纪律,以及组合式架构里查找为什么比调用更危险。
进入拓展 →