总览
第一篇只做一件事:让一个 AI 真正使用一把椅子。不要先设计大系统,也不要一上来接 Mass。你需要先亲眼看到 Slot 从 Free 变成 Claimed,再变成 Occupied,最后释放回 Free。这个闭环跑通,后面所有复杂案例都是它的变体。
编辑器步骤
- 启用
SmartObjects插件。需要 Behavior Tree 自动执行行为时,再启用GameplayBehaviorSmartObjects;需要 StateTree 交互时,再启用GameplayInteractions。 - 创建
SmartObjectDefinition,命名SOD_Chair_Sit。 - 打开 Definition,加一个 Slot,命名
Sit_01,把 Offset 放在椅子座位前或座位点,Rotation 朝向角色坐下后的方向。 - 给 Definition 或 Slot 添加
ActivityTags,例如Activity.Sit。这是“我能做什么”的标签。 - 如果只允许人形 NPC 使用,配置
UserTagFilter,例如要求用户标签包含User.Humanoid。 - 创建椅子 Actor 或蓝图
BP_SO_Chair,添加 StaticMesh,再添加USmartObjectComponent,Definition 指向SOD_Chair_Sit。 - 放进测试关卡,打开 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 表示使用者类型。不要把标签命名成 Chair 或 NPC 这种含糊词,查询时会分不清是对象类型、行为类型还是用户类型。
常见坑
- 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 提供 FindSmartObject、CanBeClaimed、MarkSlotAsClaimed、MarkSlotAsOccupied 和 MarkSlotAsFree。
源码路径索引
SmartObjects.upluginSmartObjectsModule/Public/SmartObjectDefinition.hSmartObjectsModule/Public/SmartObjectComponent.hSmartObjectsModule/Public/SmartObjectSubsystem.hGameplayBehaviorSmartObjectsModule/Public/GameplayBehaviorSmartObjectBehaviorDefinition.h