UE5.8 Smart Objects 专题系列

UE5.8 Smart Objects 专题(一):先做一个能用的椅子

从启用插件开始,创建 SmartObjectDefinition,给椅子 Actor 添加 SmartObjectComponent,配置 Slot、ActivityTag、GameplayBehavior,并让 AI 找到、占用、走过去、使用和释放。

总览

第一篇只做一件事:让一个 AI 真正使用一把椅子。不要先设计大系统,也不要一上来接 Mass。你需要先亲眼看到 Slot 从 Free 变成 Claimed,再变成 Occupied,最后释放回 Free。这个闭环跑通,后面所有复杂案例都是它的变体。

UE5.8 Smart Objects 专题(一):先做一个能用的椅子 配图
第一个案例只追求闭环:对象进系统,AI 找到 Slot,占用,移动,使用,释放。

编辑器步骤

  1. 启用 SmartObjects 插件。需要 Behavior Tree 自动执行行为时,再启用 GameplayBehaviorSmartObjects;需要 StateTree 交互时,再启用 GameplayInteractions
  2. 创建 SmartObjectDefinition,命名 SOD_Chair_Sit
  3. 打开 Definition,加一个 Slot,命名 Sit_01,把 Offset 放在椅子座位前或座位点,Rotation 朝向角色坐下后的方向。
  4. 给 Definition 或 Slot 添加 ActivityTags,例如 Activity.Sit。这是“我能做什么”的标签。
  5. 如果只允许人形 NPC 使用,配置 UserTagFilter,例如要求用户标签包含 User.Humanoid
  6. 创建椅子 Actor 或蓝图 BP_SO_Chair,添加 StaticMesh,再添加 USmartObjectComponent,Definition 指向 SOD_Chair_Sit
  7. 放进测试关卡,打开 Gameplay Debugger 的 Smart Object 类别,确认椅子和 Slot 能画出来。

最小 C++ 查询

#include "SmartObjectRequestTypes.h"
#include "SmartObjectRuntime.h"
#include "SmartObjectSubsystem.h"
#include "SmartObjectTypes.h"

bool UMySmartObjectHelper::TryClaimChair(AActor& UserActor, float Radius, FSmartObjectClaimHandle& OutClaim)
{
    UWorld* World = UserActor.GetWorld();
    USmartObjectSubsystem* Subsystem = USmartObjectSubsystem::GetCurrent(World);
    if (!Subsystem)
    {
        return false;
    }

    FSmartObjectRequestFilter Filter;
    Filter.UserTags.AddTag(FGameplayTag::RequestGameplayTag(TEXT("User.Humanoid")));
    Filter.ActivityRequirements = FGameplayTagQuery::BuildQuery(
        FGameplayTagQueryExpression().AllTagsMatch().AddTag(
            FGameplayTag::RequestGameplayTag(TEXT("Activity.Sit"))));

    const FVector Origin = UserActor.GetActorLocation();
    FSmartObjectRequest Request(FBox::BuildAABB(Origin, FVector(Radius)), Filter);
    const FSmartObjectRequestResult Result = Subsystem->FindSmartObject(Request, &UserActor);
    if (!Result.IsValid() || !Subsystem->CanBeClaimed(Result.SlotHandle))
    {
        return false;
    }

    OutClaim = Subsystem->MarkSlotAsClaimed(
        Result.SlotHandle,
        ESmartObjectClaimPriority::Normal,
        FConstStructView::Make(FSmartObjectActorUserData(&UserActor)));

    return OutClaim.IsValid();
}

移动过去后,行为开始时调用 MarkSlotAsOccupied,结束或中断时调用 MarkSlotAsFree。真实项目里这三步通常分散在 BT Task、StateTree Task 或 AbilityTask 里,但生命周期不能少。

使用案例

把测试关卡做成三把椅子:一把普通椅子、一把已经 Disabled 的椅子、一把只允许 User.VIP 使用的椅子。让 AI 带 User.Humanoid 但不带 User.VIP。这会同时验证空间查询、启用状态和 UserTagFilter。能解释为什么某把椅子没被选中,比“能坐上去”更重要。

架构分析

椅子 Actor 本身不是 Smart Object 的全部。Actor 只是世界里的载体;USmartObjectComponent 持有 FSmartObjectDefinitionReference,并在注册时把 Definition、Transform、OwnerData 交给 USmartObjectSubsystem。Definition 是资产,Slot 是资产里的相对位置,Subsystem 才保存运行时句柄和状态。

FSmartObjectRequestResult 只是“找到了候选 Slot”。它不是占用权。真正防止别人抢走的动作是 MarkSlotAsClaimed,真正开始使用是 MarkSlotAsOccupied。这三个阶段分清楚,很多并发 bug 会消失。

项目落地

第一批命名建议:SOD_Chair_Sit 表示定义资产,BP_SO_Chair 表示带组件的对象蓝图,Activity.Sit 表示行为能力,User.Humanoid 表示使用者类型。不要把标签命名成 ChairNPC 这种含糊词,查询时会分不清是对象类型、行为类型还是用户类型。

常见坑

  • Slot 放在椅子中心,AI MoveTo 会撞到椅子:先把入口点放在椅子前方,再让动画 root motion 或对齐逻辑处理最后半米。
  • Definition 没 Validate:无效资产不会正常注册到运行时。
  • ActivityTags 写在对象和 Slot 上但策略没理解:先统一写在 Slot,后面再用合并/覆盖策略。
  • AI 只 Find 不 Claim:两个 AI 会同时冲向同一把椅子。
  • 中断没释放:BT Abort、StateTree Exit、Ability Cancel 都必须释放 Claim。

源码依据

USmartObjectComponent::BeginPlay 会注册到 USmartObjectSubsystem;组件的 Definition 存在 FSmartObjectDefinitionReference 里。USmartObjectDefinition 里每个 FSmartObjectSlotDefinition 保存 Offset、Rotation、ActivityTags、UserTagFilter、SelectionPreconditions、BehaviorDefinitions 和 DefinitionData。USmartObjectSubsystem 提供 FindSmartObjectCanBeClaimedMarkSlotAsClaimedMarkSlotAsOccupiedMarkSlotAsFree

源码路径索引

  • SmartObjects.uplugin
  • SmartObjectsModule/Public/SmartObjectDefinition.h
  • SmartObjectsModule/Public/SmartObjectComponent.h
  • SmartObjectsModule/Public/SmartObjectSubsystem.h
  • GameplayBehaviorSmartObjectsModule/Public/GameplayBehaviorSmartObjectBehaviorDefinition.h