做自定义编辑器工具时,最常遇到的一个需求就是:让用户在关卡视口里点一下某个物件,工具立刻把这个物件高亮出来,然后拿着这个物件去干后续的活——批量改材质、收集资产信息、检查贴图尺寸,诸如此类。
这个动作在运行游戏的时候很好写,无非是射线检测加 HitResult,可一旦放进编辑器工具里,老办法就失灵了。原因也很直接:编辑器视口有自己的一套拾取和选择体系,跟运行时输入那套完全是两码事。我第一次做这类工具时,就是没想明白这一点,硬是用运行时逻辑去编辑器里捞对象,结果点击事件根本到不了角色控制器,最后只能灰溜溜地改用编辑器原生的选择机制。而这条路上最顺手的钥匙,就是引擎提供的 HighlightPickedActors 相关接口。
这篇内容我按自己的实操路径来写:从底层逻辑讲清楚,再给一个能直接抄作业的工具示例,最后把别人文档里很少提到的坑一并列出来。
1. 编辑器的"拾取"和运行时的"射线检测"根本不是一回事
1.1 我第一次在编辑器工具里用射线检测翻车的过程
最开始我做一个小工具,目标很简单:打开工具面板,在视口里点一个 StaticMeshActor,面板上就显示这个物件的移动性、碰撞设置和贴图尺寸。我当时第一反应就是用熟悉的射线检测方案:从玩家相机打一条线到鼠标位置,拿 HitResult 反推出 Actor。
听起来没毛病,但放到编辑器环境里就处处碰壁。
编辑器视口虽然长得很像游戏画面,但它本质上是SLevelViewport这种 Slate 控件,鼠标点击事件首先被视口控件捕获,然后交给编辑器的FEditorViewportClient去处理。编辑器里根本没有一个常见的"Pawn"去执行GetHitResultUnderCursor,所以我用 PlayerController 的方案直接抓瞎。就算强行绕道,用FViewportClient的鼠标事件去手动算一条FHitResult,也会发现编辑器视口里有很多特殊对象:Gizmo 拖拽柄、网格对齐点、BSP 面、编辑器专属的调试绘制物……这些玩意在运行时根本不存在,但在编辑器拾取中会干扰结果。
所以结论先放在这里:在编辑器工具里做"点选一个对象",不要自己去写射线,直接用引擎编辑器的拾取子系统,让它负责把"鼠标点击到了哪个 Actor"这件事翻译给你。
1.2 编辑器选择体系真正的入口:EditorActorSubsystem
UE5 之后,编辑器脚本系统把以前散落在各个库里的函数统一收编到了一类 Subsystem 里。我们这里要关注的是EditorActorSubsystem(C++ 里是UEditorActorSubsystem,Python 里是unreal.EditorActorSubsystem)。
这个 Subsystem 负责的就是关卡里 Actor 级别的操作:选中、取消选中、获取当前选择、把一批 Actor 设为当前选择。平时我们手动用鼠标框选一堆 Actor,本质上就是在不断更新这个 Subsystem 内部维护的"选择集合"。
几个最常用的接口:
| 接口 | 作用 |
|---|---|
get_selected_actors() | 拿当前选中的所有 Actor |
set_selected_actors(actors) | 把指定 Actor 列表设为当前选择 |
set_actor_selection_state(actor, flag) | 单独设置某个 Actor 的选中/取消 |
clear_actor_selection() | 清空当前选择 |
highlight_picked_actors() | 进入"拾取高亮"模式,让用户在视口中点击一个对象 |
on_selection_changed | 选择集合发生变化时触发的事件委托 |
其中highlight_picked_actors()正好就是这个标题的核心。它像一个冒泡排序的问题吗?不是,它更像一个"暂停键":调用之后,编辑器会等待你在视口中点击一个 Actor,点完,引擎内部就把这个 Actor 放进选择集合,并触发高亮渲染。
1.3 什么时候该用 HighlightPickedActors
我后来在项目里总结出一个经验:凡是工具流程需要"人工指定一个对象,再自动处理这个对象"的,都适合用highlight_picked_actors起步。
典型场景有这几种:
- 美术在场景里点选一个镜头,工具自动把该镜头的近裁剪面、光圈参数导成表格。
- 关卡策划点选一个物件,工具直接把该物件绑定的数据资产路径输出到日志。
- 地编在检查漏刷碰撞时,点选一个地面块,工具高亮它并列出该块引用了哪些贴图,以及贴图的内存占用。
- 批量操作钱,第一步往往就是"先选定这个批次的代表对象",用这个接口锁定目标。
如果一个工具根本不需要人工点选,那自然用不上它;但如果需要"你和引擎之间做一次交互式对象传递",它就是最贴近日常习惯的方式。
2. HighlightPickedActors 到底做了什么:高亮与选中的关系
2.1 一次完整调用内部发生了什么
很多人以为highlight_picked_actors()是"直接把当前选中的 Actor 高亮一下",这个理解其实不准。它真正做的是把编辑器视口切换到"等待用户拾取一个 Actor"的状态,拾取动作完成之后,被点到的 Actor 才会进入选择集合并显示高亮。
一次完整流程可以拆成四步:
- 工具代码调用
highlight_picked_actors()。 - 编辑器接管视口的下一次鼠标点击,做一次精确的拾取(Picking)。
- 被点击到的 Actor 被加入当前选择集合。
- 视口因为选择集合变化,自动给该 Actor 绘制高亮轮廓,同时触发
on_selection_changed委托。
这里有个非常关键的理解:编辑器里的"高亮",本质上是"选中"的视觉表现,而不是一个独立于选择之外的标记。也就是说,highlight_picked_actors并没有发明一套"高亮专用通道",它只是替你完成了从拾取到选中这一步。高亮线条本身由编辑器视口的 Selection Outline 系统统一渲染,遵循你编辑器偏好设置里那个"选择轮廓颜色"的设定。
所以,如果你调用完这个函数立刻调用get_selected_actors(),理论上是能拿到对象的——前提是你已经完成了点击。如果用户还没点击视口,那这次查询大概率还是拿到之前的选择。这个时序问题很容易坑人,后面我专门讲。
2.2 它和 SetSelectedActors、GetSelectedActors 的关系
编码视角里,这几个函数的关系很像是三层结构:
GetSelectedActors:只管"当前选中了谁"。SetSelectedActors:只管"我强行指定选中谁"。HighlightPickedActors:是前两者的交互式入口,它把"用户在视口里点到谁"翻译成"谁应该被选中"。
举个例子,你用脚本写set_selected_actors([actorA]),A 会立刻高亮,但用户没有参与点击;你用highlight_picked_actors(),则是把决定权交给用户,等用户点了某个 Actor,引擎再自动调用类似"选中指定对象"的逻辑去更新选择集合。
落实到蓝图上,我的建议是:
- 如果工具的输入是"用户自己选中的 Actor",用
get_selected_actors()就够。 - 如果工具的流程是"点击按钮后,屏蔽普通操作,强制用户去点一个对象",用
highlight_picked_actors()。 - 如果需要在用户点击过后立刻执行后续逻辑,不要跟在
highlight_picked_actors()后面直接写处理代码,应该绑定on_selection_changed事件,在回调里执行。
第三个建议尤其重要。编辑器 UI 事件和脚本执行流不是同一个线程模型,用户点击视口的时间是未知的,你没法在同步流程里等到那次点击。事件委托是唯一可靠的通知方式。
2.3 从源码角度怎么看这个函数(UE5.3 参考)
在 UE5.3 的UEditorActorSubsystem相关源码里,HighlightPickedActors的签名大致是:
UFUNCTION(BlueprintCallable, Category = "Editor Scripting | Actor Editing") void HighlightPickedActors();没有参数,也没有返回值,C++ 里调用非常简洁:
UEditorActorSubsystem* ActorSubsystem = GEditor->GetEditorSubsystem<UEditorActorSubsystem>(); ActorSubsystem->HighlightPickedActors();Python 里对应写法是:
import unreal actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) actor_subsystem.highlight_picked_actors()需要注意,不同 UE 小版本里这个函数可能有细微差异。早期版本里,这个功能可能掩护在UEditorLevelLibrary或别的编辑器脚本库里,函数名也可能是单数HighlightPickedActor。我在从 UE5.1 项目升到 UE5.3 项目时,就遇到过脚本 API 改名的情况。建议你写工具前先在你的引擎版本里搜索一下highlight_pick,看看当前版本到底是哪个函数名、属于哪个 Subsystem,不要直接抄互联网上旧版本的代码。
3. 实操落地:用 HighlightPickedActors 搭一个"点击查看对象信息"的小工具
3.1 蓝图方案:Editor Utility Widget + On Selection Changed
大部分人做编辑器小工具的首选是 Editor Utility Widget。它的运行环境和普通 UMG 不一样,启动时就在编辑器进程里,很多编辑器专用节点可以直接暴露出来。
搭建步骤我按顺序整理一遍:
- 内容浏览器里右键 → UI → Editor Utility Widget,起名
WB_PickActorInspector。 - 面板上放一个按钮,绑定点击事件,逻辑节点里调用
Editor Actor Subsystem的Highlight Picked Actors节点。 - 在工具的 Event Graph 里,从同一个
Get Editor Actor Subsystem节点拖出On Selection Changed事件,绑定一个自定义事件,比如命名为OnSelectionUpdated。 - 在
OnSelectionUpdated事件里,从事件输出拿到Selection State,再从中取出Selected Actors数组。 - 如果只想处理第一个 Actor,用
Get节点取索引 0;然后把Actor引用传给后续的属性读取流程。
用蓝图的好处是调试直观,节点连线能直接在断点上看到中间数据。坏处是,一旦逻辑变复杂,节点图会膨胀到很难维护。我个人建议:只做简单展示用蓝图没问题,一旦涉及批量遍历、资产引用分析,直接上 Python 或者 C++ 编辑器模块。
3.2 Python 方案:脚本化拾取与回调
Python 方案在我看来是编辑器工具里性价比最高的形式。它能直接跑在编辑器里,改起来不用编译,配合unreal模块可以做到和蓝图几乎一样的事。
先看最简单的版本:
import unreal actor_subsystem = unreal.get_editor_subsystem(unreal.EditorActorSubsystem) def on_selection_changed(selection_state): actors = selection_state.get_editable_actor_array() if not actors: return actor = actors[0] unreal.log("Pick result: " + actor.get_actor_label()) # 在这里写你的后续处理逻辑 actor_subsystem.on_selection_changed.add_callable(on_selection_changed) actor_subsystem.highlight_picked_actors()需要注意,selection_state.get_editable_actor_array()这个方法是从SelectionState对象上取出可编辑的 Actor 数组。不同引擎版本字段名可能不同,有的版本直接暴露actor_array属性,有的版本需要通过get_editor_property("selected_actors")获取。如果你在某个版本里拿不到数组,先打印dir(selection_state)看看有哪些可用成员。
另一个要点:脚本跑完后,委托要记得移除。如果你是在一个工具窗口里长期运行的脚本,每次打开工具都绑一次回调,关掉又不清理,委托就会越积越多,最终导致同样一个点击动作被触发十几次。清理方法也很简单:
actor_subsystem.on_selection_changed.remove_callable(on_selection_changed)3.3 拿到 Actor 引用后,怎么安全地读取属性
从回调里拿到的是AActor引用,这是编辑器的有效对象,但你不一定能立刻读取到想要的属性,因为很多属性在 Component 上,而不在 Actor 本身。
比如你想读一个 StaticMeshActor 的网格资产路径:
mesh_actor = actor # 假设类型是 StaticMeshActor static_mesh_component = mesh_actor.static_mesh_component mesh = static_mesh_component.static_mesh asset_path = mesh.get_path_name()如果你不确定 Actor 类型,一定要先做is_a判断:
if not actor.is_a(unreal.StaticMeshActor): unreal.log_warning("Not a StaticMeshActor") return这也是我踩过不少次坑的地方:直接访问 Actor 上的属性,结果属性不存在报错,整个脚本中断,偶尔还会把编辑器搞到无响应。安全做法是"先判断类型、再取组件、再取属性、最后才交给下一步逻辑"。
4. 高亮之后能干的四件实事
高亮本身不是目的,拿到那个对象开始干活才是。
4.1 批量属性合规检查
我项目里有一个工具,专门检查场景里所有可移动物件的碰撞设置。美术每次提交场景前跑一遍:先自动选中一批疑似有问题的 Actor,然后高亮第一个,面板上显示它的Collision Enabled、Generate Overlap Events、Mobility三项数值。
这里highlight_picked_actors扮演的是"逐个审查"的角色:
- 用户点下一个 Actor,工具把它高亮。
- 面板立刻显示该 Actor 的三项关键设置。
- 如果合规,点击"下一项"按钮,工具自动取消当前选中并继续等待下一个点击。
- 如果不合规,点击"标记",工具把这个 Actor 写进一个临时列表。
逻辑很简单,但实际用起来效率非常高。美术不需要自己在几万个物资里翻找,只需要一路点下去,像做流水线质检一样。
4.2 资产依赖快速盘点
另一个场景是查资源依赖。点选一个 Actor,立刻列出它引用的所有资产路径,并且按贴图、材质、网格、物理资产分类。
核心思路是利用AssetRegistrySubsystem查引用关系,而不是自己去遍历组件里的资产引用,因为后者往往不完整。举个简化例子:
asset_registry = unreal.AssetRegistryHelpers.get_asset_registry() def get_dependencies(asset_path): dep_list = unreal.ArrayAssetIdentifier() asset_registry.get_dependencies(asset_path, dep_list) return dep_list把highlight_picked_actors选中的网格资产的路径交给这个函数,就能快速获取被引用资产列表。这个工具在排查"为什么某个贴图没被打包"时非常有用。
4.3 一键替换材质或标签的流程
再比如批量给物件打标签。地编在组织场景时经常要给一栋楼的所有部件加同一个 Tag,人工一个个点开细节面板操作非常费神。用这个模式,点选一个目标物件,工具就以它为基准,把所有同类网格、位置相近的物件统一打上某个 Tag:
def apply_tag(actor, tag_name): actor.tags.append(tag_name)这种工具的关键点在于,第一步用highlight_picked_actors确定"基准对象",而不是手动在 UI 上输入对象名。因为对象名往往是SM_House_A_007这种随机后缀,手输容易错,点选不容易错。
4.4 把这些操作串成每日工作流
等这几个小工具各自跑通,下一步就是串成一个"综合检查面板":一个 Editor Utility Widget 里放几个按钮,分别对应"规格检查""资产盘点""打标签"。
执行顺序通常是:
- 点击"开始检查"按钮。
- 工具清空旧选择,调用
highlight_picked_actors()。 - 用户点选一个 Actor。
- 回调里根据当前模式,分发到对应的处理函数。
- 处理完,面板显示结果,同时保留高亮,方便用户确认。
这里有一个实用的操作技巧:每次处理完一个 Actor,主动调用clear_actor_selection()再重新高亮这个 Actor,可以强制视口刷新一下轮廓状态,避免某些版本里轮廓颜色残留或者消失的问题。
5. 我用 HighlightPickedActors 写工具时踩过的几个坑
5.1 点击了但是没反应:检查前置条件
这个坑出现的频率最高,我自己和团队同事都遇到过。症状是:点了按钮,光标看起来也进入了拾取模式,但无论怎么点关卡物件,回调就是不触发,或者干脆点击没反应。
排查顺序建议按这个来:
- 确认当前选中的是关卡视口。有的工具窗口把键盘焦点拿走了,鼠标点击事件到不了视口。你需要在调用前把焦点还给视口,或者至少提示用户先点一下视口再点按钮。
- 确认没有其他编辑器模式拦截拾取。比如
Landscape编辑模式、Foliage编辑模式下,视口会有自己的拾取逻辑,highlight_picked_actors可能不生效。切换到默认的Placement或Select模式再试。 - 确认目标 Actor 是可见且可拾取的。被
TemporarilyHide的 Actor、被关掉bSelectable的 Actor,鼠标是点不到的,它会被视口拾取逻辑直接忽略。 - 确认视口没有开启某些特殊预览模式。比如某些无光照预览、路径追踪预览模式下,拾取行为会有差异。实在不行就切回 Lit 模式。
5.2 高亮集合没清干净导致后续误选
如果工具里连续执行多次"点击对象→处理"的循环,很容易出现上一次选中的 Actor 还留在选择集合里,下一次点选时变成"让我看到你新选的那个,但旧的高亮还在"。
这个状态往往会影响你自己的处理逻辑:比如你取get_selected_actors()时拿到的就是两个 Actor 而不是一个。
解决办法是在进入下一次拾取前主动清空选择:
actor_subsystem.clear_actor_selection() actor_subsystem.highlight_picked_actors()不过注意,这个操作本身会触发一次on_selection_changed,如果回调里没有判断actors是否为空,就有可能在清空的一瞬间跑一遍错误逻辑。所以回调函数里的第一行永远是空数组检查。
5.3 高亮性能开销与视口模式的影响
在大关卡里,如果你选择了一批非常复杂的网格并保持高亮,编辑器视口会明显变卡。这是因为 Selection Outline 需要对选中物体的边缘做后处理级别的渲染,复杂网格的轮廓生成代价不小。
如果发现这个问题,我一般这么做:
- 处理完一个 Actor 后,立刻调用
clear_actor_selection(),不要让它一直保持高亮。 - 如果确实需要持续指示"我正在处理哪个对象",可以同时使用编辑器里的
Actor Label气泡和日志输出,而不是依赖高亮轮廓。 - 某些版本可以在
Editor Preferences里调整高亮的反锯齿、粗细等参数,但这些属于全局设置,不要为目标工具单独改。
在大型场景里,我建议高亮只作为短时反馈,持续时间不要超过 1 到 2 秒。视觉上的确认可以用浮动文字、血量条一样的 Actor 标签气泡来替代。
5.4 想要"只高亮不改变选中"怎么办:替代方案
有一类需求经常被提出来:工具要是不想改变用户当前的选择集,只临时高亮一下某个对象,那该怎么办?
说实话,highlight_picked_actors不太适合这种需求,因为它天然走的是"选中即高亮"这个路线。你不想要真正的选择变化,就得自己画高亮。
我在项目里试过几种替代方案:
- 给网格生成一个描边材质实例:动态创建一个自发光叠加材质,挂在目标 Actor 的网格组件上,用完再恢复。这个方案效果可控,但动态创建材质实例本身有开销,不适合高频切换。
- 使用编辑器 Debug Draw:在目标 Actor 周围画一个线框盒或者定位线,可以持续提供视觉反馈,不改动任何选择状态。缺点是它只是线框,不是完整的物体描边。
- 自定义一个简单的 FEdMode:在编辑器模式下接管 Draw 事件,手动绘制高亮。这个方案最灵活,但实现成本也最高。我不建议第一次做工具的人直接上这个方案,等真遇到性能或交互瓶颈再说。
我的习惯是:能用选择高亮就不自己画;必须自己画时,优先 Debug Draw;Debug Draw 不够再上材质实例。把复杂度控制在需要它的地方,工具才容易维护。
如果你只是想快速让用户在视口里挑一个物体,然后把它高亮出来,highlight_picked_actors这条路几乎不用改任何配置就能跑通。我的建议是,先基于它做一个最小闭环:按钮点击→拾取高亮→回调里拿到 Actor→打印名称。等这个闭环真正通了,再往里面加批量检查、资产分析这些重逻辑。编辑器工具开发最忌讳一上来就堆功能,因为后面每加一层逻辑,前边"对象有没有拿到"这个基础问题都可能变一种新的出错方式。