Claim/Use 시스템
ClaimHandle, SmartObjectSubsystem의 Claim/Use/Release 라이프사이클을 마스터합니다
Slot 라이프사이클
Free → Claimed → Occupied → Free
Smart Object Slot은 Free → Claimed → Occupied(Used) → Free의 라이프사이클을 가집니다. Claim은 예약, Use는 실제 사용이며, 하나의 Slot은 한 번에 하나의 사용자만 점유합니다.
// Slot 상태
enum class ESmartObjectSlotState : uint8
{
Free, // 사용 가능
Claimed, // 예약됨 (이동 중)
Occupied, // 사용 중
Disabled, // 비활성화됨
};
// 라이프사이클 흐름
// 1. Find: 적절한 Smart Object 검색
// 2. Claim: Slot 예약 (다른 AI 접근 방지)
// 3. Move To: Slot 위치로 이동
// 4. Use: 실제 상호작용 시작
// 5. Release: 사용 완료 후 해제
// Claim은 "이동 중 다른 AI가 선점하는 것"을 방지
// Claim 없이 바로 Use하면:
// AI_A와 AI_B가 동시에 같은 의자로 이동
// → 먼저 도착한 AI만 앉고 다른 AI는 실패
// Claim이 있으면:
// AI_A가 Claim → AI_B는 해당 Slot 검색 불가
// → AI_B는 다른 의자를 찾아감Claim 후 일정 시간 내에 Use하지 않으면 Claim이 만료됩니다. AI가 이동 중 장애물에 막혀 도착하지 못하면 Claim을 명시적으로 Release해야 합니다.
SmartObjectSubsystem API
검색, Claim, Use, Release 핵심 API
USmartObjectSubsystem은 Smart Object의 전체 라이프사이클을 관리하는 월드 서브시스템입니다. 검색부터 해제까지 모든 조작이 이 서브시스템을 통해 이루어집니다.
// SmartObjectSubsystem 접근
USmartObjectSubsystem* SOSubsystem =
USmartObjectSubsystem::GetCurrent(GetWorld());
// 1. 검색 (Find)
FSmartObjectRequest Request;
Request.Filter.ActivityRequirements.RequiredTags.AddTag(
FGameplayTag::RequestGameplayTag(FName("Activity.Sit")));
Request.QueryBox = FBox(
PawnLocation - FVector(2000),
PawnLocation + FVector(2000));
TArray<FSmartObjectRequestResult> Results;
SOSubsystem->FindSmartObjects(Request, Results);
// 2. Claim (예약)
if (Results.Num() > 0)
{
FSmartObjectClaimHandle ClaimHandle =
SOSubsystem->Claim(Results[0].SmartObjectHandle,
Results[0].SlotHandle);
if (ClaimHandle.IsValid())
{
// 예약 성공 → Slot 위치로 이동
FVector SlotLocation;
FRotator SlotRotation;
SOSubsystem->GetSlotLocation(
ClaimHandle, SlotLocation, SlotRotation);
// MoveToLocation으로 이동...
}
}
// 3. Use (사용 시작)
// Slot 위치에 도착한 후:
if (ClaimHandle.IsValid())
{
const USmartObjectBehaviorDefinition* BehaviorDef =
SOSubsystem->Use<USmartObjectBehaviorDefinition>(
ClaimHandle);
if (BehaviorDef)
{
// 행동 정의에 따라 실행
// 예: 앉기 애니메이션 재생
}
}
// 4. Release (해제)
SOSubsystem->Release(ClaimHandle);
// 또는 MarkSlotAsFree() 사용FSmartObjectClaimHandle은 가볍게 복사 가능한 핸들입니다. Blackboard에 저장하거나 BT 노드 간에 전달하여 Claim 상태를 추적하세요.
Gameplay Behavior와 Smart Object
SmartObject 사용 시 실행되는 행동 정의
UGameplayBehavior는 Smart Object 사용 시 실행되는 행동을 정의합니다. USmartObjectGameplayBehaviorDefinition이 Behavior 클래스를 참조하며, Slot의 BehaviorDefinition에 설정합니다.
// GameplayBehavior - Smart Object 사용 시 행동
UCLASS()
class UGameplayBehavior_Sit : public UGameplayBehavior
{
GENERATED_BODY()
public:
virtual bool Trigger(AActor& Avatar,
const UGameplayBehaviorConfig* Config,
AActor* SmartObjectOwner) override
{
// 앉기 시작
ACharacter* Character = Cast<ACharacter>(&Avatar);
if (!Character) return false;
// 앉기 애니메이션 재생
UAnimInstance* AnimInstance =
Character->GetMesh()->GetAnimInstance();
AnimInstance->Montage_Play(SitMontage);
// 완료 시 EndBehavior() 호출 필요
return true; // 성공적으로 시작
}
virtual void EndBehavior(AActor& Avatar,
const UGameplayBehaviorConfig* Config,
AActor* SmartObjectOwner) override
{
// 일어서기 처리
ACharacter* Character = Cast<ACharacter>(&Avatar);
UAnimInstance* AnimInstance =
Character->GetMesh()->GetAnimInstance();
AnimInstance->Montage_Play(StandUpMontage);
}
protected:
UPROPERTY(EditAnywhere)
UAnimMontage* SitMontage;
UPROPERTY(EditAnywhere)
UAnimMontage* StandUpMontage;
};
// SmartObjectGameplayBehaviorDefinition
// Slot의 BehaviorDefinitions에 추가하는 에셋
UCLASS()
class USmartObjectGameplayBehaviorDefinition
: public USmartObjectBehaviorDefinition
{
// GameplayBehavior 클래스 참조
UPROPERTY(EditAnywhere)
TSubclassOf<UGameplayBehavior> GameplayBehaviorConfig;
};
// Use 시 자동 실행
// SOSubsystem->Use() 호출 시
// → BehaviorDefinition에서 GameplayBehavior 생성
// → Trigger() 자동 호출
// → 완료 시 EndBehavior() 호출GameplayBehavior의 Trigger()가 true를 반환하면 행동이 시작된 것입니다. 비동기 행동(애니메이션 재생 등)은 완료 시점에 EndBehavior()를 명시적으로 호출해야 Slot이 해제됩니다.
Smart Object 검색 최적화
공간 쿼리와 캐싱 전략
Smart Object 검색은 태그 필터링과 공간 쿼리를 결합합니다. 다수의 Smart Object가 있는 환경에서 효율적인 검색을 위한 최적화 기법을 살펴봅니다.
// 공간 기반 검색 최적화
// SmartObjectSubsystem은 내부적으로
// 공간 분할 구조를 사용하여 빠른 검색 지원
// 검색 범위 제한
FSmartObjectRequest Request;
Request.QueryBox = FBox(
PawnLocation - FVector(1500),
PawnLocation + FVector(1500));
// 작은 범위 → 빠른 검색
// 태그 필터링 우선
// 공간 검색보다 태그 필터가 먼저 적용되므로
// 세밀한 태그 설계가 성능에 유리
Request.Filter.ActivityRequirements.RequiredTags.AddTag(
SitTag); // 넓은 "Activity" 대신 구체적 태그 사용
// 검색 결과 캐싱
// 같은 쿼리를 반복하지 않도록
TArray<FSmartObjectRequestResult> CachedResults;
float LastSearchTime = 0.f;
void FindSmartObjectCached()
{
float CurrentTime = GetWorld()->GetTimeSeconds();
if (CurrentTime - LastSearchTime < 2.0f
&& CachedResults.Num() > 0)
{
return; // 캐시 사용
}
// 새로 검색
CachedResults.Reset();
SOSubsystem->FindSmartObjects(Request, CachedResults);
LastSearchTime = CurrentTime;
}
// 우선순위 기반 선택
// 여러 결과 중 최적 선택
FSmartObjectRequestResult SelectBest(
const TArray<FSmartObjectRequestResult>& Results,
const FVector& PawnLocation)
{
float BestScore = -1.f;
int32 BestIndex = 0;
for (int32 i = 0; i < Results.Num(); ++i)
{
FVector SlotLoc;
SOSubsystem->GetSlotLocation(
Results[i].SlotHandle, SlotLoc);
float Distance = FVector::Dist(PawnLocation, SlotLoc);
float Score = 1.0f / (Distance + 1.f);
// 가까운 오브젝트 선호
if (Score > BestScore)
{
BestScore = Score;
BestIndex = i;
}
}
return Results[BestIndex];
}검색 결과를 2~3초 캐싱하면 같은 프레임에 여러 AI가 동시에 검색해도 성능 영향이 적습니다. Claim 실패 시에만 캐시를 무효화하고 재검색하세요.
핵심 요약
- Slot 라이프사이클은 Free → Claimed → Occupied → Free이며, Claim이 동시 접근을 방지한다
- SmartObjectSubsystem의 Find → Claim → Use → Release API로 전체 흐름을 제어한다
- GameplayBehavior의 Trigger()/EndBehavior()로 Smart Object 사용 행동을 정의한다
- 검색 범위 제한과 결과 캐싱으로 다수의 Smart Object 환경에서도 효율적으로 동작한다
도전 과제
배운 내용을 직접 실습해보세요
Claim -> Use -> Release 전체 흐름을 코드로 구현하세요. ClaimSmartObject()로 슬롯을 예약하고, UseSmartObject()로 사용 시작, ReleaseSmartObject()로 해제하는 과정을 확인합니다.
하나의 Smart Object에 3개 이상의 AI가 동시에 Claim을 시도하는 상황을 만드세요. 선착순 예약이 올바르게 작동하고, 실패한 AI가 대안을 찾는 Fallback 로직을 구현합니다.
USmartObjectBehaviorDefinition을 상속하여 커스텀 사용 행동(물건 줍기, 레버 당기기)을 정의하세요. GameplayAbility와 연동하여 Smart Object 사용 시 특정 어빌리티가 발동되도록 합니다.