언리얼엔진(UE)

[UE] 몬스터 AI 구현 #6 (Perception에 따른 상태 변화)

2h1824 2025. 2. 28. 22:57

1. AI관련 Enum 생성

// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "AIEnum.generated.h"

UENUM(BlueprintType)
enum class EMovementSpeed : uint8
{
	Idle	UMETA(DisplayName = "Idle"),
	Walking	UMETA(DisplayName = "Walking"),
	Jogging	UMETA(DisplayName = "Jogging"),
	Sprinting	UMETA(DisplayName = "Sprinting")
};

UENUM(BlueprintType)
enum class EAIState : uint8
{
	Passive		UMETA(DisplayName = "Passive"),
	Attacking	UMETA(DisplayName = "Attacking"),
	Frozen		UMETA(DisplayName = "Frozen"),
	Investigating	UMETA(DisplayName = "Investigating"),
	Dead		UMETA(DisplayName = "Dead")
};

UENUM(BlueprintType)
enum class EAISense : uint8
{
	None		UMETA(DisplayName = "None"),
	Sight		UMETA(DisplayName = "Sight"),
	Hearing		UMETA(DisplayName = "Hearing"),
	Damage		UMETA(DisplayName = "Damage")
};
  • 이동 속도, 상태, 감각 관련 Enum 정의

2. EnemyAIController 수정

EnemyAIController.h

// Fill out your copyright notice in the Description page of Project Settings.
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once

#include "CoreMinimal.h"
#include "AIController.h"
#include "AIEnum.h"
#include "EnemyAIController.generated.h"

/**
 * 
 */

UCLASS()
class STARHUNT_API AEnemyAIController : public AAIController
{
	GENERATED_BODY()


protected:
	const FName StateKeyName="State";
	const FName AttackTargetKeyName="AttackTarget";
	const FName PointOfInterestKeyName="PointOfInterest";

	UPROPERTY(VisibleAnywhere,Category="AI")
	EAIState CurrentState;
	
public:
	virtual void OnPossess(APawn* InPawn) override;
	virtual void BeginPlay() override;
	virtual void OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result) override;

	UFUNCTION(BlueprintPure,Category="AI")
	EAIState GetCurrentState() const;
	
	UFUNCTION(BlueprintCallable,Category="AI")
	void SetAIState(EAIState NewState);
	
	UFUNCTION(BlueprintCallable,Category="AI")
	void SetAttackTarget(AActor* AttackTarget);

	UFUNCTION(BlueprintCallable,Category="AI")
	void SetPointOfInterest(const FVector PointOfInterest);
};

EnemyAIController.cpp

// Fill out your copyright notice in the Description page of Project Settings.
// Copyright Epic Games, Inc. All Rights Reserved.

#include "EnemyAIController.h"
#include "GameFramework/Character.h"
#include "BehaviorTree/BehaviorTreeComponent.h"
#include "BaseEnemy.h"
#include "BehaviorTree/BlackboardComponent.h"
#include "GameFramework/CharacterMovementComponent.h"

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

	// Use Acceleration when move along path (associated with Animation)
	ACharacter* Character1 = Cast<ACharacter>(InPawn);
	if (Character1)
	{
		UCharacterMovementComponent* MovementComp = Character1->GetCharacterMovement();
		if (MovementComp)
		{
			MovementComp->bRequestedMoveUseAcceleration=true;
		}
	}

	ABaseEnemy* Enemy=Cast<ABaseEnemy>(InPawn);
	if (Enemy)
	{
		UBehaviorTree* BT=Enemy->GetBehaviorTree();
		if (BT)
		{
			RunBehaviorTree(BT);
		}
	}
}

void AEnemyAIController::BeginPlay()
{
	Super::BeginPlay();
}

void AEnemyAIController::OnMoveCompleted(FAIRequestID RequestID, const FPathFollowingResult& Result)
{
	Super::OnMoveCompleted(RequestID, Result);
}

EAIState AEnemyAIController::GetCurrentState() const
{
	return CurrentState; 
}

void AEnemyAIController::SetAIState(EAIState NewState)
{
	CurrentState = NewState;
	if (UBlackboardComponent* BB=GetBlackboardComponent())
	{
		BB->SetValueAsEnum(StateKeyName, static_cast<uint8>(NewState));
	}
}

void AEnemyAIController::SetAttackTarget(AActor* AttackTarget)
{
	if (UBlackboardComponent* BB=GetBlackboardComponent())
	{
		BB->SetValueAsObject(AttackTargetKeyName,AttackTarget);
	}
}

void AEnemyAIController::SetPointOfInterest(const FVector PointOfInterest)
{
	if (UBlackboardComponent* BB=GetBlackboardComponent())
	{
		BB->SetValueAsVector(PointOfInterestKeyName, PointOfInterest);
	}
}
  • 현재 State를 저장하는 변수 생성
  • AI가 빙의하는 시점에 가속도 관련 옵션 true 설정
  • 추가로 Enemy에 지정된 BehaviorTree를 실행하도록 설정
  • BlackBoard의 State를 변경시키는 함수 및 현재 State 반환하는 함수 생성
  • 추가로 BlackBoard의 AttackTarget, PoinOfInterest와 같은 Key값을 설정하는 함수 생성

3. BaseEnemy

BaseEnemy.h

// Fill out your copyright notice in the Description page of Project Settings.
// Copyright Epic Games, Inc. All Rights Reserved.
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "PatrolPath.h"
#include "BehaviorTree/BehaviorTree.h"
#include "BaseEnemy.generated.h"

//전방 선언
enum class EMovementSpeed : uint8;

//Delegate 선언
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnAttackEnd);

UCLASS()
class STARHUNT_API ABaseEnemy : public ACharacter
{
	GENERATED_BODY()

public:
	// Sets default values for this character's properties
	ABaseEnemy();

protected:
	// Called when the game starts or when spawned
	virtual void BeginPlay() override;


	//Power of Character
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Power")
	float Power;
	
	//Attack Montage
	UPROPERTY(EditAnywhere, Category="Attack")
	UAnimMontage* AttackMontage;
	
	// Score
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Score")
	int32 Score;

	//Patrol Path
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Path")
	APatrolPath* PatrolPath;
	
	// Max Health
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Health")
	float MaxHealth;
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Health")
	float Health;

	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "AI")
	UBehaviorTree* BehaviorTree;

	// Death handling function
	UFUNCTION(BlueprintCallable, Category = "Health")
	virtual void OnDeath();

public:	
	// Called every frame
	//virtual void Tick(float DeltaTime) override;

	// Called to bind functionality to input
	//virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

	//Get Patrol Path
	UFUNCTION(BlueprintPure, Category = "Path")
	APatrolPath* GetPatrolPath() const;

	// Get health variables
	UFUNCTION(BlueprintPure, Category = "Health")
	float GetHealth() const;
	UFUNCTION(BlueprintPure, Category = "Health")
	float GetMaxHealth() const;
	// Healing
	UFUNCTION(BlueprintCallable, Category = "Health")
	void AddHealth(const float Amount);
	// Damage handling function
	virtual float TakeDamage(float DamageAmount, struct FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser) override;
	
	UFUNCTION(BlueprintCallable, Category = "Movement")
	void SetMovementSpeed(const EMovementSpeed Speed);

	//Get BehaviorTree
	UFUNCTION(BlueprintPure, Category = "AI")
	UBehaviorTree* GetBehaviorTree() const;

	UFUNCTION(BlueprintCallable, Category = "AI")
	void Attack();

	UPROPERTY(BlueprintAssignable, Category="Attack")
	FOnAttackEnd OnAttackEnd;

	UFUNCTION()
	void OnMontageEnded(UAnimMontage* Montage, bool bInterrupted);
};

BaseEnemy.cpp

// Fill out your copyright notice in the Description page of Project Settings.
// Copyright Epic Games, Inc. All Rights Reserved.

#include "BaseEnemy.h"
#include "EnemyAIController.h"
#include "Animation/AnimInstance.h"
#include "Animation/AnimMontage.h"
#include "Animation/AnimSequence.h"
#include "GameFramework/CharacterMovementComponent.h"

// Sets default values
ABaseEnemy::ABaseEnemy()
{
 	// 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;
	AIControllerClass = AEnemyAIController::StaticClass();
	AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;

	PatrolPath=nullptr;
	BehaviorTree=nullptr;
	AttackMontage=nullptr;
	Power=0;
	Health=MaxHealth=0.0f;
	Score=0;
}

// Called when the game starts or when spawned
void ABaseEnemy::BeginPlay()
{
	Super::BeginPlay();
	
}


float ABaseEnemy::GetHealth() const
{
	return Health;
}

float ABaseEnemy::GetMaxHealth() const
{
	return MaxHealth;
}

void ABaseEnemy::AddHealth(const float Amount)
{
	Health = FMath::Clamp(Health + Amount, 0.0f, MaxHealth);
}

void ABaseEnemy::OnDeath()
{
	// Deliver Score to Game Instance

	Destroy();
}

float ABaseEnemy::TakeDamage(float DamageAmount, FDamageEvent const& DamageEvent, AController* EventInstigator, AActor* DamageCauser)
{
	float ActualDamage = Super::TakeDamage(DamageAmount, DamageEvent, EventInstigator, DamageCauser);

	Health = FMath::Clamp(Health - DamageAmount, 0.0f, MaxHealth);
	if (Health <= 0.0f) 
	{
		OnDeath();
	}
	return ActualDamage;
}

APatrolPath* ABaseEnemy::GetPatrolPath() const
{
	return PatrolPath;
}

void ABaseEnemy::SetMovementSpeed(const EMovementSpeed Speed)
{
	if (UCharacterMovementComponent* MovementComp = GetCharacterMovement())
	{
		switch (Speed)
		{
			case EMovementSpeed::Idle:
				MovementComp->MaxWalkSpeed = 0.0f;
			case EMovementSpeed::Walking:
				MovementComp->MaxWalkSpeed = 100.0f;
			case EMovementSpeed::Jogging:
				MovementComp->MaxWalkSpeed = 300.0f;
			case EMovementSpeed::Sprinting:
				MovementComp->MaxWalkSpeed = 500.0f;
			default:
				break;
		}
	}
}

UBehaviorTree* ABaseEnemy::GetBehaviorTree() const
{
	return BehaviorTree;
}

void ABaseEnemy::Attack()
{
	//메시 유효 
	if (!GetMesh()) return;

	UAnimInstance* AnimInstance = GetMesh()->GetAnimInstance();
	//둘다 유효
	if (AnimInstance&&AttackMontage)
	{
		//몽타주 실행
		AnimInstance->Montage_Play(AttackMontage);
		//몽타주 끝났을 때 이벤트 바인딩
		AnimInstance->OnMontageEnded.Clear();
		AnimInstance->OnMontageEnded.AddDynamic(this,&ABaseEnemy::OnMontageEnded);
	}
}

void ABaseEnemy::OnMontageEnded(UAnimMontage* Montage, bool bInterrupted)
{
	if (Montage==AttackMontage)
	{
		OnAttackEnd.Broadcast();
	}
}

// Called every frame
//void ABaseEnemy::Tick(float DeltaTime)
//{
//	Super::Tick(DeltaTime);
//
//}

// Called to bind functionality to input
//void ABaseEnemy::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
//{
//	Super::SetupPlayerInputComponent(PlayerInputComponent);
//
//}

 

  • 몬스터마다 수행하는 BehaviorTree를 달리할 예정이므로 BehaviorTree를 가리킬 포인터 추가
  • 공격 몽타주가 몬스터별로 다르므로 실행할 애니메이션 몽타주를 지정할 것을 가리키는 포인터 추가
  • AI에서 현재 실행해야할 BehaviorTree를 알 수 있도록 Get함수 생성
  • 상태 변화에 따라 이동속도를 달리 설정할 수 있도록 이동속도를 조절하는 Set함수 생성
  • 데미지 로직은 아직 없지만 Attack이라는 이름의 공격을 수행하는 함수 생성
    ㅡ> 공격 애니메이션 몽타주 실행 및 몽타주 끝날 시의 이벤트 바인딩 (델리게이트 이용)
  • 공격 애니메이션 몽타주가 끝나는 시점에 호출되는 함수(OnMontageEnded) 생성

4. Perception 상태 변화 (블루프린트: 소스코드로 변환 예정)

1. 시각에 감지되었을 때의 Handling function

2. 청각에 감지되었을 때의 handling function

3. 피해가 감지되었을 때의 handling function

4. 특정 액터가 어떤 감각을 통해 감지되었는지 확인

5. 위 4가지 함수를 이용해 인식된 감각마다 알맞은 Handling function 할당

5. 정리

완료

  • 블루 프린트로 구성되었던 AI, Enemy 관련 기능들 소스 코드 변환 및 리팩토링
  • 청각, 시각, 피해 3가지 감각에 따른 알맞은 handling 함수 배정

예정

  • EQS 추가
  • 원거리 적군 추가
  • 부위별 데미지 차등
  • 보스

참고자료

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