오늘의 할일
1. 코드카타 정리
2. 세션 정리
자연수 n을 3진법 상에서 앞뒤로 뒤집은 후, 이를 다시 10진법으로 표현한 수를 return
이면 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진법이라 알려주고 숫자로 바꾼다"
PlayerState
GameState - 전역 정보를 저장하는 곳
SpartaCharacter
#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)
UGameplayStatics::ApplyDamage
공격자가 데미지를 줄 대상 액터와 데미지 양, 데미지를 유발한 주체 등 인자로 넘겨 호출
대상 액터의 TakeDamage() 함수를 호출 시도
AActor::TakeDamage
Actor의 가상 함수. 모든 액터가 기본적으로 이 함수를 가지고 있고 필요하다면 자식 클래스(캐릭터 등)에서 오버라이드.
#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() 실행
#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();
}
#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();
}
}
GameMode - 게임의 규칙
멀티플레이에서는 서버 전용으로 동작
어떤 캐릭터를 스폰할지, 플레이어가 사망했을 때 어떻게 처리할지를 결정
GameState - 공유되어야 하는 전역 상태, 레벨당 1개 존재
전역 데이터 관리용
점수, 남은 시간, 현재 게임 단계(Phase), 스폰된 오브젝트의 총 개수 등
멀티플레이 - 서버가 관리
싱글 플레이 : 전역적으로 공유해야 할 정보를 한 군데서 관리하면 유지보수가 더 편함. “몇 개의 아이템이 스폰되었는지”, “현재 게임 진행도는 어느 정도인지” 같은 데이터를 일괄 관리할 수 있기 때문
전역 점수를 GameState 에 저장
점수를 획득할 때마다 AddScore() 호출 -> 점수가 증가
GameStateBase 클래스 생성(SpartaGameStateBase)
#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);
};
#include "SpartaGameStateBase.h"
ASpartaGameStateBase::ASpartaGameStateBase()
{
Score = 0;
}
int32 ASpartaGameStateBase::GetScore() const
{
return Score;
}
void ASpartaGameStateBase::AddScore(int32 Amount)
{
Score += Amount;
}
#include "SpartaGameMode.h"
#include "SpartaCharacter.h"
#include "SpartaPlayerController.h"
#include "SpartaGameStateBase.h"
ASpartaGameMode::ASpartaGameMode()
{
DefaultPawnClass = ASpartaCharacter::StaticClass();
PlayerControllerClass = ASpartaPlayerController::StaticClass();
// 우리가 만든 GameState로 설정
GameStateClass = ASpartaGameStateBase::StaticClass();
}
#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();
}
}
플레이어 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
#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)
};
#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);
}
}
#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;
};
#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를 만들 때는 기능을 하나의 코드에 다 몰아넣지 않고, 역할별로 분리
Behavior Tree(BT)를 사용하는 이유
스파게티 코드 방지: 행동 패턴이 복잡해질수록 C++나 블루프린트 코드로만 짜면 코드가 엄청나게 길어지고 읽기 힘들어지는데 이때 BT를 쓰면 흐름을 한눈에 파악할 수 있음
쉬운 디버깅 (시각적 추적): 게임을 실행해놓고 BT 창을 띄우면, AI가 현재 어떤 행동을 하고 있는지 불이 켜지며 실시간으로 보여줌. 문제가 터지면 트리의 어느 부분(어떤 행동)에서 막혔는지 바로 알 수 있음.
재사용성과 캡슐화: 사실 BT가 행동을 만들어주는 것은 아님. 세부적인 행동 코드(예: 공격하기, 주변 탐색하기)는 C++나 블루프린트로 각각 따로 짜야 함 (이게 Task). BT는 이렇게 만들어진 행동 블록들을 시각적으로 감싸서 배치하고 순서를 조립하는 관리 도구 느낌
MoveTo는 Tick에 없는데 어떻게 매 프레임 쫓아갈까?
"MoveTo는 Tick에 넣어서 쓰는 게 아닌데 어떻게 매 프레임 쫓아가는 건가요?"
MoveTo 명령이 떨어지면, 엔진 내부의 Navigation System이 "A에서 B로 가는 경로"를 계산PathFollowingComponent이 Tick처럼 매 프레임 위치를 갱신하며 캐릭터의 트랜스폼을 움직임💡 최종 요약 테이블
| 구성 요소 | 비유 | 주요 역할 |
|---|---|---|
| Character (Pawn) | 몸 | 실제 월드에서 걷고, 부딪히고, 애니메이션을 재생함 |
| AI Controller | 뇌 | 캐릭터에 빙의하여 BT(행동 지침)를 실행시킴 |
| Blackboard (BB) | 메모리 | 타겟 위치, 현재 상태 등 판단에 필요한 데이터를 저장 |
| Behavior Tree (BT) | 흐름도 | BB의 데이터를 바탕으로 어떤 행동을 할지 순서와 조건을 결정 (시각적 관리) |
| Task (C++/BP) | 실제 행동 | 공격, 정찰 등 실질적인 특정 행동의 내부 로직 코드 |