BT를 사용하는 인스턴스의 데이터 캐싱

  • AI 캐릭터들이 BT를 사용하면서, 인스턴스 별로 가져야 할 데이터가 있다
    • Target과의 거리를 기록해두어야 한다든가..
    • Timer를 재기 위해 체크하는 현재 남은 시간이라든가..
    • 즉, 캐싱(Cache)을 해야 하는 데이터가 있을 것이다

Member 변수로 저장해도 되는가?

UCLASS()
class MYGAME_API UBTTask_MyCustomTask : public UBTTaskNode
{
    GENERATED_BODY()

public:
  // ...

private:
  // ...

  // 남은 시간
  float ElapsedTimer = 0;
}

EBTNodeResult::Type UBTTask_MyCustomTask::TickTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory, float DeltaSeconds)
{
    Super::TickTask(OwnerComp, NodeMemory, DeltaSeconds);
    // ...

    // 이렇게 해도 되나??
    ElapsedTimer -= DeltaSeconds;

    // ...
    return EBTNodeResult::InProgress;
}
  • BT를 사용하는 Instance가 ElapsedTimer를 체크하고 특정 행동을 해야한다고 하자
  • 위와 같이 Task의 멤버변수로 두고 관리하는 게 맞을까?
    • 당연히 안된다
  • 여러 AI가 같은 BTTask 인스턴스를 공유하기 때문
    • member 변수 값이 덮어씌울 수 있다
    • 이로써 다른 AI의 행동에 의도치 않은 영향을 미칠 수 있다

올바른 방법은?

Blackboard

  • Blackboard의 데이터는 객체 별로 관리되므로 안전한 방법이다
    • 단, 캐싱해야 하는 데이터가 너무 많은 경우에는 관리가 힘들어질 수 있다

Pawn/Controller

  • AI Controller나 Controlled Pawn에 필요한 변수들을 선언하고 접근할 수도 있다
    • 데이터가 원래 해당 클래스에 있다면 상관없겠지만, 상관없는 데이터를 넣어둔다면 구조적으로 별로다

NodeMemory 사용

UCLASS()
class MYGAME_API UBTTask_MyCustomTask : public UBTTaskNode
{
  GENERATED_BODY()

public:
  // NodeMemory 구조체 정의
  struct FMyTaskMemory
  {
    AActor* TargetActor;
    float StartTime;
    bool bIsInitialized;
        
    FMyTaskMemory()
    {
      TargetActor = nullptr;
      StartTime = 0.0f;
      bIsInitialized = false;
    }
  };

  virtual uint16 GetInstanceMemorySize() const override;
  virtual EBTNodeResult::Type ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
  virtual EBTNodeResult::Type AbortTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory) override;
};

// BTTask_MyCustomTask.cpp
uint16 UBTTask_MyCustomTask::GetInstanceMemorySize() const
{
  return sizeof(FMyTaskMemory);
}

EBTNodeResult::Type UBTTask_MyCustomTask::ExecuteTask(UBehaviorTreeComponent& OwnerComp, uint8* NodeMemory)
{
  FMyTaskMemory* MyMemory = reinterpret_cast<FMyTaskMemory*>(NodeMemory);
    
  if (!MyMemory->bIsInitialized)
  {
    // 초기화 로직
    MyMemory->TargetActor = /* 타겟 찾기 로직 */;
    MyMemory->StartTime = GetWorld()->GetTimeSeconds();
    MyMemory->bIsInitialized = true;
  }
    
  // 메모리의 데이터 사용
  if (MyMemory->TargetActor)
  {
    // 작업 수행
  }
    
  return EBTNodeResult::InProgress;
}
  • C++을 사용할 수 있다면 이것이 가장 일반적이고 안전한 방법이다
    • Blueprint로는 사용할 수 없는 듯하다
  • BT의 Task, Decorator, Service 등 여러 타입에 대해서 NodeMemory를 인스턴스 별로 관리할 수 있다
  • 복잡도에 따라 위의 3가지 중 1가지 방법을 선택해 사용하면 된다고 하는데, Blueprint로만 작업한느 환경이 아니라면 기능 의존성을 고려해 NodeMemory 기능을 구현하는 것이 가장 적절해 보인다

Categories: ,

Updated: