UE5.8 Smart Objects 与交互槽位:从工坊里的排队说起

📑 目录

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、使用条件。
FSmartObjectSlotHandleSlot 的句柄,申请成功后返回给 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 时,通常调用 FindSmartObjectClaimSmartObjectFind 只返回可选 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:#ffffcc

Claim 操作是原子的。两个 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 的工作流程是:

  1. 激活时根据 Blackboard 里的 Smart Object 引用或 Request 配置,调用 FindSmartObject
  2. 找到候选后 Claim 一个 Slot。
  3. 调用 Use 获取 Behavior Definition,如果 Definition 里指定了子行为树,就运行子树。
  4. 子树完成后,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.ArcherWeapon.Bow

不符合这两个 Tag 的 Slot 直接排除。比如没有远程武器的 NPC 不会申请射击位,射击位也不会分配给近战单位。

6.2 评分函数

过滤之后,多个 Slot 可能都符合条件。此时需要评分函数决定谁最好。评分通常基于距离、朝向、威胁、历史占用等因素。

假设某个 Slot 的得分为 SS,可以写成:

S=wdfd(d)+wafa(θ)+wtft(T)+whfh(H)S = w_d \cdot f_d(d) + w_a \cdot f_a(\theta) + w_t \cdot f_t(T) + w_h \cdot f_h(H)

其中 dd 是 AI 到 Slot 的距离,θ\theta 是 Slot 朝向与目标方向的夹角,TT 是 Slot 附近的威胁值,HH 是历史占用惩罚。wdw_dwaw_awtw_twhw_h 是权重系数,ff 是各自归一化函数。

具体实现里,距离函数通常用反比或指数衰减:

fd(d)=11+λdf_d(d) = \frac{1}{1 + \lambda d}

λ\lambda 控制衰减速度。如果 λ=0.01\lambda = 0.01,距离 100 单位时 fd0.5f_d \approx 0.5;距离 0 时 fd=1f_d = 1。这个参数需要策划反复调,太近会让 AI 扎堆,太远会让 AI 跑冤枉路。

朝向函数可以用余弦值:

fa(θ)=1+cos(θ)2f_a(\theta) = \frac{1 + \cos(\theta)}{2}

θ=0\theta = 0 时,Slot 正对目标,得分最高;θ=180\theta = 180^\circ 时,背对目标,得分最低。这个函数对射击位和掩体位都很重要。

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 值得花时间认真研究。先把铁匠铺里那三把锤子安排好,再扩展到整座城市的交互网络,会是一条稳妥的路线。