프로그래밍/Unreal Engine

230331_언리얼 포트폴리오(Project_Zombie_FPS) : AI Perception

이동헌 2023. 3. 31. 19:39

글 형식을 살짝 바꾸어 이제 일기 형식이 아니라 제대로 기록해보고 싶어졌다.. 검색 유입이 한두명 있던데 그분들이라도 도움이 되도록!.. 내 얕은 지식으로 인해 헷갈릴 수도 있지만 도움이 되었으면 한다.

 

오늘은 AI Perception에 청각을 추가하고, 실제 사운드들을 받아 사운드큐를 만들었다. 또, 지형에 맞는 소리를 내도록 switch문으로 사운드큐를 선택하여 출력하도록 하였다. 여기서 AI Perception을 시각과 함께 묶어서 정리해보려 한다.

 

아래는 언리얼 엔진 사용자라면 항상 참고하게 되는 공식 홈페이지의 AI Perception설명이다.

https://docs.unrealengine.com/5.0/en-US/ai-perception-in-unreal-engine/

 

AI Perception

Documents the AI Perception Component and how it is used to generate awareness for AI.

docs.unrealengine.com

이 사이트 가면 아래 내용 뿐만 아니라 더 풍부한 설명이 있다! 부족하다면 사이트를 참고해주시길..

 

TMI) 내 프로젝트에 동물과 좀비가 있고 이들의 시각은 당연히 추가하였었고, 오늘은 청각을 추가하였다.

동물은 청각데이터를 통해 적이라면 도망치고, 좀비는 청각데이터를 통해 적이라면 공격하도록 만들기 위해서이다.

정리하는 김에 시각관련과 같이 정리해야겠다.

 

우선 언리얼 엔진의 기본 AI는 움직이고 싶은 캐릭터에 AI 컨트롤러가 빙의되어 컨트롤러로 조종하는 방식이다. 여기서 핵심은 AI Perception 기능 구현에는 AI 컨트롤러 부분에 해야한다는 것. 나는 공격을 하지 않는 동물들(적을 보면 도망치는 동물)을 모두 NonAttackAnimalAIController에 구현했고, 이 컨트롤러만 재활용 하면 모든 동물별로 따로 설정해줄 필요가 없다. 매우 편안한것..ㅎㅎ

 

나는 NonAttackAnimalAIController 헤더에 다음과 같이 인공지능 시야/청각을 감지할 변수를 지정했다.

private:
    // 인공지능 시야
    class UAISenseConfig_Sight* SightConfig;
    // 인공지능 청각
    class UAISenseConfig_Hearing* HearingConfig;

그리고 NonAttackAnimalAIController의 cpp 파일이다.

#include "MyKeys.h"

#include "Perception/AIPerceptionComponent.h"
#include "Perception/AISenseConfig.h"
#include "Perception/AISenseConfig_Sight.h"
#include "Perception/AISenseConfig_Hearing.h"
#include "Perception/AIPerceptionStimuliSourceComponent.h"

#include "NonAttackAnimalAIController.h"

ANonAttackAnimalAIController::ANonAttackAnimalAIController()
{
    PrimaryActorTick.bCanEverTick = true;
    // ... 일부 생략
    
    // * 인지 컴포넌트 생성
    SetPerceptionComponent(*CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("PerceptionComponent")));
    // * 시각 생성
    SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("SightConfig"));
    // * 청각 생성
    HearingConfig = CreateDefaultSubobject<UAISenseConfig_Hearing>(TEXT("HearingConfig"));

    // * 인지 설정
    GetPerceptionComponent()->SetDominantSense(*SightConfig->GetSenseImplementation());
    GetPerceptionComponent()->ConfigureSense(*SightConfig);
    GetPerceptionComponent()->ConfigureSense(*HearingConfig);
    GetPerceptionComponent()->OnTargetPerceptionUpdated.AddDynamic(this, &ANonAttackAnimalAIController::MyPerceptionUpdate);

    // * 시야
    SightConfig->SightRadius = 3000.f;
    SightConfig->LoseSightRadius = 3200.f;
    SightConfig->PeripheralVisionAngleDegrees = 90.f;
    SightConfig->SetMaxAge(10.f);
    SightConfig->DetectionByAffiliation.bDetectEnemies = true;
    SightConfig->DetectionByAffiliation.bDetectFriendlies = true;
    SightConfig->DetectionByAffiliation.bDetectNeutrals = true;

    SightConfig->AutoSuccessRangeFromLastSeenLocation = 3100.f;

    // * 청각
    HearingConfig->HearingRange = 1500.f;
    HearingConfig->SetMaxAge(10.f);
    HearingConfig->DetectionByAffiliation.bDetectEnemies = true;
    HearingConfig->DetectionByAffiliation.bDetectFriendlies = true;
    HearingConfig->DetectionByAffiliation.bDetectNeutrals = true;

    // AI 팀
    SetGenericTeamId(TeamNum::Deer);
}

 풀어서 설명하자면 다음과 같다.

// * 인지 컴포넌트 생성
SetPerceptionComponent(*CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("PerceptionComponent")));

SetPerceptionComponent 함수를 통해 AIController에 기본적으로 있는 UAIPerceptionComponent 멤버를 초기화해준다. 이 때 참조형으로 값을 줘야하는데 CreateDefaultSubobject<Type>()함수는 Type의 포인터형으로 반환하므로 앞에 *을 붙여 주소가 아닌 값 자체를 전달해주었다.

// * 시각 생성
SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("SightConfig"));
// * 청각 생성
HearingConfig = CreateDefaultSubobject<UAISenseConfig_Hearing>(TEXT("HearingConfig"));

이건 기본적인 생성 함수이다.

// * 인지 설정
GetPerceptionComponent()->SetDominantSense(*SightConfig->GetSenseImplementation());
GetPerceptionComponent()->ConfigureSense(*SightConfig);
GetPerceptionComponent()->ConfigureSense(*HearingConfig);
GetPerceptionComponent()->OnTargetPerceptionUpdated.AddDynamic(this, &ANonAttackAnimalAIController::MyPerceptionUpdate);

SetDominantSense : 좀 더 Dominant(중점을 둘)한 감각을 정한다. 여기서는 SightConfig, 즉 시야감각에 더 집중.

ConfigureSense : 내가 멤버변수로 가지고 있다고 해도 UAIPerceptionComponent가 모른다. 이 함수를 통해 이 변수를 UAIPerceptionComponent가 관리하도록 해주는 것.

OnTargetPerceptionUpdated : PerceptionComponent에서 무언가 인지했을때 호출되는 이벤트, AddDynamic을 통해 사용자 지정 함수를 등록하였다. 해당 함수 원형은 void MyPerceptionUpdate(AActor* actor, FAIStimulus stimulus) 이다.

 

이제 시각 관련 변수를 보자.

// * 시야
SightConfig->SightRadius = 3000.f;
SightConfig->LoseSightRadius = 3200.f;
SightConfig->PeripheralVisionAngleDegrees = 90.f;
SightConfig->SetMaxAge(10.f);
SightConfig->DetectionByAffiliation.bDetectEnemies = true;
SightConfig->DetectionByAffiliation.bDetectFriendlies = true;
SightConfig->DetectionByAffiliation.bDetectNeutrals = true;
SightConfig->AutoSuccessRangeFromLastSeenLocation = 3100.f;

사실 여기는 위에 소개한(이미 알거라고 생각합니다...) 사이트에 다 나와있는 정보이다.

SightRadius : 시야 반경

LoseSightRadius : 한번 시야에 잡힌 대상이 시야에 감지되지 않기 위해 떨어져야 하는 거리

PeripheralVisionAngleDegrees : 시야각, 한쪽 눈마다의 각도이기 때문에 90.f로 지정한 이 동물의 시야각은 양쪽 합 180도

SetMaxAge(float age) : 대상을 인식한 뒤 age만큼 지속 후 없어짐.

DetectionByAffiliation.bDetectXXXX : XXXX를 인식하는지 여부(이는 AITeam과 관련되어있다)

AutoSuccessRangeFromLastSeenLocation : 마지막으로 본 대상이 이 거리안에 있으면 항상 인지가능

 

다음은 청각이다.

// * 청각
HearingConfig->HearingRange = 1500.f;
HearingConfig->SetMaxAge(10.f);
HearingConfig->DetectionByAffiliation.bDetectEnemies = true;
HearingConfig->DetectionByAffiliation.bDetectFriendlies = true;
HearingConfig->DetectionByAffiliation.bDetectNeutrals = true;

이것도 시각과 다를 것 없다. 참고로 HearingRange도 반지름이다.

 

마지막 코드이다.

// AI 팀
SetGenericTeamId(TeamNum::Deer);

이는 인지 능력으로 인식한 상대가 적인지, 아군인지, 또는 중립 캐릭터인지 인식하기 위해 TeamId를 설정해주는 함수이다.  TeamNum이라는 namespace를 MyKeys.h를 만들어 따로 써주었고, 사실 저건 그냥 정수이다.

 

아까 위에서 AddDynamic으로 인지 업데이트 시 MyPerceptionUpdate함수를 호출하도록 등록하였는데, 여기서 TeamId를 활용한다.(이건.. 나중에 할까요?호호)

 

이렇게하면 AI 컨트롤러의 기본적인 세팅은 끝났다. 간단하게 시각, 청각 범위 내에 이 감각을 자극할 것들이 감지되면 MyPerceptionUpdate함수를 호출해주는 구조인 것이다.

 

그렇다면 감각을 자극할 것이 필요한데..이는 Stimuli Source를 통해 자극을 준다. 이는 컴포넌트이므로, 나의 경우 동물이나 좀비가 플레이어를 발견해야 하니 플레이어 캐릭터에 아래와 같은 컴포넌트 멤버를 만들었다.(헤더)

private:
    // * 인공지능 관련
    UPROPERTY()
        class UAIPerceptionStimuliSourceComponent* Stimulus;

그리고 생성자의 코드는 아래와 같다.

#include "Perception/AIPerceptionStimuliSourceComponent.h"
#include "Perception/AISense_Sight.h"
//#include "Perception/AISense_Hearing.h"

APlayerCharacter::APlayerCharacter()
{
    // Set this character to call Tick() every frame.  You can turn this off to improve performance if you don't need it.
    PrimaryActorTick.bCanEverTick = true;
    // ... 생략
    
    // * 인공지능
    Stimulus = CreateDefaultSubobject<UAIPerceptionStimuliSourceComponent>(TEXT("Stimulus"));
    Stimulus->RegisterForSense(TSubclassOf<UAISense_Sight>());
    //Stimulus->RegisterForSense(TSubclassOf<UAISense_Hearing>());
    Stimulus->RegisterWithPerceptionSystem();
}

Stimulus 생성 후 함수는 간단하다.

RegisterForSense : 이 Stimulus를 소유한 액터가 어떤 감각에 대해 자극할 것인지 정함. 여기선 시야이다.

* 여기서 Hearing 감각에 대해 자극을 주도록 등록하면 이를 소유한 액터가 항상 청각자극을 주게 되는데 이는 원하는 방향이 아니었다. 나는 AActor::MakeNoise함수를 통해 AI의 청각을 자극할 수 있기 때문에 실제 소리가 발생할때마다 MakeNoise를 호출하여 청각 자극을 주도록 함!

RegisterWithPerceptionSystem : 이제 이 Stimulus를 소유한 액터가 감각에 대한 자극 소스로 등록

 

이렇게 하면 AI 디버깅 화면에 다음과 같이 보인다.

위에 노란 원이 청각, 초록 원이 시각, 빨간 원이 시각으로 감지된 대상이 감지되지 않기 위해 벗어나야 하는 범위이다.

아, 디버깅 모드를 켜는 키는 '(따옴표), 여기서 4번을 누르면 AI Perception 디버깅 라인을 그려준다.

또, 이 원들의 색깔은 마음에 드는 색으로 바꿀 수 있다! 하지만 굳이..안바꿀 것 같다...