최종 프로젝트 - AI Enemy 구현

정혜창·2025년 5월 8일

내일배움캠프

목록 보기
52/64

AIController, BehaviorTree, BehaviorTreeComponent 비유, 상관관계, 작동흐름

  • 비유
    • AIController = 장난감의 두뇌
    • BehaviorTree = 장난감을 움직이는 설계도
    • BehaviorTreeComponent = 두뇌에 부착하는 에드온 기능같은 느낌. BehaviorTree를 해석하여 실행
  • 작동 흐름
    • 에디터에서 UBehaviorTree를 만든다
    • AIController가 실행될 때 RunBehaviorTree를 호출
      • 주로 OnPossess에서 RunBehaviorTree(BehaviorTree) 코드를 작성
    • 이때 내부적으로 BehaviorTreeComponent가 활성화
    • BehaviorTreeComponent가 트리 노드들을 해석하고 실행한다.
  • 보통 구조 예시
    AEnemyAIController::AEnemyAIController()
    {
        BehaviorTreeComponent = CreateDefaultSubobject<UBehaviorTreeComponent>(TEXT("BehaviorTreeComponent"));
        BlackboardComponent = CreateDefaultSubobject<UBlackboardComponent>(TEXT("BlackboardComponent"));
    }
    
    void AEnemyAIController::BeginPlay()
    {
        Super::BeginPlay();
    
        if (BehaviorTree && BehaviorTree->BlackboardAsset)
        {
            UseBlackboard(BehaviorTree->BlackboardAsset, BlackboardComponent);
            RunBehaviorTree(BehaviorTree);  // 비헤비어 트리 컴포넌트가 실행됨
        }
    }
    

UAIPerceptionComponent, UAISenseConfig_Sight 상관관계와 의미

  • UAIPerceptionComponent
    • AI의 감각 시스템 전체를 관리하는 컴포넌트
    • 여러감각을 통합적으로 다룰 수 있음
    • AAIController에 부착하여, AI가 어떤 감각 정보를 인식할지 결정
  • UAISenseConfig_Sight
    • AI의 시각 감각에 대한 설정 정보를 담는 구성 객체
    • SightRadius, LoseSightRadius, PeripheralVistionAngleDegrees, DetectionByAffiliation 등을 정의
    • UAIPerceptionComponent에 등록 되어야 실제로 사용가능
  • 보통 구조 예시
// 보통 AIController에서 다음과 같이 설정합니다:

SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("Sight Config"));

SightConfig->SightRadius = 1000.0f;
SightConfig->LoseSightRadius = 1200.0f;
SightConfig->PeripheralVisionAngleDegrees = 90.0f;
SightConfig->SetMaxAge(5.0f);
SightConfig->DetectionByAffiliation.bDetectEnemies = true;
SightConfig->DetectionByAffiliation.bDetectNeutrals = true;
SightConfig->DetectionByAffiliation.bDetectFriendlies = true;

GetPerceptionComponent()->ConfigureSense(*SightConfig);
GetPerceptionComponent()->SetDominantSense(SightConfig->GetSenseImplementation());
  • 비유
    • UAISenseConfig_Sight는 "눈을 어떻게 사용할 것인지"에 대한 매뉴얼
    • UAIPerceptionComponent는 그 매뉴얼을 보고 실제로 눈을 사용하는 AI의 두뇌

몬스터가 종류가 많기 때문에 Sight 설정 정보를 DataTable에서 가져오는 방식이 좋을 것 같았음

  • DataTable 만드는법 리마인드
    1. 에디터에서 C++ 클래스를 만든다. (None)

    2. 아래와 같이 DataTable에 들어갈 Row(행)을 구조체로 만든다

      #pragma once
      
      #include "CoreMinimal.h"
      #include "Engine/DataTable.h" // FTableRowBase 정의되어 있음
      #include "MonsterSightData.generated.h"
      
      USTRUCT(BlueprintType)
      struct FMonsterSightData : public FTableRowBase
      {
          GENERATED_BODY()
      
          UPROPERTY(EditAnywhere, BlueprintReadWrite)
          float SightRadius;
      
          UPROPERTY(EditAnywhere, BlueprintReadWrite)
          float LoseSightRadius;
      
          UPROPERTY(EditAnywhere, BlueprintReadWrite)
          float PeripheralVisionAngle;
      };
      
    3. 사용하고 싶은 클래스에 UDataTable* TableName 이런식으로 만들어서 에디터에서 할당시킬 수 있도록 Reflection 시킨다.

    4. 자식클래스에서도 사용이 가능하도록 하고 싶으면 DefaultOnly로 설정하는 것이 바람직하다.

      1. EditAnywhrere 하면 인스턴스에서도 Table을 설정을 할 수 있기때문에 다르게 참조할 수 있는 위험성이 높아짐
    5. 아래와 같이 코드를 작성

      // MonsterAIController.h
      #pragma once
      
      #include "CoreMinimal.h"
      #include "AIController.h"
      #include "Perception/AIPerceptionComponent.h"
      #include "Perception/AISenseConfig_Sight.h"
      #include "MonsterSightData.h"
      #include "MonsterAIController.generated.h"
      
      UCLASS()
      class AMonsterAIController : public AAIController
      {
          GENERATED_BODY()
      
      public:
          AMonsterAIController();
      
      protected:
          virtual void BeginPlay() override;
      
          void LoadSightDataFromTable();
      
      protected:
          UPROPERTY(EditDefaultsOnly, Category = "AI")
          UDataTable* SightDataTable;
      
          UPROPERTY(EditDefaultsOnly, Category = "AI")
          FName MonsterId;
      
          UPROPERTY()
          UAIPerceptionComponent* PerceptionComponent;
      
          UPROPERTY()
          UAISenseConfig_Sight* SightConfig;
      };
      
      // MonsterAIController.cpp
      #include "MonsterAIController.h"
      
      AMonsterAIController::AMonsterAIController()
      {
          PerceptionComponent = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("PerceptionComponent"));
          SetPerceptionComponent(*PerceptionComponent);
      
          SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("SightConfig"));
          SightConfig->DetectionByAffiliation.bDetectEnemies = true;
          SightConfig->DetectionByAffiliation.bDetectFriendlies = true;
          SightConfig->DetectionByAffiliation.bDetectNeutrals = true;
      
          PerceptionComponent->ConfigureSense(*SightConfig);
          PerceptionComponent->SetDominantSense(SightConfig->GetSenseImplementation());
      }
      
      void AMonsterAIController::BeginPlay()
      {
          Super::BeginPlay();
          LoadSightDataFromTable();
      }
      
      void AMonsterAIController::LoadSightDataFromTable()
      {
          if (SightDataTable == nullptr)
          {
              UE_LOG(LogTemp, Warning, TEXT("SightDataTable is not assigned!"));
              return;
          }
      
          const FMonsterSightData* SightRow = SightDataTable->FindRow<FMonsterSightData>(MonsterId, TEXT("MonsterSight"));
          if (SightRow && SightConfig)
          {
              SightConfig->SightRadius = SightRow->SightRadius;
              SightConfig->LoseSightRadius = SightRow->LoseSightRadius;
              SightConfig->PeripheralVisionAngleDegrees = SightRow->PeripheralVisionAngleDegrees;
              SightConfig->SetMaxAge(SightRow->SenseInterval);
          }
          else
          {
              UE_LOG(LogTemp, Warning, TEXT("No matching row found for MonsterId: %s"), *MonsterId.ToString());
          }
      }
    6. 자식클래스에서 DataTable 사용 예시

      // TentacleAIController.h
      #pragma once
      
      #include "CoreMinimal.h"
      #include "MonsterAIController.h"
      #include "TentacleAIController.generated.h"
      
      UCLASS()
      class ATentacleAIController : public AMonsterAIController
      {
          GENERATED_BODY()
      
      public:
          ATentacleAIController();
      };
      // TentacleAIController.cpp
      #include "TentacleAIController.h"
      
      ATentacleAIController::ATentacleAIController()
      {
          MonsterId = FName("Tentacle");
      }

BehaviorTreeComponent, BlackboardComponent는 왜 생성만 하고 부착하지 않는가

  • UBehaviorTreeComponent, UBlackboardComponent는 ActorComponent지만 SceneComponent는 아니다.
  • 즉, 공간상의 위치가 필요없는 논리 컴포넌트이기 때문에 부착과정 생략
  • SetPerceptionComponent(*AIPerceptionComponent);
    • PerceptionComponent의 경우 컴포넌트를 생성한 후, 컨트롤러가 사용할 PerceptionComponent를 알려주는, 즉 등록하는 절차가 필요함
    • 그렇지 않으면 GetPerceptionComponent() 호출 시 nullptr 반환할 가능성이 있음.
    • StimuliSource 등록 등 기능이 동작하지 않을 가능성이 있다.

DetectionByAffiliation

  • AIPerception 의 감지 대상 필터링을 위한 설정.
  • true면 감지를 함, false면 무시
  • 그럼 “적”, “아군”, “중립” 은 어떻게 결정?
    • 단순히 Tag나 이름으로 되는게 아니라, FAISenseAffiliationFilter는 TeamID 기반으로 판단

      GetPerceptionComponent()->SetSenseImplementation(UAISense_Sight::StaticClass());
      
      // 예: AIController에서 팀 ID 설정
      SetGenericTeamId(FGenericTeamId(1)); // 예
      PlayerCharacter->SetGenericTeamId(FGenericTeamId(0));
    • 관계 판별 로직

      ETeamAttitude::Type AAIController::GetTeamAttitudeTowards(const AActor& Other) const
    • 이 함수는 AI와 대상 Actor의 TeamID를 비교해서 어떤 관계인지 자동으로 리턴함.

(UBlackboardComponent*&) BlackboardComponent

	if (IsValid(BehaviorTree))
	{
		UseBlackboard(BehaviorTree->BlackboardAsset, (UBlackboardComponent*&)BlackboardComponent);
		RunBehaviorTree(BehaviorTree);

		LOG(TEXT("AIController Possess"));
	}
}
  • 여기서 UseBlackboard의 두 번째 인자로 (UBlackboardComponent*&) 로 강제 타입 캐스팅을 하고 있음
  • 이유는 BlackboardComponent를 TObjectPtr 로 선언했기 때문
    • TObjectPtr는 Unreal의 스마트포인터로, 직접 포인터와는 타입이 다르다.
      • 굳이 TObjectPtr를 쓰는 이유는 GC 때문. 리플렉션 하지않은 직접포인터는 GC 시스템이 추적하지 못할 수도 있다.
      • 즉, TObjectPtr를 쓰면 개발자가 UPROPERTY()를 쓰지 않아도 GC 추적 가능성을 높이고, 코드 안정성이 향상됌.
    • 하지만 UseBlackboard()는 UBlackboardComponent*& 즉, “UBlackboardComponent에 대한 포인터 참조를 요구한다. 따라서 직접적 호환이 안됌
    • 그래서 강제로 타입 캐스팅을 한 것.
profile
Unreal 1기

0개의 댓글