본문 바로가기
Unreal 게임 개발/Unreal Tool 활용

Unreal AIController 제작

by daisy0461 2024. 10. 7.

이번에 BehaviorTree에 쓰일 AIContoroller(AIC)를 직접 제작해보았다.

 

UPawnSensingComponent에서 UAIPerceptionComponent로 변경

UPawnSensingComponent: 간단한 AI 감지 시스템에 필요한 경우 사용된다.

UAIPerceptionComponent: PawnSensingComponent에서 제공하는 시각, 청각 감지 기능뿐만 아니라 손상 등 다양한 감지 기능을 지원한다. 

 

이렇기에 이후에 더 확장성이 높은 UAIPerceptionComponent로 바꾸었다.

AIPerceptionComponent = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("AI Perception"));

AIC의 생성자에 다음과 같이 추가했다.

 

OnPossess

https://dev.epicgames.com/documentation/en-us/unreal-engine/API/Runtime/AIModule/AAIController/OnPossess

위 도큐먼트를 들어가보면 OnPossess의 매개변수인 InPawn은 AI Controller를 소유하고 있는 Pawn이 들어온다.

Enemy AIC의 OnPossess를 간단하게 아래와 같이 작성했다.

void ABaseEnemyAIController::OnPossess(APawn* InPawn)
{
    Super::OnPossess(InPawn);

    UE_LOG(LogTemp, Display, TEXT("OnPossess Call"));
    Enemy = Cast<AEnemy>(InPawn);
    if (!Enemy)
    {
        UE_LOG(LogTemp, Display, TEXT("AI Controller Cast Fail"));
        return;
    }
    Enemy->OnEnemyDeath.AddDynamic(this, &ABaseEnemyAIController::OnEnemyDied);

    if (AIPerceptionComponent)
    {
        AIPerceptionComponent->OnPerceptionUpdated.AddDynamic(this, &ABaseEnemyAIController::OnPerceptionUpdated);
    }

    BlackboardComponent = Enemy->GetBlackboardComponent();
    if (!BlackboardComponent)
    {
        UE_LOG(LogTemp, Display, TEXT("Blackboard Component cann't find"));
        return;
    }
    BlackboardAsset = Enemy->GetBlackboardComponent()->GetBlackboardAsset();
    if(!BlackboardAsset){
        UE_LOG(LogTemp, Display, TEXT("BlackboardAsset cann't find"));
    }

	//UseBlackboard를 사용하지 않고 RunBehavior를 사용하면 BlackboardKey에 값이 들어가지 않음.
    //RunBehaviorTree 전, 후 상관없이 사용해야 정상적으로 값이 들어간다.
    UseBlackboard(BlackboardAsset, BlackboardComponent);
    RunBehaviorTree(Enemy->GetBehaviorTree());
    
    SetEnemyState(EEnemyState::EES_Passive);
    FName AttackRadiusKeyName = TEXT("AttackRadius");
    FName DefendRadiusKeyName = TEXT("DefendRadius");
    BlackboardComponent->SetValueAsFloat(AttackRadiusKeyName, Enemy->GetAttackRadius());
    BlackboardComponent->SetValueAsFloat(DefendRadiusKeyName, Enemy->GetDefendRadius());
}

위 코드에서 중요한 점이 UseBlackboard이다.

여기서 작성된 AIC는 BlackboardKey를 사용하는데 위치가 중요하다.

RunBehavior의 전 후는 상관 없지만 가장 하단에 있는
BlackboardComponent->SetValueAs~~~와 같은 BlackboardKey값을 정해줄 때 보다는 먼저 선언이 되어있어야
Blackboard에 값이 들어간다.

처음 AIC를 작성하는 것이라서 UseBlackboard의 존재를 모르고 SetValue만 사용했다가 많이 헤맸다.

 

OnPerceptionUpdated

void ABaseEnemyAIController::OnPerceptionUpdated(const TArray<AActor*>& UpdatedActors)
{
    //UE_LOG(LogTemp, Display, TEXT("OnPerceptionUpdated called. Updated Actors count: %d"), UpdatedActors.Num());
    
    for (AActor* Actor : UpdatedActors)
    {
        GetPerceptionInfo(Actor);
    }
}

 

OnPerceptionUpdated에 대해서 설명하려면 좀 길다.
AIPerceptionComponent에 있는 ProcessStimuli함수가 매 프레임(Tick과 비슷한 형식으로)마다 실행이 된다.

이때 새로운 자극(Stimuli)이 들어오면 OnTargetPerceptionComponent가 실행된다.

void ABaseEnemyAIController::OnTargetPerceptionUpdated(AActor* Actor, FActorPerceptionUpdateInfo PerceptionInfo)
{
    if (PerceptionInfo.Stimulus.WasSuccessfullySensed())
    {
        UE_LOG(LogTemp, Display, TEXT("Actor %s was sensed using sense %d"), *Actor->GetName(), PerceptionInfo.Stimulus.Type);
    }
    else
    {
        UE_LOG(LogTemp, Display, TEXT("Actor %s was no longer sensed using sense %d"), *Actor->GetName(), PerceptionInfo.Stimulus.Type);
    }
}

예시로 이렇게 생겼다.

매개변수로 AActor*와 어떤 자극이 들어왔는지(FActorPerceptionUpdateInfo)가 들어온다.

FActorPerceptionUpdateInfo는 어떠한 자극(시각, 청각, 손상)을 나타낸다.
그렇다면 자극이 총 5가지가 들어왔다고 가정을 하면 OnTargetPerception은 5번 호출이 된다는 의미이다.

당연하겠지만 선언되어있지 않다면 실행하지 않는다.


그리고 나서 OnPerceptionUpdated가 실행된다. OnTargerPerception이 있던 없던 자극을 한번에 TArray에 모아서 매개변수로 전달된다.

여기서도

FActorPerceptionBlueprintInfo PerceptionInfo;
AIPerceptionComponent->GetActorsPerception(Actor, PerceptionInfo);

와 같이 개별적으로 어떤 자극을 통해 인식되었는지 확인 가능하다. 또한

WasSuccessfullySensed()

해당 함수를 통해서 자극이 현재 감지된 상태인지 아닌지 확인할 수 있다.

위에 말이 좀 이상한 것처럼 보일 수 있다.

OnPerceptionUpdated는 자극을 TArray에 모아서 매개변수로 전달하는 것인데 왜 다시 감지된 것인지 확인을 하지?
왜냐면 자극이 활성화 되었다가 다시 사라질 수 있기 때문이다. 간단하게 AI가 감지했지만 지금은 못하는 상황인 것이다.
예를 들어 Sight(시각)로 A를 감지하고 있다가 A가 AI의 시야에서 벗어나면 위 함수의 실행 결과는 false가 나오게 된다.

 

위와 같이 자극을 모아서 한번에 처리하는 것이 OnPerceptionUpdated다.

OnTargetPerceptionUpdated은 그럼 왜 쓰냐?
OnTargetPerceptionUpdated는 즉시 실행되는 콜백함수이기 때문이다.
감각 자극이 발생할 때 즉각적으로 AI가 반응을 할 수 있도록 해준다. 하지만 OnPerceptionUpdated의 GetActorsPerception은 자극 정보를 수동으로 요청하는 방법이기에 AI가 주기적으로 상태를 확인해야하기 때문에 OnTargetPerceptionUpdated보단 반응 속도가 떨어질 수 있다.

 

혹시 AIController를 더 만들다가 추가적으로 공부하거나 작성해야할 내용이 있다면 작성해야겠다.