TIL(46)

이채원·2026년 8월 26일

TIL

목록 보기
46/77

오늘의 할일
1. 코드카타 정리
2. 세션 정리


코드카타 정리

자연수 n을 3진법 상에서 앞뒤로 뒤집은 후, 이를 다시 10진법으로 표현한 수를 return

  • 45 % 3 = 0
  • 15 % 3 = 0
  • 5 % 3 = 2
  • 1 % 3 = 1

이면 1200임 거꾸로 어떻게 뒤집게 하지
모듈러 써보기

using namespace std;

int solution(int n) {
    int answer = 0;
    
    while (n > 0) {
        // 기존 answer를 3배로 늘려 자릿수 하나 올리기
        // + n을 3으로 나눈 나머지
        answer = answer * 3 + (n % 3);
        
        // n을 3으로 나누어 다음 자릿수를 계산
        n /= 3;
    }
    
    return answer;
}

AI가 작성한 코드

#include <string>

using namespace std;

int solution(int n) {
    string ternary = "";
    
    // 3진법으로 변환하면서 동시에 문자열로 만들기
    // (나머지를 문자열 뒤에 계속 이어 붙이면 자연스럽게 '뒤집힌' 3진법이 완성됩니다)
    while (n > 0) {
        ternary += to_string(n % 3);
        n /= 3;
    }
    
    // 2. stoi를 사용하여 3진법 문자열을 10진수로 즉시 변환
    return stoi(ternary, nullptr, 3);
}

stoi (String TO Integer): 보통 문자열 "123"을 숫자 123으로 바꿀 때 쓰는 함수

세 번째 매개변수 (진법): 이 문자열이 몇 진법으로 적혀있는지를 알려주는 기능

끝에 3이라고 적기만 하면, 알아서 3진법임을 인지하고 10진수로 바꿔서 반환 처리. (nullptr - 변환이 끝난 위치를 저장할 포인터, 보통 필요 없어서 비워둠)

"나머지를 구해서 문자열에 이어 붙인다" ➔ "3진법이라 알려주고 숫자로 바꾼다"


C++와 Unreal Engine으로 3D 게임 개발 : 9회차 정리

3-4 : 캐릭터 체력 및 점수 관리 시스템 구현하기

PlayerState

  • 싱글 플레이에서는 굳이?
  • 멀티 플레이에서는 중요

GameState - 전역 정보를 저장하는 곳

SpartaCharacter

  • 체력
    Health
    MaxHealth 100
  • 데미지 처리
    UGamePlayStatics::ApplyDamage() - 데미지를 입히는 것
    -> 어떤 Actor에 데미지를 입혔는지.
    AActor::TakeDamage() - 데미지를 받는 것

1. 캐릭터 체력 시스템

캐릭터 클래스에 체력 변수 및 함수 선언

  • SpartaCharacter.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Character.h"
#include "SpartaCharacter.generated.h"

class USpringArmComponent;
class UCameraComponent;
struct FInputActionValue;

UCLASS()
class TESTPROJECT2_API ASpartaCharacter : public ACharacter
{
	GENERATED_BODY()

public:
	ASpartaCharacter();

	// 현재 체력을 가져오는 함수
	UFUNCTION(BlueprintPure, Category = "Health")
	int32 GetHealth() const;

	// 체력을 회복시키는 함수
	UFUNCTION(BlueprintCallable, Category = "Health")
	void AddHealth(float Amount);

protected:
	// 최대 체력
	UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Health")
	float MaxHealth;
	// 현재 체력
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Health")
	float Health;

	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;
	
	// 데미지 처리 함수 - 외부로부터 데미지를 받을 때 호출됨
	// 또는 AActor의 TakeDamage()를 오버라이드
	virtual float TakeDamage(float DamageAmount,
		struct FDamageEvent const& DamageEvent,
		AController* EventInstigator,	//데미지를 누가 입혔는지
		AActor* DamageCauser) override;	//데미지를 입힌 오브젝트

	// 사망 처리 함수 (체력이 0 이하가 되었을 때 호출)
	UFUNCTION(BlueprintCallable, Category = "Health")
	virtual void OnDeath();

};

데미지 및 회복 처리

데미지 처리 흐름 (ApplyDamage → TakeDamage)

  1. UGameplayStatics::ApplyDamage
    공격자가 데미지를 줄 대상 액터와 데미지 양, 데미지를 유발한 주체 등 인자로 넘겨 호출
    대상 액터의 TakeDamage() 함수를 호출 시도

  2. AActor::TakeDamage
    Actor의 가상 함수. 모든 액터가 기본적으로 이 함수를 가지고 있고 필요하다면 자식 클래스(캐릭터 등)에서 오버라이드.

  • SpartaCharacter.cpp
#include "SpartaCharacter.h"
#include "SpartaPlayerController.h"
#include "EnhancedInputComponent.h"
#include "Camera/CameraComponent.h"
#include "GameFramework/SpringArmComponent.h"
#include "GameFramework/CharacterMovementComponent.h"
#include "GameFramework/Actor.h"
#include "Kismet/GameplayStatics.h"	//UGameplayStatics 때문에 추가

ASpartaCharacter::ASpartaCharacter()
{
	PrimaryActorTick.bCanEverTick = false;
	
	GetCharacterMovement()->MaxWalkSpeed = NormalSpeed;

	// 초기 체력 설정
	MaxHealth = 100.0f;
	Health = MaxHealth;
}

int32 ASpartaCharacter::GetHealth() const {
	return Health;
}

// 체력 회복 함수
void ASpartaCharacter::AddHealth(float Amount)
{
	// 최대 체력을 초과하지 않도록 제한함
	Health = FMath::Clamp(Health + Amount, 0.0f, MaxHealth);
	UE_LOG(LogTemp, Log, TEXT("Health increased to: %f"), Health);
}

// 데미지 처리 함수
float ASpartaCharacter::TakeDamage(float DamageAmount,
	FDamageEvent const& DamageEvent,
	AController* EventInstigator,	//데미지를 유발한 주체(Controller)
	AActor* DamageCauser)	//데미지를 직접 발생시킨 오브젝트(총알, 폭발물 등)
{
	// 기본 데미지 처리 로직 호출 (필수는 아님)
	float ActualDamage = Super::TakeDamage(DamageAmount,
		DamageEvent, EventInstigator, DamageCauser);
	// 체력을 데미지만큼 감소시키고, 0 이하로 떨어지지 않도록 Clamp
	Health = FMath::Clamp(Health - DamageAmount, 0.0f, MaxHealth);
	UE_LOG(LogTemp, Warning, TEXT("Health decreased to: %f"), Health);
	// 체력이 0 이하가 되면 사망 처리
	if (Health <= 0.0f)
	{
		OnDeath();
	}
	// 실제 적용된 데미지를 반환
	return ActualDamage;
}

// 사망 처리 함수
void ASpartaCharacter::OnDeath()
{
	UE_LOG(LogTemp, Error, TEXT("Character is Dead!"));
	// 사망 후 로직
}

지뢰 아이템 데미지 함수 수정

MineItem이 폭발할 때 주변 액터에게 데미지를 입히기
-> UGameplayStatics::ApplyDamage 함수 호출해 해당 액터의 TakeDamage() 실행

  • MineItem.cpp
#include "MineItem.h"
#include "Components/SphereComponent.h"
#include <Kismet/GameplayStatics.h>

void AMineItem::Explode() {
	TArray<AActor*> OverlappingActors;
	ExplosionCollision->GetOverlappingActors(OverlappingActors);

	for (AActor* Actor : OverlappingActors) {
		if (Actor && Actor->ActorHasTag("Player"))
		{
			// 데미지를 발생시켜 Actor->TakeDamage()가 실행되도록 함
			UGameplayStatics::ApplyDamage(
				Actor, // 데미지를 받을 액터
				ExplosionDamage, // 데미지 양
				nullptr, // 데미지를 유발한 주체 (지뢰를 설치한 캐릭터가 없으므로 nullptr)
				this, // 데미지를 유발한 오브젝트(지뢰)
				UDamageType::StaticClass() // 기본 데미지 유형
				);
		}
	}
	DestroyItem();
}

힐링 아이템 체력 회복 함수 수정

  • HealingItem.cpp
    ActorHasTag("Player") 로 플레이어인지 판별
#include "HealingItem.h"
#include "SpartaCharacter.h"


AHealingItem::AHealingItem() {
	HealAmount = 20.0f;
	ItemType = "Healing";

}

void AHealingItem::ActivateItem(AActor* Activator) {
	if (Activator && Activator->ActorHasTag("Player"))
	{
		if (ASpartaCharacter* PlayerCharacter = Cast<ASpartaCharacter>(Activator))
		{
			// 캐릭터의 체력을 회복
			PlayerCharacter->AddHealth(HealAmount);
		}
		DestroyItem();
	}
}

2. 점수 관리 시스템 구현하기

GameMode - 게임의 규칙

멀티플레이에서는 서버 전용으로 동작
어떤 캐릭터를 스폰할지, 플레이어가 사망했을 때 어떻게 처리할지를 결정

GameState - 공유되어야 하는 전역 상태, 레벨당 1개 존재
전역 데이터 관리용
점수, 남은 시간, 현재 게임 단계(Phase), 스폰된 오브젝트의 총 개수 등
멀티플레이 - 서버가 관리

싱글 플레이 : 전역적으로 공유해야 할 정보를 한 군데서 관리하면 유지보수가 더 편함. “몇 개의 아이템이 스폰되었는지”, “현재 게임 진행도는 어느 정도인지” 같은 데이터를 일괄 관리할 수 있기 때문

GameState에 점수 데이터 & 함수 추가

전역 점수를 GameState 에 저장
점수를 획득할 때마다 AddScore() 호출 -> 점수가 증가

GameStateBase 클래스 생성(SpartaGameStateBase)

  • SpartaGameStateBase.h
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/GameStateBase.h"
#include "SpartaGameStateBase.generated.h"

UCLASS()
class TESTPROJECT2_API ASpartaGameStateBase : public AGameStateBase
{
	GENERATED_BODY()

public:
	ASpartaGameStateBase();
	// 전역 점수를 저장하는 변수
	UPROPERTY(VisibleAnywhere, BlueprintReadWrite, Category = "Score")
	int32 Score;
	// 현재 점수를 읽는 함수
	UFUNCTION(BlueprintPure, Category = "Score")
	int32 GetScore() const;
	// 점수를 추가해주는 함수
	UFUNCTION(BlueprintCallable, Category = "Score")
	void AddScore(int32 Amount);
};
  • SpartaGameStateBase.cpp
#include "SpartaGameStateBase.h"

ASpartaGameStateBase::ASpartaGameStateBase()
{
	Score = 0;
}
int32 ASpartaGameStateBase::GetScore() const
{
	return Score;
}
void ASpartaGameStateBase::AddScore(int32 Amount)
{
	Score += Amount;
}

GameMode와 GameState 연동

  • SpartaGameMode.h
#include "SpartaGameMode.h"
#include "SpartaCharacter.h"
#include "SpartaPlayerController.h"
#include "SpartaGameStateBase.h"

ASpartaGameMode::ASpartaGameMode()
{
	DefaultPawnClass = ASpartaCharacter::StaticClass();
	PlayerControllerClass = ASpartaPlayerController::StaticClass();
	// 우리가 만든 GameState로 설정
	GameStateClass = ASpartaGameStateBase::StaticClass();
}

코인 아이템 점수 획득 함수 수정

  • CoinItem.cpp
#include "CoinItem.h"
#include "Engine/World.h"
#include "SpartaGameStateBase.h"

ACoinItem::ACoinItem()
{
	PointValue = 0;	//코인 아이템이 제공하는 점수량
	ItemType = "DefaultCoin";
}
void ACoinItem::ActivateItem(AActor* Activator)
{
	if (Activator && Activator->ActorHasTag("Player"))
	{
		if (UWorld* World = GetWorld())
		{
			if (ASpartaGameStateBase* GameState = World->GetGameState<ASpartaGameStateBase>())
			{
				GameState->AddScore(PointValue);
			}
		}
		DestroyItem();
	}
}

적 AI 기초 특강 : 1회차 정리

플레이어 vs AI 차이점을 알아야함

플레이어 : 키보드/마우스 입력 - PlayerController - Character class - 움직임

AI : 입력 X - 프로그래밍된 로직이 존재(프로그래밍 로직을 짠 대로 움직임) - AI Controller 필요 - Character class 필요 - 움직임

<AI의 요소들>

AI Controller (뇌 역할) : 무엇을 할지 결정하는 곳
Pawn/Character (몸 역할) : 실제로 맵에서 움직이는 메시, Controller의 명령을 받아 실행
Navigation System (지도 역할) : AI 가 어디로 갈 수 있는지 알려주는 시스템, 장애물을 피해가는 경로 계산
Perception System(감각) : 시각, 청각 등
Blackboard (기억)
Behavior Tree(행동 결정) : IF-THEN 로직을 시각적으로 표현

Actor : 언리얼 월드에 배치될 수 있는 가장 기본적인 객체. 캐릭터, 카메라, 아이템 등 많은 클래스가 Actor에서 시작!!
Pawn : Controller가 조종할 수 있는 액터. 플레이어나 AI나 조종하는 대상
Character : Pawn에 이동 기능과 Capsule, Character Movement 등이 기본 포함된 클래스. 사람형 캐릭터를 만들 때 많이 사용
Controller : Pawn을 직접 움직이는 명령을 내리는 객체
Possess : Controller가 특정 Pawn을 자신의 조종 대상으로 잡는 것.
Navigation/NavMesh : AI가 이동 가능한 영역과 경로를 계산하기 위한 시스템
Component : Actor에 기능을 붙이는 부품
Blackboard : Behavior Tree가 사용할 값을 저장하는 AI의 기억공간
Behavior Tree : AI 행동의 우선순위와 조건을 트리 구조로 구성하는 시스템

오늘의 강의 순서

기본 AI 생성 - 아무것도 안하고 서있는 AI
👇
Navigation 추가 - 랜덤하게 돌아다니는 AI
👇
Perception 추가 - AI에게 눈을 달아주기
👇
추적 기능 - 플레이어를 쫓아가는 AI

  • SpartaAIController.h
#pragma once // 헤더 파일이 한번만 포함되도록 막아 주는 전처리

#include "CoreMinimal.h" //언리얼에서 자주 사용하는 기본 타입과 매크로를 사용하기 위한 헤더
#include "AIController.h" //AI 전용 Controller 클래스인 AAIController를 사용하기 위한 헤더
#include "Perception/AIPerceptionTypes.h" // FAIStimulus 같은 AI 인지 시스템(시각, 청각 등) 관련 구조체를 쓰기 위한 헤더
#include "SpartaAIController.generated.h" // UHT가 분석해서 자동 생성한 리플렉션 코드를 포함

class UAIPerceptionComponent; // 전방 선언: 포인터만 사용할 클래스를 미리 알려줘서 Include로 인한 빌드 속도 저하를 막음
class UAISenseConfig_Sight;   // 전방 선언: AI의 시야(Sight) 설정을 담당하는 클래스

UCLASS() // UHT에게 이 클래스를 언리얼 리플렉션 시스템과 가비지 컬렉션 시스템이 인식하도록 등록하는 매크로
class AIBASICLESSON_API ASpartaAIController : public AAIController
{
	GENERATED_BODY() 

public:
	ASpartaAIController(); // 생성자

protected:
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
	UAIPerceptionComponent* AIPerception; // AI의 눈, 귀 역할을 할 인지 컴포넌트 포인터

	// AI 카테고리로 묶어서 에디터에 표시. 시야각, 인지 거리 등의 설정을 담는 포인터
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
	UAISenseConfig_Sight* SightConfig; 

	// 중요: 에디터에 노출은 안 하지만 UPROPERTY()를 붙인 이유
	// 추적하던 타겟(Actor)이 갑자기 파괴되었을 때 
    // 이 포인터가 쓰레기 메모리를 가리키지 않도록 
    // 가비지 컬렉션(GC) 시스템이 알아서 nullptr로 안전하게 비우려고 붙임
	UPROPERTY()
	AActor* CurrentTarget = nullptr; // 현재 AI가 쫓아가고 있는 목표물 (초기값은 없음)

	UFUNCTION()
	void OnPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus); // AI가 무언가를 감지했을 때(보거나 놓쳤을 때) 실행될 함수

	bool bIsChasing = false; // 현재 누군가를 쫓고 있는지 상태를 기억하는 스위치(True/False)
	FTimerHandle ChaseTimer; // 추적 로직(UpdateChase)을 일정 시간마다 반복 실행하기 위해 타이머를 관리

	virtual void BeginPlay() override;
	
	virtual void OnPossess(APawn* InPawn) override;
	// 부모(AAIController)에 있는 함수를 재정의. AI 컨트롤러(뇌)가 특정 폰(몸통)에 빙의해서 조종을 시작하는 순간 호출됨

	void StartChasing(AActor* Target); // 타겟을 발견했을 때 추적을 시작하는 커스텀 함수
	void StopChasing(); // 타겟을 놓쳤을 때 추적을 멈추는 커스텀 함수
	void UpdateChase(); // 추적 중일 때, 타겟의 위치로 계속 이동 명령을 내리기 위해 반복 호출될 커스텀 함수

private: 
	void MoveToRandomLocation(); // 추적할 타겟이 없을 때 주변을 랜덤하게 돌아다니는 함수

	FTimerHandle RandomMoveTimer; // 랜덤 이동 타이머를 관리. 나중에 멈추거나 시간을 바꿀 때 필요함

	UPROPERTY(EditAnywhere, Category = "AI")
	float MoveRadius = 1000.0f; // 랜덤으로 돌아다닐 반경 (기본값 1000cm = 10m)
};
  • SpartaAIController.cpp
#include "SpartaAIController.h"
#include "TimerManager.h" // 타이머(SetTimer 등)를 사용하기 위한 헤더
#include "NavigationSystem.h" // 내비게이션 메시 시스템을 사용하기 위한 헤더
#include "Perception/AIPerceptionComponent.h" // AI 인지(시야, 청각 등) 컴포넌트 헤더
#include "Perception/AISenseConfig_Sight.h" // 시야 센서 설정을 위한 헤더
#include "SpartaAICharacter.h" // 조종할 AI 캐릭터 클래스 헤더 (이동 속도 변경 등에 사용)
#include "Kismet/GameplayStatics.h" // GetPlayerPawn 같은 유용한 전역 함수들을 쓰기 위한 헤더

ASpartaAIController::ASpartaAIController()
{	
	// 인지 컴포넌트(눈, 귀 역할)를 메모리에 생성하고 AIPerception 포인터에 담음
	AIPerception = CreateDefaultSubobject<UAIPerceptionComponent>(TEXT("AIPerception"));
	
	// 이 컨트롤러의 메인 인지 컴포넌트로 방금 만든 AIPerception을 지정
	SetPerceptionComponent(*AIPerception);

	// 시각 센서 설정 객체를 생성
	SightConfig = CreateDefaultSubobject<UAISenseConfig_Sight>(TEXT("SightConfig"));
	
	SightConfig->SightRadius = 1500.0f; // AI가 대상을 처음 발견할 수 있는 최대 거리 (15m)
	SightConfig->LoseSightRadius = 2000.0f; // 대상을 놓치게 되는 거리 (20m). 
	SightConfig->PeripheralVisionAngleDegrees = 90.0f; // AI의 좌우 시야각
	SightConfig->SetMaxAge(5.0f); // 감지된 대상에 대한 기억을 유지하는 시간

	// 누구를 감지할 것인가? (아군, 적군, 중립) 여기서는 true로 다 켜둬서 일단 시야에 들어오면 다 감지함
	SightConfig->DetectionByAffiliation.bDetectEnemies = true;
	SightConfig->DetectionByAffiliation.bDetectNeutrals = true;
	SightConfig->DetectionByAffiliation.bDetectFriendlies = true;

	// 세팅이 끝난 시각 센서를 인지 컴포넌트에 장착
	AIPerception->ConfigureSense(*SightConfig);	
	// 이 AI의 가장 주감각을 시각(Sight)으로 설정
	AIPerception->SetDominantSense(SightConfig->GetSenseImplementation());
}

void ASpartaAIController::BeginPlay() {

	Super::BeginPlay();	

	if (AIPerception) {
		// 중요: 대상의 감지 상태(보임/안 보임)가 변할 때마다 OnPerceptionUpdated 함수를 자동 실행하도록 이벤트에 연결
		AIPerception->OnTargetPerceptionUpdated.AddDynamic(
			this,
			&ASpartaAIController::OnPerceptionUpdated
		);
	}

	// 게임 시작 1초 뒤부터, 3초 간격으로 무한 반복하여 MoveToRandomLocation 함수를 실행하는 타이머 켜기
    //TimerManager에서 타이머를 등록하여 일정 시간 뒤 또는 반복적으로 함수 호출
	GetWorldTimerManager().SetTimer(RandomMoveTimer, this, 
		&ASpartaAIController::MoveToRandomLocation,
		3.0f, true, 1.0f);
		
}

void ASpartaAIController::OnPossess(APawn* InPawn) {
	Super::OnPossess(InPawn); // Controller가 Pawn에 빙의할 때 필요한 엔진 기본 로직 실행
	
	// 빙의할 대상이 정상적으로 존재한다면
	if (InPawn)
	{
		// Output Log에 Warning으로 어떤 액터를 조종하게 되었는지 이름 출력
		UE_LOG(LogTemp, Warning, TEXT("[Sparta] AI Controller is controlling %s."), *InPawn->GetName());
	}
}

void ASpartaAIController::MoveToRandomLocation() {
	APawn* MyPawn = GetPawn(); // 현재 이 뇌가 조종하고 있는 Pawn을 가져옴

	if (!MyPawn)
	{
		// 몸통이 없으면 에러 로그 띄우고 아래 코드는 의미 없으니 진행 안 함
		UE_LOG(LogTemp, Error, TEXT("[Sparta] No Pawn to control."));
	}

	// 현재 맵에 깔려있는 내비게이션 시스템을 가져옴
	UNavigationSystemV1* NavSystem = UNavigationSystemV1::GetCurrent(GetWorld());
	if (!NavSystem)
	{
		UE_LOG(LogTemp, Error, TEXT("[Sparta] Could not find Navigation System."));
	}

	// 내비게이션 시스템이 찾아낸 랜덤 좌표를 담아둘 빈 구조체 생성
	FNavLocation RandomLocation;
	
	// 내 몸통 위치를 기준으로 MoveRadius(10m) 반경 내에서 걸어갈 수 있는 랜덤 좌표를 찾아 RandomLocation에 담기
	// 길을 찾았다면 true, 못 찾았다면 false를 리턴
	bool bFoundLocation = NavSystem->GetRandomReachablePointInRadius(
		MyPawn->GetActorLocation(),
		MoveRadius,
		RandomLocation);

	if (bFoundLocation) {
		// 찾은 목적지로 걸어가기
		MoveToLocation(RandomLocation.Location);	
		UE_LOG(LogTemp, Warning, TEXT("[Sparta] Move target: %s"), *RandomLocation.Location.ToString());
	}
	else {
		// 내비 메시가 안 깔려있거나, 갇혀있어서 갈 곳이 없으면 경고 출력
		UE_LOG(LogTemp, Warning, TEXT("[Sparta] Could not find a reachable location."));
	}
}

void ASpartaAIController::OnPerceptionUpdated(AActor* Actor, FAIStimulus Stimulus) {
	// 월드에 있는 플레이어를 가져옴
	APawn* PlayerPawn = UGameplayStatics::GetPlayerPawn(GetWorld(), 0);
	
	// 시야에 들어온 액터가 플레이어가 아니라면 무시 & 함수 종료 (적 AI끼리 서로 쫓아다니는 걸 방지)
	if (Actor != PlayerPawn)
	{
		return;
	}

	// 대상이 내 시야에 들어왔다면 (WasSuccessfullySensed == true)
	if (Stimulus.WasSuccessfullySensed())
	{
		UE_LOG(LogTemp, Warning, TEXT("[Sparta] Saw something! %s"), *Actor->GetName());

		// 대상 머리 위에 초록색 글씨로 "Saw: 플레이어이름"을 2초 동안 그림 (디버깅용)
		DrawDebugString(
			GetWorld(),
			Actor->GetActorLocation() + FVector(0, 0, 100),
			FString::Printf(TEXT("Saw: %s"), *Actor->GetName()),
			nullptr,
			FColor::Green,
			2.0f,
			true
		);

		StartChasing(Actor); // 발견했으니 추적 시작됨
	}
	else // 대상이 시야에서 벗어났다면 (WasSuccessfullySensed == false)
	{
		UE_LOG(LogTemp, Warning, TEXT("[Sparta] Missed it! %s"), *Actor->GetName());

		// 놓친 위치에 빨간색 글씨로 "Missed: 플레이어이름"을 2초 동안 그림
		DrawDebugString(
			GetWorld(),
			Actor->GetActorLocation() + FVector(0, 0, 100),
			FString::Printf(TEXT("Missed: %s"), *Actor->GetName()),
			nullptr,
			FColor::Red,
			2.0f,
			true
		);

		StopChasing(); // 안 보이니 추적 중지됨
	}
}

void ASpartaAIController::StartChasing(AActor* Target)
{
	// 이미 추적 중인데 대상이 똑같다면 코드 또 실행하지 않고 종료
	if (bIsChasing && CurrentTarget == Target) return;

	CurrentTarget = Target; // 목표물을 기억함
	bIsChasing = true; // 추적 스위치 ON

	// 쫓아가야 하니까 기존에 켜뒀던 랜덤 순찰 타이머를 강제로 꺼버림
	GetWorldTimerManager().ClearTimer(RandomMoveTimer);

	// 현재 조종 중인 폰(몸통)을 ASpartaAICharacter 타입으로 변환
	if (ASpartaAICharacter* AIChar = Cast<ASpartaAICharacter>(GetPawn()))
	{
		// 변환에 성공했다면 순찰 속도가 아니라 달리기 속도로 세팅 변경
		AIChar->SetMovementSpeed(AIChar->RunSpeed);
	}

	UpdateChase(); // 일단 한 번 타겟 쪽으로 이동
	
	// 0.25초마다 UpdateChase 함수를 무한 반복하는 추적 타이머 켜기 
	// (타겟이 움직일 수 있으니 계속 목적지를 갱신해 줘야 함)
	GetWorldTimerManager().SetTimer(
		ChaseTimer,
		this,
		&ASpartaAIController::UpdateChase,
		0.25f,
		true
	);
}

void ASpartaAIController::StopChasing()
{
	// 쫓고 있지 않았다면 아무것도 안 하고 종료
	if (!bIsChasing) return;

	CurrentTarget = nullptr; // 타겟 정보 삭제
	bIsChasing = false; // 추적 스위치 OFF

	// 쫓아가는 걸 멈췄으니까 0.25초마다 길을 찾던 추적 타이머 끄기
	GetWorldTimerManager().ClearTimer(ChaseTimer);

	StopMovement(); // 가던 길 멈추기

	// 다시 몸통을 불러와서
	if (ASpartaAICharacter* AIChar = Cast<ASpartaAICharacter>(GetPawn()))
	{
		// 달리기 모드 해제, 걷기 속도로 다시 돌아옴
		AIChar->SetMovementSpeed(AIChar->WalkSpeed);
	}

	// 2초 정도 가만히 서서 두리번거리다가, 다시 3초마다 랜덤 순찰 시작하게끔 타이머 다시 시작
	GetWorldTimerManager().SetTimer(
		RandomMoveTimer,
		this,
		&ASpartaAIController::MoveToRandomLocation,
		3.0f,
		true,
		2.0f 
	);
}

void ASpartaAIController::UpdateChase()
{
	// 타겟이 존재하고, 추적 모드일 때만 실행
	if (CurrentTarget && bIsChasing)
	{
		// 목표 액터를 향해 이동
		// 100.0f는 AcceptanceRadius(허용 반경), 타겟과 1m 거리에 도달하면 도착한 것으로 치고 멈추기
		MoveToActor(CurrentTarget, 100.0f);
	}
}
  • SpartaAICharacter.h
#pragma once 

#include "CoreMinimal.h" 
#include "GameFramework/Character.h" // 이동, 충돌 캡슐, 애니메이션(스켈레탈 메시) 기능이 기본으로 달린 'ACharacter' 부모 클래스 포함
#include "Perception/AIPerceptionStimuliSourceComponent.h" // AI의 시각/청각에 자극(Stimuli)을 주는 컴포넌트 포함
#include "SpartaAICharacter.generated.h"

// UENUM(BlueprintType): 이 열거형(Enum) 데이터를 블루프린트에서도 변수 타입으로 쓸 수 있게 엔진에 등록하는 매크로
UENUM(BlueprintType)
enum class EZombieType : uint8 { // uint8(1바이트 정수) 기반의 좀비 타입 목록 생성
	// UMETA(DisplayName = "..."): 에디터 디테일 패널에서 읽기 편하게 표시
	HumanZombie UMETA(DisplayName = "HumanZombie"), // 인간형 좀비 타입
	DogZombie UMETA(DisplayName = "DogZombie"),     // 강아지형 좀비 타입
};

UCLASS()
class AIBASICLESSON_API ASpartaAICharacter : public ACharacter
{
	GENERATED_BODY() 

public:
	ASpartaAICharacter();
	
	// 캐릭터의 이동 속도를 바꿔주는 커스텀 함수 
	// (AI 컨트롤러에서 이 함수를 불러서 평소엔 걷고(WalkSpeed), 추적할 땐 뛰게(RunSpeed) 만들기)
	void SetMovementSpeed(float NewSpeed); 

	UPROPERTY(EditAnywhere, Category = "AI")
	float WalkSpeed = 300.0f; // 평소 걷는 속도 (기본값 300cm/s)

	UPROPERTY(EditAnywhere, Category = "AI")
	float RunSpeed = 600.0f; // 타겟을 쫓아갈 때 달리는 속도 (기본값 600cm/s)

	// 에디터에서 '인간형'인지 '강아지형'인지 선택할 수 있게 해주는 변수
	UPROPERTY(EditAnywhere)
	EZombieType zombieType;

	// 중요: TMap은 맵(사전) 자료구조
	// Key(왼쪽)에 좀비 타입을 넣으면, 그에 맞는 Value(오른쪽) 스켈레탈 메시를 짝지어줌
	// 인간형을 고르면 사람을, 강아지형을 고르면 강아지를 입혀주기 위한 변수
	UPROPERTY(EditAnywhere)
	TMap<EZombieType, USkeletalMesh*> ZombieMeshes;

protected:
	virtual void BeginPlay() override;

	// 캐릭터에 이 컴포넌트가 달려있어야 다른 AI의 인지 시스템(AIPerception)에 자신을 등록할 수 있음
	UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "AI")
	UAIPerceptionStimuliSourceComponent* StimuliSource;

public:	
	virtual void Tick(float DeltaTime) override;

	virtual void SetupPlayerInputComponent(class UInputComponent* PlayerInputComponent) override;

};
  • SpartaAICharacter.cpp
#include "SpartaAICharacter.h"
#include "SpartaAIController.h"
#include "GameFramework/CharacterMovementComponent.h" // 캐릭터의 걷기 속도, 회전 등 이동을 제어하는 컴포넌트 헤더
#include "Perception/AISense_Sight.h" // 시각(Sight) 센서에 등록하기 위한 헤더

ASpartaAICharacter::ASpartaAICharacter()
{
	PrimaryActorTick.bCanEverTick = true;

	AIControllerClass = ASpartaAIController::StaticClass();
	// Character를 기본적으로 조종할 AIController 클래스를 지정

	AutoPossessAI = EAutoPossessAI::PlacedInWorldOrSpawned;
	// 레벨 배치 또는 Spawn 시 AIController가 자동으로 이 Character에 빙의하도록 설정

	// 부모가 기본으로 가지고 있는 이동 컴포넌트를 가져와서 Movement 포인터에 담음
	UCharacterMovementComponent* Movement = GetCharacterMovement();

	Movement->MaxWalkSpeed = WalkSpeed; // 캐릭터의 최대 걷기 속도를 헤더에서 정한 WalkSpeed로 세팅
	
	Movement->bOrientRotationToMovement = true;
	// Character가 이동하는 방향을 바라보도록 회전 설정
	
	Movement->RotationRate = FRotator(0.0f, 540.0f, 0.0f);
	// Character가 이동 방향으로 회전할 때의 속도 설정

	// 여기 있다고 신호 보낼 자극(Stimuli) 컴포넌트를 메모리에 생성
	StimuliSource = CreateDefaultSubobject<UAIPerceptionStimuliSourceComponent>(TEXT("StimuliSource"));
}

void ASpartaAICharacter::SetMovementSpeed(float NewSpeed)
{
	// 캐릭터의 이동 컴포넌트를 잘 가져왔다면
	if (UCharacterMovementComponent* Movement = GetCharacterMovement())
	{
		Movement->MaxWalkSpeed = NewSpeed; // 일단 NewSpeed로 세팅
		
		if (zombieType == EZombieType::HumanZombie) {
			// 인간형 좀비면 기본 걷기 속도(WalkSpeed * 1)로 설정
			Movement->MaxWalkSpeed = WalkSpeed * 1;
		}
		else if (zombieType == EZombieType::DogZombie) {
			// 강아지형 좀비면 인간형보다 2배 빠르게(WalkSpeed * 2) 달리도록 설정
			Movement->MaxWalkSpeed = WalkSpeed * 2;
		}
		
		// 로그에는 처음에 넘겨받은 NewSpeed를 출력
		UE_LOG(LogTemp, Warning, TEXT("[Sparta] Speed changed: %.1f"), NewSpeed);
	}
}

void ASpartaAICharacter::BeginPlay()
{
	Super::BeginPlay();
	SetMovementSpeed(100); 
	// 게임 시작하자마자 속도를 100으로 바꾸라고 명령 

	// 자극(Stimuli) 컴포넌트가 잘 생성되었다면
	if (StimuliSource)
	{
		// 1. '시각(Sight)' 센서에 감지되는 대상으로 등록
		StimuliSource->RegisterForSense(TSubclassOf<UAISense_Sight>());
		
		// 2. 전체 AI 인지 시스템에 내 존재를 공식적으로 등록
		StimuliSource->RegisterWithPerceptionSystem();
	}
}

void ASpartaAICharacter::Tick(float DeltaTime)
{
	Super::Tick(DeltaTime); 
}

void ASpartaAICharacter::SetupPlayerInputComponent(UInputComponent* PlayerInputComponent)
{
	Super::SetupPlayerInputComponent(PlayerInputComponent);
	// 플레이어의 키보드/마우스 입력을 처리하는 함수. 
}

** SpartaAIController 지정(디테일 창)

<zombie Type 설정>

이렇게 코드 너무 힘들게 쓰고 막 여러 줄 쓰면 가독성 떨어지고 -> 그래서 Behavior Tree 사용하면 좋음!

<BT 활용>

- 정리 -
1. 언리얼 AI 시스템의 역할 분담 (몸, 뇌, 도면, 메모리)

언리얼에서 AI를 만들 때는 기능을 하나의 코드에 다 몰아넣지 않고, 역할별로 분리

  • 캐릭터 (Character) = "몸통"
  • 역할: 이동, 충돌 처리, 애니메이션 재생 등 물리적인 행동을 담당
  • 특징: C++ 캐릭터 클래스 코드 안에는 복잡한 AI 로직을 넣지 않기. 컨트롤러가 누구인지 연결해 주는 정도의 기본 세팅만 존재하게끔!!
  • AI 컨트롤러 (AI Controller) = "뇌"
  • 역할: 캐릭터(몸통)에 빙의(Possess)하여 조종하는 찐주체
  • 특징: 스스로 생각하기보다 자신이 가지고 있는 행동 지침서(BT)를 보고 캐릭터에게 명령을 내림
  • Behavior Tree, BT = "행동 지침서 (도면도)"
  • 역할: AI가 상황에 따라 어떤 행동을 해야 할지 판단하는 의사결정 흐름도 (예: 적이 보이면 쫓아간다 -> 공격 사거리 안에 오면 때린다)
  • 특징: 복잡한 if-else 코드를 텍스트로 길게 쓰면 가독성이 떨어지므로 시각적인 노드 형태로 편하게 관리하고 연결해 주는 툴
  • 블랙보드 (Blackboard, BB) = "메모리 (기억 장치)"
  • 역할: BT가 결정을 내릴 때 참고하는 데이터 저장소
  • 특징: "플레이어의 현재 위치", "도망갈 목적지 좌표", "나의 현재 상태(공격 중, 순찰 중)" 같은 정보들이 변수 형태로 저장
  1. Behavior Tree(BT)를 사용하는 이유

  2. 스파게티 코드 방지: 행동 패턴이 복잡해질수록 C++나 블루프린트 코드로만 짜면 코드가 엄청나게 길어지고 읽기 힘들어지는데 이때 BT를 쓰면 흐름을 한눈에 파악할 수 있음

  3. 쉬운 디버깅 (시각적 추적): 게임을 실행해놓고 BT 창을 띄우면, AI가 현재 어떤 행동을 하고 있는지 불이 켜지며 실시간으로 보여줌. 문제가 터지면 트리의 어느 부분(어떤 행동)에서 막혔는지 바로 알 수 있음.

  4. 재사용성과 캡슐화: 사실 BT가 행동을 만들어주는 것은 아님. 세부적인 행동 코드(예: 공격하기, 주변 탐색하기)는 C++나 블루프린트로 각각 따로 짜야 함 (이게 Task). BT는 이렇게 만들어진 행동 블록들을 시각적으로 감싸서 배치하고 순서를 조립하는 관리 도구 느낌

  5. MoveTo는 Tick에 없는데 어떻게 매 프레임 쫓아갈까?

"MoveTo는 Tick에 넣어서 쓰는 게 아닌데 어떻게 매 프레임 쫓아가는 건가요?"

  • 내부적인 비동기(Asynchronous) 처리: MoveTo 명령이 떨어지면, 엔진 내부의 Navigation System이 "A에서 B로 가는 경로"를 계산
  • 엔진이 백그라운드에서 알아서 갱신: 목적지나 경로가 정해지면 언리얼 엔진의 PathFollowingComponent이 Tick처럼 매 프레임 위치를 갱신하며 캐릭터의 트랜스폼을 움직임

💡 최종 요약 테이블

구성 요소비유주요 역할
Character (Pawn)몸실제 월드에서 걷고, 부딪히고, 애니메이션을 재생함
AI Controller뇌캐릭터에 빙의하여 BT(행동 지침)를 실행시킴
Blackboard (BB)메모리타겟 위치, 현재 상태 등 판단에 필요한 데이터를 저장
Behavior Tree (BT)흐름도BB의 데이터를 바탕으로 어떤 행동을 할지 순서와 조건을 결정 (시각적 관리)
Task (C++/BP)실제 행동공격, 정찰 등 실질적인 특정 행동의 내부 로직 코드
profile
초보 개발자

0개의 댓글