상세 컨텐츠

본문 제목

Unreal C++ 싱글플레이 FPS 슈터 게임 팀 프로젝트 적 AI - 1

Unreal C++

by hyunjunstar 2026. 5. 19. 20:50

본문

AI 구조

적 AI에서 아래 기능들을 구현 하였다.

 - 적 캐릭터 구현
 - 체력/방어력 시스템

 - 피격/사망 처리
 - 플레이어 탐지
 - 플레이어 추적
 - 공격 범위 내 공격
 - NavMesh 기반 경로 탐색
 - 장애물 회피

AI 폴더 구조

구현한 코드

맨 처음 구현했던 코드는 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 중심으로 구현 하였다.

 

 

 

관련글 더보기