UE5.8 Smart Objects 与交互槽位:从工坊里的排队说起
引子:工坊里的混乱排队
去年夏天,我接到一个任务:给项目里的主城加一批生活 NPC。这群 NPC 白天要在铁匠铺打杂、在城墙巡逻、在酒馆休息。策划的要求听起来简单:让 AI 看起来有事可做,别都挤成一堆。
第一版实现很快上线。我在铁匠铺里放了三个工作台,每个工作台挂了一个 Trigger Box。NPC 靠近后就播放打铁动画,结束后离开。测试时我发现,当有五个 NPC 同时想打铁,三个工作台会被瞬间占满,剩下两个 NPC 站在原地发呆,或者反复在三个工作台之间打转。更糟的是,一个 NPC 刚走开,另一个 NPC 还没完全站到位置,第三个 NPC 就判定工作台为空,三个人挤在同一个动画锚点上。
城墙上的射手表现也没好到哪去。我放了十二个射击位,敌人从东边进攻时,所有射手都往最近的几个位置跑,导致城垛某一段挤满了人,其他位置却空着。酒馆座椅的问题更微妙:两个 NPC 同时选中同一把椅子,坐下时发生穿模,其中一个被挤出几米远。
这些问题有个共同点:AI 知道世界里有交互对象,但不知道怎么有序地占用这些对象上的具体位置。UE5.8 的 Smart Objects(智能对象)和 Slot(交互槽位)正是为了解决这个问题而存在的。
flowchart TD
subgraph world ["游戏世界"]
workbench["铁匠工作台"]
chair["酒馆椅子"]
slotA["槽位 A"]
slotB["槽位 B"]
end
subgraph ai ["NPC 大脑"]
brain["AIController / MassEntity"]
request["Slot 申请请求"]
end
subgraph system ["Smart Object 系统"]
so["SmartObjectSubsystem"]
collection["Slot 集合"]
behavior["Behavior Definition"]
end
brain --> request
request --> so
so --> workbench
so --> chair
workbench --> collection
chair --> collection
collection --> slotA
collection --> slotB
behavior --> so一、Smart Object 是什么
Smart Object 不是指某个具体的 Actor 类,而是 UE5.8 提供的一套框架。它的核心思想是:把「世界里的可交互资源」和「使用这些资源的 AI 行为」解耦。
一个铁匠工作台本身只是一个 Static Mesh Actor,上面挂一个 SmartObjectComponent 之后,它就变成 Smart Object。这个组件不直接控制 NPC,而是向 SmartObjectSubsystem 注册自己,声明「我这里有若干 Slot,每个 Slot 都配有一段 Behavior」。NPC 不再直接查询工作台,而是向子系统申请一个 Slot。子系统根据条件筛选、评分、分配,NPC 拿到 Slot 后执行对应的 Behavior。
这种设计带来几个好处:
- 工作台不需要知道谁会来使用它。新增一种 NPC 时,工作台代码不用改。
- NPC 不需要硬编码目标位置。它只需要说「我想打铁」,具体站到哪个位置由 Smart Object 系统决定。
- 交互逻辑可以复用。同一把椅子可以被顾客、乞丐、士兵共用,各自触发不同的动画和对话。
UE5.8 里和 Smart Object 相关的主要类有这几个:
| 类 | 作用 |
|---|---|
USmartObjectSubsystem | 全局管理器,负责注册、查询、分配 Slot。 |
USmartObjectComponent | 挂到 Actor 上,把普通对象注册成 Smart Object。 |
USmartObjectDefinition | 定义 Slot 布局、每个 Slot 的 Behavior、使用条件。 |
FSmartObjectSlotHandle | Slot 的句柄,申请成功后返回给 AI。 |
USmartObjectBehaviorDefinition | 描述占用 Slot 时要执行的行为,常见子类是 Behavior Tree 版本。 |
一个 Smart Object 可以包含多个 Slot。每个 Slot 有世界空间中的 Transform、可选的 Tag、使用条件、以及占用后的行为。Slot 之间可以有从属关系,比如攻城锤需要六个人同时推拉,就可以定义六个关联 Slot。
二、Slot 定义与申请
Slot 是 Smart Object 框架里最具体的概念。可以把 Slot 理解成「交互锚点」:它有位置、朝向、占用者、使用条件、执行行为。铁匠铺的工作台可以定义三个 Slot,分别对应工作台的左侧、正面、右侧。酒馆的长椅可以定义两个 Slot,对应左右两个座位。
2.1 在编辑器里配置 Slot
Smart Object Definition 资产是配置 Slot 的地方。打开资产后,能看到一个 Slot 列表。每个 Slot 需要填:
- Offset:相对于 Smart Object Actor 原点的局部 Transform。
- Behavior Definition:占用该 Slot 后执行的 Behavior,最常用的是
SmartObjectBehaviorDefinition的 Behavior Tree 子类。 - Tags:Slot 自身的 Gameplay Tags,用来筛选和匹配。
- User Tags:对占用者提出的要求,比如「必须是铁匠」或「必须装备双手武器」。
- Selection Preconditions:额外选择条件,比如「只选面向敌人的 Slot」。
城墙射击位的配置会更复杂一些。每个射击位要朝向城墙外侧,Tag 设为 Combat.AttackPosition.Ranged,User Tag 要求占用者持有远程武器。这样当射手 AI 搜索攻击位时,Smart Object 系统会自动排除近战敌人、背对城外的 Slot,以及已经被占用的 Slot。
2.2 运行时申请 Slot
AI 申请 Slot 时,通常调用 FindSmartObject 或 ClaimSmartObject。Find 只返回可选 Slot 的信息,Claim 则真正占用。UE5.8 的推荐流程是:先调用 FindSmartObject 拿到候选,用自定义评分选出最佳,再调用 Claim 锁定它。
USTRUCT()
struct FMySlotRequest
{
GENERATED_BODY()
UPROPERTY(EditAnywhere, Category = "SmartObject")
FGameplayTagContainer ActivityTags;
UPROPERTY(EditAnywhere, Category = "SmartObject")
FGameplayTagContainer UserTags;
UPROPERTY(EditAnywhere, Category = "SmartObject")
TSubclassOf<USmartObjectBehaviorDefinition> BehaviorClass;
};
void UMyAIAbility::RequestWorkbenchSlot(AActor* User)
{
USmartObjectSubsystem* SO = USmartObjectSubsystem::GetCurrent(GetWorld());
if (!SO || !User) return;
FSmartObjectRequest Request;
Request.BehaviorDefinitionClass = USmartObjectBehaviorDefinition::StaticClass();
Request.ActivityTags = FGameplayTagContainer(FGameplayTag::RequestGameplayTag(TEXT("Activity.Blacksmith")));
Request.UserTags = FGameplayTagContainer(FGameplayTag::RequestGameplayTag(TEXT("Role.Worker")));
Request.QueryBox = FBox::BuildAABB(User->GetActorLocation(), FVector(500.0f));
FSmartObjectRequestResult Result = SO->FindSmartObject(Request);
if (Result.IsValid())
{
FSmartObjectClaimHandle Claim = SO->Claim(Result);
if (Claim.IsValid())
{
USmartObjectBehaviorDefinition* Behavior = SO->Use(Claim);
RunBehaviorForClaim(User, Claim, Behavior);
}
}
}上面的代码块约四十行。核心逻辑是构造 FSmartObjectRequest,指定活动标签、用户标签、查询范围,然后 Find、Claim、Use 三步走。Use 会返回 Behavior Definition,告诉 AI 接下来该执行哪段 Behavior Tree 或 State Tree。
2.3 申请流程
AI 从产生意图到真正站上 Slot,Smart Object 系统内部会做很多事情。下面这张图把流程拆开。
flowchart TD
A["NPC 产生交互意图"] --> B["构造 FSmartObjectRequest"]
B --> C{查询空间范围内已注册 Smart Object}
C --> D["筛选符合 ActivityTags 的对象"]
D --> E["检查每个 Slot 的 UserTags 与占用状态"]
E --> F{有可用 Slot?}
F -->|"否"| G["返回失败 / 加入等待队列"]
F -->|"是"| H["按评分函数排序候选 Slot"]
H --> I["Claim 锁定最高分 Slot"]
I --> J["标记 Slot 为已占用"]
J --> K["Use 返回 Behavior Definition"]
K --> L["AI 移动到 Slot 位置并执行行为"]
style I fill:#ffcccc
style H fill:#ffffccClaim 操作是原子的。两个 NPC 同时申请同一个 Slot 时,子系统只让第一个成功,第二个拿到失败或次优 Slot。这样就解决了前文提到的 NPC 挤占同一个工作台的问题。
三、释放与超时
Slot 不能无限占用。NPC 打铁结束后必须释放 Slot,否则其他 NPC 永远用不上。UE5.8 提供了 Release 接口,通常在 Behavior Tree 的 Task 末尾调用。
3.1 正常释放
AI 执行完 Slot 对应的行为后,调用 USmartObjectSubsystem::Release(ClaimHandle)。释放后 Slot 回到可用池,其他 NPC 可以再次申请。释放时可以选择是否同时停止正在运行的 Behavior。
Behavior Tree 里常见的写法是:Sequence 末尾加一个自定义 Task BTT_ReleaseSmartObjectSlot。这个 Task 接收 Blackboard 里的 Claim Handle,调用 Release,然后清空黑板键。Sequence 结构可能是:
- MoveTo Slot Location
- Rotate to face Slot Forward
- Play Workbench Animation
- Release Slot
如果中间任何一步失败,比如 NPC 被玩家推开、动画被打断,都要确保 Release 被调用。否则会出现「幽灵占用」:Slot 看起来空着,系统却认为它还在使用中。
3.2 超时与异常释放
实际游戏中,AI 可能被玩家攻击、被事件打断、或者因为导航失败永远走不到 Slot 位置。Smart Object 系统支持 Slot 使用超时机制:在 Claim 时设置 MaxDuration,超过时间后自动释放。
超时的实现可以在 AI 侧,也可以在 Smart Object 侧。AI 侧的做法是给 Claim Handle 关联一个计时器,时间到了强制 Release。Smart Object 侧的做法是在 USmartObjectSubsystem 中维护一个占用超时列表,每帧检查是否有 Slot 该被收回。
更稳妥的做法是两边都做:AI 行为超时后主动释放,子系统再兜底回收那些没释放的 Slot。兜底回收的触发要谨慎,因为有些交互本身就很长,比如攻城锤需要推三十秒,不能把正常的长交互误判为超时。
四、与 Behavior Tree / Mass 的集成
Smart Object 框架设计之初就考虑了两种主要使用方式:传统的 Behavior Tree AI,以及面向大规模的 Mass AI。
4.1 Behavior Tree 集成
Behavior Tree 是大多数项目里 AI 的指挥中枢。UE5.8 提供了内置 Task BTTask_UseSmartObject,可以直接在行为树里申请并使用 Smart Object。
这个 Task 的工作流程是:
- 激活时根据 Blackboard 里的 Smart Object 引用或 Request 配置,调用
FindSmartObject。 - 找到候选后 Claim 一个 Slot。
- 调用
Use获取 Behavior Definition,如果 Definition 里指定了子行为树,就运行子树。 - 子树完成后,Task 自动 Release Slot。
在铁匠铺场景里,NPC 的顶层 Behavior Tree 可以很简单:Selector 下面是「如果该工作,就去申请工作台 Slot」。申请到后,子行为树负责播放打铁动画、释放 Slot。没有申请到,NPC 就站在一旁等待,或者换一个活动。
等待策略有很多选择。可以让 NPC 在附近 Idle 等待,每隔几秒重新 Find 一次;也可以让 Smart Object 维护一个等待队列,Slot 释放后按优先级通知下一个 NPC。UE5.8 本身没有内置等待队列,但可以通过 Gameplay Tag 事件或自定义子系统实现。
4.2 Mass AI 集成
Mass AI 是 UE5 面向大规模智能体的框架。和传统 AI 不同,Mass 里的智能体没有 AAIController,数据存在 Fragment 里,逻辑写在 Processor 里。Smart Object 和 Mass 的集成通过 Mass Smart Object 模块完成。
Mass 场景下的典型流程是:
sequenceDiagram
participant NPC as "Mass Entity"
participant Proc as SmartObjectProcessor
participant Sub as SmartObjectSubsystem
participant Slot as "Slot 实例"
NPC->>Proc: Tick 更新意图
Proc->>Sub: FindSmartObject(Request)
Sub-->>Proc: 返回候选结果
Proc->>Sub: Claim(SlotHandle)
Sub-->>Proc: 返回 ClaimHandle
Sub->>Slot: 标记占用
Proc->>NPC: 写入 Slot Transform Fragment
NPC->>NPC: 移动到 Slot 位置
NPC->>Proc: 行为完成
Proc->>Sub: Release(ClaimHandle)
Sub->>Slot: 解除占用Mass 的优势在于批量处理。Processor 会一次性处理所有带有 SmartObject 相关 Fragment 的 Entity,而不是每个 NPC 单独 Tick。这让 Smart Object 系统可以扩展到上千个智能体。
和传统 Behavior Tree 方式相比,Mass 方式下的 Slot 占用数据也更轻量。Claim Handle 和 Slot Transform 都可以作为紧凑的 Fragment 存储,不需要完整的 Blackboard 和 Behavior Tree 实例。
五、交互点管理(攻击位、掩体、座位)
Smart Objects 最常见的用途之一,就是管理世界里各种「位置型资源」。这类资源的特点是:数量有限、需要排队、有朝向要求、不同 AI 适用性不同。
5.1 攻击位
城墙射手场景里,攻击位就是典型的位置型资源。策划在城墙上放了若干射击位,每个射击位有朝向、高度、遮蔽条件。射手 AI 想要射击时,会向 Smart Object 系统申请一个 Combat.AttackPosition.Ranged 的 Slot。
攻击位的设计要考虑几个因素:
- 朝向:Slot 的 Forward Vector 应该指向城外敌人方向,避免 AI 背对敌人射击。
- 高度:高位射击位视野好,但暴露风险大;低位射击位有掩体,但射程受限。
- 遮挡:Slot 附近是否有城垛、矮墙,决定 AI 是否需要起身射击。
- 距离:Slot 到目标的距离决定使用哪种武器动画。
这些差异可以通过 Slot 上的 Tag 和评分函数表达。高价值 Slot 优先分配给精英射手,普通射手用剩余位置。
5.2 掩体
掩体位的核心需求是:AI 受到攻击时,能够快速找到附近的可靠遮蔽。掩体位通常用 Combat.Cover Tag 标记,每个 Slot 还要说明遮蔽方向、高度、是否可站立。
一个优秀的掩体系统,Slot 数量会比攻击位更多,分布也更密集。因为掩体是临时性的,AI 可能几秒就换一个位置。Smart Object 让这种频繁切换变得可控:AI 先申请一个掩体 Slot,跑过去蹲下,几秒后再申请下一个,原 Slot 自动释放。
掩体位的评分函数和攻击位不同。攻击位看视野和射程,掩体位看距离威胁点的远近、遮蔽完整性、以及到下一个目标点的连通性。
5.3 座位
酒馆座位是生活向 AI 最常用的交互点。座位场景对穿模和顺序很敏感,所以 Slot 的 Transform 精度要求更高。每个椅子可以定义一到两个 Slot,长凳可以定义更多。
座位还有一个特殊需求:同一个座位可以被不同 AI 重复使用,但不同 AI 的坐下动画不同。成年人坐下、小孩爬上椅子、醉汉歪倒,这些都可以通过 Slot 上绑定不同的 Behavior Definition 实现。AI 申请座位时,子系统会根据 AI 自身的 Tag 选择匹配的 Behavior。
graph LR
A["攻击位"] -->|Tag| A1[Combat.AttackPosition.Ranged]
A -->|"评分"| A2["视野 > 射程 > 风险"]
B["掩体位"] -->|Tag| B1[Combat.Cover]
B -->|"评分"| B2["距离威胁 < 遮蔽完整 < 连通性"]
C["座位"] -->|Tag| C1[Life.Seat]
C -->|"评分"| C3["距离 < 朝向匹配 < 行为匹配"]
style A fill:#ffcccc
style B fill:#ffffcc
style C fill:#ccffcc六、条件与评分
Smart Object 系统的强大之处,在于它不会随便给 AI 分配一个 Slot,而是可以根据条件筛选和评分,选出最合适的那个。
6.1 硬性条件
每个 FSmartObjectRequest 可以指定 Activity Tags 和 User Tags。Activity Tags 描述 AI 想做什么,User Tags 描述 AI 自身属性。Smart Object 系统会先过滤掉不匹配的 Slot,只在候选集里评分。
以城墙射手为例:
- Activity Tags:
Combat.AttackPosition.Ranged - User Tags:
Role.Archer、Weapon.Bow
不符合这两个 Tag 的 Slot 直接排除。比如没有远程武器的 NPC 不会申请射击位,射击位也不会分配给近战单位。
6.2 评分函数
过滤之后,多个 Slot 可能都符合条件。此时需要评分函数决定谁最好。评分通常基于距离、朝向、威胁、历史占用等因素。
假设某个 Slot 的得分为 ,可以写成:
其中 是 AI 到 Slot 的距离, 是 Slot 朝向与目标方向的夹角, 是 Slot 附近的威胁值, 是历史占用惩罚。、、、 是权重系数, 是各自归一化函数。
具体实现里,距离函数通常用反比或指数衰减:
控制衰减速度。如果 ,距离 100 单位时 ;距离 0 时 。这个参数需要策划反复调,太近会让 AI 扎堆,太远会让 AI 跑冤枉路。
朝向函数可以用余弦值:
时,Slot 正对目标,得分最高; 时,背对目标,得分最低。这个函数对射击位和掩体位都很重要。
6.3 动态调整
评分不是一成不变的。战斗激烈时,可以降低距离权重、提高掩体权重;安全区域里,可以降低威胁权重、提高朝向权重。这些调整可以通过 Gameplay Tag 或游戏状态驱动。
UE5.8 的 Smart Object 系统支持自定义 ISmartObjectSlotSelectionQuery,让项目自己实现评分逻辑。自定义查询可以访问更多上下文,比如当前波次、敌人分布、任务阶段等。
七、网络同步
多人游戏里,Smart Object 的占用状态必须同步到所有客户端,否则会出现一个玩家看到 NPC 坐在椅子上,另一个玩家看到同一个椅子空着的状况。
7.1 服务端权威
Smart Object 的 Claim 和 Release 操作应该在服务端执行。客户端可以发起申请请求,但最终是否成功由服务端决定。这种服务端权威设计能避免作弊和状态不一致。
Behavior Tree 方式下,AI 的 Controller 通常只在服务端存在,客户端看到的是同步过来的动画和位置。Smart Object 的占用状态自然也在服务端维护,客户端只需要接收 Slot 被占用后的结果。
Mass AI 方式下,Mass 模拟通常在服务端运行,客户端通过复制代理位置看到结果。Smart Object 相关 Fragment 可以配置为服务端专用,不需要同步到客户端。
7.2 Slot 状态同步
Smart Object 子系统内部维护了一个 Slot 状态表。对于需要同步的 Slot,可以使用 RepNotify 或 Condition 同步机制:
- Slot 被占用时,服务端广播占用事件,客户端播放对应 AI 的占用动画。
- Slot 被释放时,服务端广播释放事件,客户端清理占用表现。
- 对于大量 Slot,可以使用 dormancy 或优先级复制,减少网络流量。
如果 AI 使用 Slot 的过程较长,比如攻城锤需要多人协同,还要同步每个协同者的进度。这种场景下,Smart Object 往往和 Gameplay Ability System 配合,用 GAS 的网络同步能力驱动长交互。
7.3 延迟处理
网络延迟会让 Claim 操作产生竞争。客户端 A 和客户端 B 几乎同时申请同一个 Slot,服务端收到两个请求。即使服务端按顺序处理,客户端的反馈也会有先后。后收到结果的客户端需要优雅地处理失败:让 AI 寻找下一个可用 Slot,或者播放一个「位置被占了」的过渡动画。
八、性能优化
Smart Object 框架本身已经做了不少优化,但大规模使用时仍需注意几个点。
8.1 空间查询优化
FindSmartObject 的查询范围不要太大。范围越大,需要检查的 Slot 越多,评分函数调用次数也越多。对于开放世界,可以把世界划分成网格,每个网格维护自己内部的 Smart Object 列表。AI 只查询自己所在网格和相邻网格的 Slot。
UE5.8 的 Smart Object 子系统内部使用了空间加速结构,但项目侧仍然可以通过合理设置查询范围、限制 Activity Tags 数量来减少开销。
8.2 减少 Find 频率
不要每帧都调用 FindSmartObject。大多数 AI 的交互意图变化并不频繁,每 0.5 秒查询一次通常足够。对于等待 Slot 的 AI,可以采用指数退避:第一次查询失败后等 0.5 秒,第二次等 1 秒,第三次等 2 秒,避免大量 AI 同时查询造成峰值。
8.3 Slot 数量控制
Slot 不是越多越好。每个 Slot 都要占用内存,每次查询都要检查。策划摆放交互点时,应该根据实际玩法需要决定密度。城墙射手位可能每十米一个就够了,酒馆座位按真实座位数配置即可。
Mass AI 场景下,Slot 数量直接影响 Processor 的处理规模。如果世界里有上万个 Slot,Processor 遍历的开销会显著增加。可以通过 LOD 机制,只激活玩家附近的 Smart Object。
8.4 缓存评分结果
评分函数里有些输入变化很慢,比如威胁分布、地形高度。可以每几秒重新计算一次这些输入,而不是每次 Find 都重新算。把威胁图、高度图等中间结果缓存起来,能大幅减少评分开销。
结语
回到开头的铁匠铺。引入 Smart Objects 之后,五个 NPC 不再挤成一堆。三个工作台各有三个 Slot,NPC 按申请顺序依次占用。打铁结束后,Slot 被释放,下一个 NPC 补上。城墙射手也学会了分散站位,十二个射击位根据敌人方向动态分配。酒馆里每个座位都有了明确的归属,穿模和争抢基本消失。
Smart Objects 的价值不在于它提供了某个具体功能,而在于它把「交互位置资源」抽象成了一种可管理、可评分、可复用的系统。AI 只需要表达意图,剩下的事情交给 Smart Object 子系统。这种设计让策划摆放交互点时更自由,让程序员维护 AI 行为时更轻松,也让最终游戏体验更有秩序。
如果你正在 UE5.8 里做生活 NPC、战斗 AI 或大规模 Mass 人群,Smart Objects 与 Slot 值得花时间认真研究。先把铁匠铺里那三把锤子安排好,再扩展到整座城市的交互网络,会是一条稳妥的路线。