언리얼엔진(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