적 AI에서 아래 기능들을 구현 하였다.
- 적 캐릭터 구현
- 체력/방어력 시스템
- 피격/사망 처리
- 플레이어 탐지
- 플레이어 추적
- 공격 범위 내 공격
- NavMesh 기반 경로 탐색
- 장애물 회피

맨 처음 구현했던 코드는 EnemyAIController가 모든 판단을 직접 처리하게끔 구현하였다.
플레이어와의 거리를 매 프레임마다 계산 후 거리에 따라 상태를 변경하였다.
EnemyAIController.h
enum을 사용해서 적이 현재 어떤 상태인지를 구분하였다.
// EnemyAIController.h
#pragma once
#include "CoreMinimal.h"
#include "AIController.h"
#include "EnemyAIController.generated.h"
// 적 AI가 가질 수 있는 상태
UENUM(BlueprintType)
enum class EEnemyAIState : uint8
{
Idle, // 대기
Chase, // 추적
Attack, // 공격
Dead // 사망
};
UCLASS()
class CH3_PROJECT_API AEnemyAIController : public AAIController
{
GENERATED_BODY()
public:
AEnemyAIController();
protected:
// 적 캐릭터를 조종하기 시작할 때 호출
virtual void OnPossess(APawn* InPawn) override;
// 매 프레임 AI 판단을 실행
virtual void Tick(float DeltaTime) override;
private:
// 추적할 플레이어
UPROPERTY()
APawn* TargetPlayer;
// 현재 조종 중인 적 캐릭터
UPROPERTY()
class AEnemyCharacter* ControlledEnemy;
// 현재 AI 상태
EEnemyAIState CurrentState;
// 마지막 공격 시간
float LastAttackTime;
// 플레이어 찾기
void FindPlayer();
// AI 상태 갱신
void UpdateAI();
// 상태 변경
void ChangeState(EEnemyAIState NewState);
// 공격 가능 여부 확인
bool CanAttack() const;
// 공격 실행
void PerformAttack();
};
EnemyAIController.cpp
UpdateAI()를 사용하여 매 프레임마다 플레이어와의 거리를 계산 후 거리에 따라 상태를 변경하였다.
// EnemyAIController.cpp
#include "EnemyAIController.h"
#include "EnemyCharacter.h"
#include "Kismet/GameplayStatics.h"
AEnemyAIController::AEnemyAIController()
{
// Tick에서 매 프레임 AI를 갱신하기 위해 활성화
PrimaryActorTick.bCanEverTick = true;
TargetPlayer = nullptr;
ControlledEnemy = nullptr;
CurrentState = EEnemyAIState::Idle;
LastAttackTime = 0.f;
}
void AEnemyAIController::OnPossess(APawn* InPawn)
{
Super::OnPossess(InPawn);
// 조종할 Pawn을 EnemyCharacter로 캐스팅
ControlledEnemy = Cast<AEnemyCharacter>(InPawn);
// 플레이어 참조 저장
FindPlayer();
}
void AEnemyAIController::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 매 프레임 AI 판단 실행
UpdateAI();
}
void AEnemyAIController::FindPlayer()
{
// 0번 플레이어 Pawn 찾기
TargetPlayer = UGameplayStatics::GetPlayerPawn(this, 0);
}
void AEnemyAIController::UpdateAI()
{
// 조종 중인 적이 없으면 중단
if (!ControlledEnemy)
{
return;
}
// 플레이어가 없으면 다시 찾기
if (!TargetPlayer)
{
FindPlayer();
return;
}
// 적이 죽었으면 Dead 상태로 변경
if (ControlledEnemy->IsDead())
{
ChangeState(EEnemyAIState::Dead);
StopMovement();
return;
}
// 적과 플레이어 사이의 거리 계산
const float Distance = FVector::Dist(
ControlledEnemy->GetActorLocation(),
TargetPlayer->GetActorLocation()
);
// 공격 범위 안이면 공격
if (Distance <= ControlledEnemy->AttackRange)
{
ChangeState(EEnemyAIState::Attack);
StopMovement();
PerformAttack();
}
// 감지 범위 안이면 추적
else if (Distance <= ControlledEnemy->DetectionRange)
{
ChangeState(EEnemyAIState::Chase);
MoveToActor(TargetPlayer);
}
// 감지 범위 밖이면 대기
else
{
ChangeState(EEnemyAIState::Idle);
StopMovement();
}
}
void AEnemyAIController::ChangeState(EEnemyAIState NewState)
{
// 같은 상태면 변경하지 않음
if (CurrentState == NewState)
{
return;
}
CurrentState = NewState;
}
bool AEnemyAIController::CanAttack() const
{
if (!ControlledEnemy || !GetWorld())
{
return false;
}
// 마지막 공격 이후 쿨타임이 지났는지 확인
const float CurrentTime = GetWorld()->GetTimeSeconds();
return CurrentTime - LastAttackTime >= ControlledEnemy->AttackCooldown;
}
void AEnemyAIController::PerformAttack()
{
// 공격 쿨타임이 안 지났으면 공격하지 않음
if (!CanAttack() || !GetWorld())
{
return;
}
// 마지막 공격 시간 갱신
LastAttackTime = GetWorld()->GetTimeSeconds();
// 초기 버전에서는 공격 로그만 출력
UE_LOG(LogTemp, Warning, TEXT("Enemy Attack"));
}
EnemyCharacter.h
적 캐릭터의 기본 스탯 값들을 설정하였다.
// EnemyCharacter.h
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "EnemyCharacter.generated.h"
UCLASS()
class CH3_PROJECT_API AEnemyCharacter : public ACharacter
{
GENERATED_BODY()
public:
AEnemyCharacter();
// 최대 체력
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Enemy|Stat")
float MaxHP;
// 현재 체력
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Enemy|Stat")
float CurrentHP;
// 방어력
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Enemy|Stat")
float Defense;
// 처치 시 지급 점수
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Enemy|Reward")
int32 ScoreValue;
// 공격 데미지
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Enemy|Attack")
float AttackDamage;
// 플레이어 감지 범위
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Enemy|AI")
float DetectionRange;
// 공격 가능 범위
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Enemy|AI")
float AttackRange;
// 공격 쿨타임
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Enemy|Attack")
float AttackCooldown;
// 데미지 처리 함수
UFUNCTION(BlueprintCallable)
void TakeDamageFromEnemy(float Damage);
// 사망 여부 반환
bool IsDead() const;
protected:
// 게임 시작 시 호출
virtual void BeginPlay() override;
// 사망 처리
void Die();
private:
// 중복 사망 처리를 막기 위한 변수
bool bIsDead;
};
EnemyCharacter.cpp
적 캐릭터의 기본 스탯을 초기화 및 각종 상태 처리 구현,
적 캐릭터 사망시 GameMode에 점수를 지급 한 후 Destroy()로 제거 하였다.
// EnemyCharacter.cpp
#include "EnemyCharacter.h"
#include "CH3_Project/ShooterGameMode.h"
#include "Kismet/GameplayStatics.h"
AEnemyCharacter::AEnemyCharacter()
{
// 적 캐릭터 자체는 Tick을 사용하지 않음
PrimaryActorTick.bCanEverTick = false;
// 기본 스탯 설정
MaxHP = 100.f;
CurrentHP = MaxHP;
Defense = 0.f;
ScoreValue = 1;
AttackDamage = 10.f;
DetectionRange = 1200.f;
AttackRange = 150.f;
AttackCooldown = 1.5f;
bIsDead = false;
}
void AEnemyCharacter::BeginPlay()
{
Super::BeginPlay();
// 게임 시작 시 체력을 최대 체력으로 초기화
CurrentHP = MaxHP;
}
void AEnemyCharacter::TakeDamageFromEnemy(float Damage)
{
// 이미 죽었으면 데미지 무시
if (bIsDead)
{
return;
}
// 방어력을 적용한 최종 데미지 계산
const float FinalDamage = FMath::Max(Damage - Defense, 1.f);
CurrentHP -= FinalDamage;
// 체력이 0 이하가 되면 사망 처리
if (CurrentHP <= 0.f)
{
Die();
}
}
bool AEnemyCharacter::IsDead() const
{
return bIsDead;
}
void AEnemyCharacter::Die()
{
// 중복 사망 처리 방지
if (bIsDead)
{
return;
}
bIsDead = true;
// GameMode에 점수 추가
AShooterGameMode* GM = Cast<AShooterGameMode>(UGameplayStatics::GetGameMode(this));
if (GM)
{
GM->AddScore(ScoreValue);
}
// 초기 버전에서는 죽으면 바로 제거
Destroy();
}
CH3_Project.Build.cs
AI 사용을 위해 AIModule, NavigationSystem, GameplayTasks 추가
// CH3_Project.Build.cs
// Copyright Epic Games, Inc. All Rights Reserved.
using UnrealBuildTool;
public class CH3_Project : ModuleRules
{
public CH3_Project(ReadOnlyTargetRules Target) : base(Target)
{
PCHUsage = PCHUsageMode.UseExplicitOrSharedPCHs;
PublicDependencyModuleNames.AddRange(new string[] { "Core",
"CoreUObject",
"Engine",
"InputCore",
"EnhancedInput",
"UMG",
"AIModule",
"NavigationSystem",
"GameplayTasks" });
PrivateDependencyModuleNames.AddRange(new string[] { });
PublicIncludePaths.AddRange(new string[] {"CH3_Project"});
// Uncomment if you are using Slate UI
// PrivateDependencyModuleNames.AddRange(new string[] { "Slate", "SlateCore" });
// Uncomment if you are using online features
// PrivateDependencyModuleNames.Add("OnlineSubsystem");
// To include OnlineSubsystemSteam, add it to the plugins section in your uproject file with the Enabled attribute set to true
}
}
정리하자면 위 코드들은 아래처럼 설계 되었다.
1. EnemyAIController가 EnemyCharacter를 Possess
2. 플레이어 Pawn 찾기
3. Tick에서 매 프레임 UpdateAI 실행
4. 플레이어와 적 사이 거리 계산
5. 공격 범위 안이면 Attack
6. 감지 범위 안이면 Chase
7. 감지 범위 밖이면 Idle
8. 적 체력이 0 이하가 되면 Dead
9. 점수 지급 후 Destroy
EnemyAIController의 역할
플레이어 탐색 > 거리 계산 > 상태 변경 > 이동 > 공격 쿨타임 관리 > 공격
EnemyCharacter
기본 스탯 관리 > 방어력 적용 > 공격 및 감지 범위 값 적용 > 사망 처리 > 점수 지급
이렇게 초기 구현은 EnemyAIController 중심으로 구현 하였다.
| Unreal C++ 싱글플레이 FPS 슈터 게임 팀 프로젝트 적 AI - 3 (0) | 2026.05.21 |
|---|---|
| Unreal C++ 싱글플레이 FPS 슈터 게임 팀 프로젝트 적 AI - 2 (0) | 2026.05.20 |
| Unreal C++ 게임 루프 및 UI 설계 후 코인 획득 게임 구현 트러블슈팅 (0) | 2026.04.30 |
| Unreal C++ 게임 루프 및 UI 설계 후 코인 획득 게임 구현 (0) | 2026.04.29 |
| 타이머 실행중에 레벨이 바뀌면 에디터가 종료되는 오류 (0) | 2026.04.21 |