Deeper Learning

[UE5] AI Perception (C++) 본문

Game Development/Unreal Engine

[UE5] AI Perception (C++)

Dlaiml 2024. 8. 25. 12:16

개발 중인 개인 프로젝트에서 정해진 구간을 순찰하다가 플레이어를 발견하면 추적하고 공격하는 AI를 개발하면서 학습한 AI Percetion에 대한 포스팅이다.

 

 

대부분 잠입 액션게임, 오픈월드 게임에서 적은 시야에 플레이어가 들어왔을 때에만 반응한다. 적이 정보를 얻는 매개가 시야, 소리, 대미지라면 훨씬 더 현실적인 AI를 만들 수 있다.

 

언리얼 엔진에서는 이를 쉽게 구현할 수 있도록 AIPerceptionComponent를 제공한다. AI Controller의 헤더에 멤버로 AIPerceptionComponent 를 추가하자

 

UPROPERTY(EditDefaultsOnly, BlueprintReadOnly)
class UAIPerceptionComponent* AIPerception;

 

 

앞서 언급한 시야(Sight) / 소리(Hearing) / 대미지(Damage) 모두 AISenseConfig로 클래스가 존재한다. (+Touch, Prediction, Team Sense)

AISenseConfig_{Sense}는 각 Sense의 설정을 담당한다. 

class UAISenseConfig_Sight* SightConfig;
class UAISenseConfig_Hearing* HearingConfig;
class UAISenseConfig_Damage* DamageSenseConfig;

 

Constructor에서 AI Perception Component를 생성하고 Perception Component로 지정하였다.

SightConfig에서 시야 범위, 인지를 멈추는 시야범위, 시야각, 정보의 유효기간, 자동으로 플레이어를 계속 인지하는 최소 거리를 설정하였다.

HearingConfig에서는 Noise 인지 거리, 정보 유효기간을 설정하였다. 

 

DetectionByAffiliation은 Enemies, Neutrals, Friendlies로 태그한 액터를 인지할지 선택할 수 있도록 하는 C++에서만 사용가능한 기능이다. (아직 세팅하지 않았기 때문에 모든 종류의 액터를 감지하도록 true로 설정하였다)

 

ABaseAIController::ABaseAIController()
{
    AIPerception = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("AIPerception"));
    SetPerceptionComponent(*AIPerception);

    // Sight Config
    SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("SightConfig"));
    SightConfig->SightRadius = 800.f;
    SightConfig->LoseSightRadius = 1200.f; 
    SightConfig->PeripheralVisionAngleDegrees = 60.f; 
    SightConfig->SetMaxAge(5.f); 
    SightConfig->AutoSuccessRangeFromLastSeenLocation = -1.f;

    // Detect only specific actors 
    SightConfig->DetectionByAffiliation.bDetectEnemies = true;
    SightConfig->DetectionByAffiliation.bDetectNeutrals = true;
    SightConfig->DetectionByAffiliation.bDetectFriendlies = true;

    AIPerception->ConfigureSense(*SightConfig);
    AIPerception->SetDominantSense(SightConfig->GetSenseImplementation());
    
    HearingConfig = CreateDefaultSubobject<UAISenseConfig_Hearing>(TEXT("HearingConfig"));
    HearingConfig->HearingRange = 500.f;
    HearingConfig->SetMaxAge(3.f);

    HearingConfig->DetectionByAffiliation.bDetectEnemies = true;
    HearingConfig->DetectionByAffiliation.bDetectNeutrals = true;
    HearingConfig->DetectionByAffiliation.bDetectFriendlies = true;
    AIPerception->ConfigureSense(*HearingConfig);
    // ...
    
    AIPerception->OnPerceptionUpdated.AddDynamic(this, &ABaseAIController::PerceptionUpdated);

    
}

 

에디터에서 확인해보면 정상적으로 AI Perception과 Sense Config가 등록된 것을 볼 수 있다. 

 

마지막에는 AIPerception의 OnPerceptionUpdated 델리게이트에 PerceptionUpdated 함수를 Bind 하였다. PerceptionUpdated 함수는 OnPerceptionUpdated의 시그니처와 동일하게 AActor 포인터의 TArray를 input으로 받도록 하였다.

 

UFUNCTION()
void PerceptionUpdated(const TArray<AActor*>& UpdatedActors);

 

AI의 Perception이 업데이트될 때마다 호출되는 델리게이트에 Bind 한 콜백 PerceptionUpdated에서는 설정한 Sense들이 감지되었는지 확인하고 이를 처리한다.

 

void ABaseAIController::PerceptionUpdated(const TArray<AActor*>& UpdatedActors)
{
	for (AActor* UpdatedActor : UpdatedActors)
	{
		FAIStimulus AIStimulus;
		AIStimulus = CanSenseActor(UpdatedActor, EAIPerceptionSense::EPS_Sight);
		if (AIStimulus.WasSuccessfullySensed())
		{
			HandleSensedSight(UpdatedActor);
		}
		AIStimulus = CanSenseActor(UpdatedActor, EAIPerceptionSense::EPS_Hearing);
		if (AIStimulus.WasSuccessfullySensed())
		{
			HandleSensedHearing(AIStimulus.StimulusLocation);
		}
		AIStimulus = CanSenseActor(UpdatedActor, EAIPerceptionSense::EPS_Damage);
		if (AIStimulus.WasSuccessfullySensed())
		{
			HandleSensedDamage(UpdatedActor);
		}
	}
}

 

Actor의 Perception을 확인하는 함수는 AI Perception Component의 GetActorsPerception 함수인데 Perception 관련 정보가 들어있는 구조체 FActorPerceptionBlueprintInfo를 출력 매개변수로 사용한다.

 

bool UAIPerceptionComponent::GetActorsPerception(AActor* Actor, FActorPerceptionBlueprintInfo& Info)
{
	bool bInfoFound = false;
	if (Actor != nullptr && Actor->IsPendingKillPending() == false)
	{
		const FActorPerceptionInfo* PerceivedInfo = GetActorInfo(*Actor);
		if (PerceivedInfo)
		{
			Info = FActorPerceptionBlueprintInfo(*PerceivedInfo);
			bInfoFound = true;
		}
	}

	return bInfoFound;
}

 

직접 정의한 Enum인 AIPerceptionSense와 동일한 Sense가 감지되었으면 해당 정보를 FAIStimulus로 반환하는 CanSenseActor 함수를 살펴보자.

 

앞서 설명한 GetActorsPerception로 Perception이 업데이트된 액터들의 정보를 저장하고 LastSensedStimuli 배열을 순회하면서 Sense가 찾고자 하는 QuerySense와 동일하면 FAIStimulus를 반환한다.

 

FAIStimulus ABaseAIController::CanSenseActor(AActor* Actor, EAIPerceptionSense AIPerceptionSense)
{
	FActorPerceptionBlueprintInfo ActorPerceptionBlueprintInfo;
	FAIStimulus ResultStimulus;

	AIPerception->GetActorsPerception(Actor, ActorPerceptionBlueprintInfo);

	TSubclassOf<UAISense> QuerySenseClass;
	switch (AIPerceptionSense)
	{
	case EAIPerceptionSense::EPS_None:
		break;
	case EAIPerceptionSense::EPS_Sight:
		QuerySenseClass = UAISense_Sight::StaticClass();
		break;
	case EAIPerceptionSense::EPS_Hearing:
		QuerySenseClass = UAISense_Hearing::StaticClass();
		break;
	case EAIPerceptionSense::EPS_Damage:
		QuerySenseClass = UAISense_Damage::StaticClass();
		break;
	case EAIPerceptionSense::EPS_MAX:
		break;
	default:
		break;
	}

	TSubclassOf<UAISense> LastSensedStimulusClass;

	for (const FAIStimulus& AIStimulus : ActorPerceptionBlueprintInfo.LastSensedStimuli)
	{
		LastSensedStimulusClass = UAIPerceptionSystem::GetSenseClassForStimulus(this, AIStimulus);


		if (QuerySenseClass == LastSensedStimulusClass)
		{
			ResultStimulus = AIStimulus;
			return ResultStimulus;
		}

	}
	return ResultStimulus;
}

 

다시 PerceptionUpdated 함수를 보면 반환한 AIStimulus의 WasSuccessFullySensed 함수로 Sense의 인지여부를 확인하고 값이 참이면 해당 Actor에 대해 로직을 수행하는 HandleSensedSight 함수를 호출한다.

// PerceptionUpdated Function
AIStimulus = CanSenseActor(UpdatedActor, EAIPerceptionSense::EPS_Sight);
if (AIStimulus.WasSuccessfullySensed())
{
    UE_LOG(LogTemp, Warning, TEXT("Sight Sensed!"));
    HandleSensedSight(UpdatedActor);
}

 

마지막으로 HandleSensed{Sense} 함수는 각 인지한 Sense에 따라 Enemy의 State를 변경하는 함수이다. State는 Blackboard의 keys값으로 Behavior Tree의 Decorator에서 이에 따라 Enemy의 행동을 결정한다.

 

Hearing Sense에서는 AIStimulus의 StimulusLocation 값을 사용하여 적이 소음이 발생한 위치를 알 수 있도록 하였다.

 

 

void ABaseAIController::HandleSensedSight(AActor* Actor)
{
	// ...
	if (bConvertToAttack && PlayerCharacter != nullptr && PlayerCharacter == Actor)
	{
		SwitchToAttackState(Actor);
	}
	// ...
}

// PerceptionUpdated
HandleSensedHearing(AIStimulus.StimulusLocation);
//	

// 
void ABaseAIController::HandleSensedHearing(FVector NoiseLocation)
{
	// ...
	SwitchToInvestigateState(NoiseLocation);
 	// ...
}
// ...

 

정리해 보면

1. AIController 클래스에서 AIPerceptionComponent와 AISense_{Sense} 를 세팅한다.
2. AI의 Perception이 업데이트될 때 호출되는 OnPerceptionUpdated 델리게이트에 직접 정의한 콜백함수를 Bind한다.
3. 콜백함수에서는 Perception이 Update 된 Actors의 배열을 순회하면서 GetActorsPerception으로 정보를 받아온다. 
4. 마지막에 감지한 Sense들의 배열인 LastSensedStimuli를 순회하면서 요청한 Sense와 동일하면 해당 Sense의 정보인 FAIStimulus를 반환한다.
5. FAIStimulus 구조체에서 필요한 정보들 추출하여 해당 Sense를 핸들링하는 함수에 전달한다. (Sight - 발견한 Actor, Hearing - Noise 발생 위치)
6. 핸들링 함수에서는 주어진 정보를 참고하여 Blackboard의 Value를 업데이트한다. (State를 바꾸거나 추적하는 Actor를 변경하는 등)
7. Behavior Tree의 Blackboard Based Condition Decorator는 업데이트된 Blackboard를 기반으로 AI의 행동을 결정한다. (소리가 난 위치로 이동 / 시야에 보인 적을 추적 및 공격)

 

AISense_Hearing을 사용하여 소리가 난 위치로 적이 이동하도록 하였고 AISense_Sight를 사용하여 시야에 발각된 액터를 추적하고 공격하도록 설정하였다.

 

에디터에서 apostrophe(') 키를 누르면 AI를 디버깅할 수 있고 Numpad 4를 눌러 Perception을 활성화시키면 AI Percetion을 시각적으로 디버깅할 수 있다. (Numpad가 없다면 ProjectSetting 또는 DefaultEngine.ini에서 GameplayDebugger 키바인딩을 수정하면 된다)

 

 

Reference

[0] https://dev.epicgames.com/documentation/en-us/unreal-engine/artificial-intelligence?application_version=4.27

[1] https://dev.epicgames.com/documentation/en-us/unreal-engine/ai-perception?application_version=4.27

[2] https://dev.epicgames.com/documentation/en-us/unreal-engine/ai-debugging?application_version=4.27

[3] https://www.youtube.com/watch?v=gsyZdKYAT_4&list=PLNwKK6OwH7eW1n49TW6-FmiZhqRn97cRy&index=3

 

Comments