C++와 Unreal Engine으로 3D 게임 개발

C++와 Unreal Engine으로 3D 게임 개발 1-5

jh009 2026. 6. 7. 23:28

1-5. 언리얼 엔진 Actor의 라이프 사이클 이해하기

1. Actor 클래스에 로그 (Log) 추가하기

 

BeginPlay() 함수에 로그 추가하기

 

Item.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h"

UCLASS()
class SPARTAPROJECT_API AItem : public AActor
{
		GENERATED_BODY()
	
public:	
		AItem();

protected:
		USceneComponent* SceneRoot;
		UStaticMeshComponent* StaticMeshComp;
		
		// BeginPlay 함수를 다시 선언
		virtual void BeginPlay() override;
};

 

 

UE_LOG(LogTemp, Warning, TEXT("My Item appears!!"))

 

로그 카테고리 (Log Category): LogTemp 라는 임시 카테고리를 사용

로그 수준 (Log Level): Warning을 사용 → 노란색 글씨로 강조되어 출력

 

Log, Display, Error 등 다양한 수준이 있음

  • Display: 일반적인 실행 흐름이나 상태 확인 메시지 (흰색) 
  • Warning: 예상치 못한 동작이나 잠재적인 문제 (노란색) 
  • Error: 즉시 수정이 필요한 심각한 문제 (빨간색)

이 코드를 작성한 뒤 빌드 후 언리얼 에디터로 돌아오면 설정 완료

 

Item.cpp

#include "Item.h"

AItem::AItem()
{
		// Component 설정
		SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
		SetRootComponent(SceneRoot);
	
		StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
		StaticMeshComp->SetupAttachment(SceneRoot);
	
		static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/Resources/Props/SM_Chair.SM_Chair"));
		if (MeshAsset.Succeeded())
		{
			StaticMeshComp->SetStaticMesh(MeshAsset.Object);
		}
	
		static ConstructorHelpers::FObjectFinder<UMaterial> MaterialAsset(TEXT("/Game/Resources/Materials/M_Metal_Gold.M_Metal_Gold"));
		if (MaterialAsset.Succeeded())
		{
			StaticMeshComp->SetMaterial(0, MaterialAsset.Object);
		}
}

void AItem::BeginPlay()
{
		Super::BeginPlay();
		
		// BeginPlay 호출 시점을 로그로 확인
	  UE_LOG(LogTemp, Warning, TEXT("My Item appears!!"));
}

고유한 카테고리 정의하고 로그 추가하기

 

Item.h

 

프로젝트 규모가 커질수록 모든 로그를 LogTemp로 찍으면 구분이 어려워짐

DEFINE_LOG_CATEGORY 사용하여 고유한 카테고리를 만들어 사용하는 것이 좋음

→ 공용 유틸리티에 카테고리 추가해서 사용

 

DECLARE_LOG_CATEGORY_EXTERN(LogSparta, Warning, All);

헤더 파일에서 로그 카테고리를 선언

  • LogSparta: 카테고리 이름 (사용자 지정)
  • Warning: 카테고리를 사용할 때, 기본적으로 Warning 이상의 로그만 출력하도록 설정
  • All: 필요하면 나중에 모든 로그를 활성화할 수 있도록 허용
#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h"

// "LogSparta"라는 이름으로 로그 카테고리 선언
DECLARE_LOG_CATEGORY_EXTERN(LogSparta, Warning, All);

UCLASS()
class SPARTAPROJECT_API AItem : public AActor
{
		GENERATED_BODY()
	
public:	
		AItem();

protected:
		USceneComponent* SceneRoot;
		UStaticMeshComponent* StaticMeshComp;
		
		virtual void BeginPlay() override;
};

 

Item.cpp

 

DEFINE_LOG_CATEGORY(LogSparta);

  • .cpp 파일에서 로그 카테고리를 구현
  • LogSparta라는 카테고리를 통해 로그 메시지를 좀 더 체계적으로 구분 가능
#include "Item.h"

// "LogSparta" 카테고리 정의 (헤더에서 선언한 것을 실제로 구현)
DEFINE_LOG_CATEGORY(LogSparta);

AItem::AItem()
{
		// Component 설정
		SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
		SetRootComponent(SceneRoot);
	
		StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
		StaticMeshComp->SetupAttachment(SceneRoot);
	
		static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/Resources/Props/SM_Chair.SM_Chair"));
		if (MeshAsset.Succeeded())
		{
			StaticMeshComp->SetStaticMesh(MeshAsset.Object);
		}
	
		static ConstructorHelpers::FObjectFinder<UMaterial> MaterialAsset(TEXT("/Game/Resources/Materials/M_Metal_Gold.M_Metal_Gold"));
		if (MaterialAsset.Succeeded())
		{
			StaticMeshComp->SetMaterial(0, MaterialAsset.Object);
		}
}

void AItem::BeginPlay()
{
	Super::BeginPlay();
	
	// 기존 LogTemp
	UE_LOG(LogTemp, Warning, TEXT("My Item appears!!"));
	// 새로 만든 LogSparta
	UE_LOG(LogSparta, Error, TEXT("My Log!!"));
}

2. 언리얼 에디터에서 로그 확인하기

Output Log 창 열기

  • 언리얼 에디터 상단 메뉴 Window → Output Log
  • 너무 많은 기존 로그가 있다면: 우클릭 → Clear Log / 새로 발생하는 로그만 모아서 보는 것이 편리함


게임 시작 후 로그 확인

 

에디터 상단의 Play 버튼을 눌러 게임(플레이 모드)을 시작

BeginPlay()가 호출 → 작성한 "My Item appears!!" 메시지가 노란색, "My Log!!" 메시지가 빨간색으로 표시


로그 필터링 (Filtering)

더보기

TIP

개발 중에는 로그를 통해 디버깅하는 습관이 매우 중요

프로젝트 최적화 단계나 출시 단계가 되면 불필요한 로그는 제거하거나 로그 레벨을 낮춰야 함

→ 너무 많은 로그는 성능 저하를 일으키고, 민감한 정보가 노출될 위험도 있기 때문임

로그가 너무 많은 경우 

  • Output Log 창 상단 혹은 우측 상단 → Filters 메뉴를 이용해 카테고리별로 로그를 걸러낼 수 있음
  • 예시) Show All 옵션을 끄고 LogTemp 만 활성화하면 LogTemp 카테고리에 해당하는 로그만 볼 수 있게 

 

필터 목록에 LogTemp 자체가 보이지 않는 경우

  • 아직 한 번도 해당 카테고리의 로그가 출력되지 않았을 수 있음
  • 그가 최소 한 번 발생해야 목록에서 필터링할 수 있음

3. 언리얼 엔진 Actor의 라이프 사이클 이해하기

액터 라이프 사이클을 알아야 하는 이유

 

초기화 시점 결정

  • 생성자 (Constructor), PostInitializeComponents, BeginPlay 등이 각각 언제 호출되는지 알아야 적절한 곳에 코드를 배치할 수 있음
  • 예시) 컴포넌트 생성(CreateDefaultSubobject)은 생성자에서, 다른 액터 참조나 월드 접근은 BeginPlay에서 처리

성능 관리

  • 매 프레임마다 호출되는 Tick 함수는 비용이 클 수 있음
  • 따라서 필요한 액터만 Tick 을 활성화하거나 이벤트 기반으로 전환해 최적화를 해야함

리소스 정리

  • 액터가 사라질 때 (EndPlay, Destroyed 등) 메모리를 해제하거나 특정 상태를 저장해야 할 수 있음
  • 적절한 시점에 필요한 정리 작업을 하지 않으면 메모리 누수예외 상황이 발생할 수 있음

주요 라이프 사이클 함수

 

언리얼 엔진의 Actor 동작 순서

생성 → 초기화 → 월드 배치 → Tick(실행) → 제거 / 이를 지원하기 위해 여러 함수가 자동 호출

 

1. 생성자 (Constructor)

  •  메모리에 생성될 때 딱 한 번 호출

2. PostInitializeComponents()

  • 액터의 모든 컴포넌트가 생성·초기화된 뒤 자동 호출
  • 컴포넌트끼리 데이터 주고받기, 상호작용 

3. BeginPlay()

  • 배치 (Spawn) 직후 호출
  • 이 시점에는 이미 월드와 다른 액터들이 준비된 상태 → 자유롭게 상호작용이 가능

4. Tick(float DeltaTime) 

  • 매 프레임마다 호출 (액터의 PrimaryActorTick.bCanEverTick = true; 설정 필요)
  • 실시간 업데이트가 필요한 로직 (캐릭터 이동, 물리 연산, 카메라 추적 등)을 처리
  • 성능이 떨어질 수 있기 때문에 신중하게 사용하기 

5. Destroyed()

  • Destroy() 함수를 직접 호출 → 액터를 제거하는 직전에 호출
  • 레벨 전환이나 게임 종료 시에는 종종 건너뛰어지기도 하기 때문에 절대적으로 보장되는 것은 X
  • Destroyed()가 불린 뒤에는 최종적으로 EndPlay()도 함께 호출
  • 수동으로 액터를 제거할 때, 마지막 정리 코드를 넣을 수 있는 곳
  • 게임 종료나 레벨 언로드 시에는 호출되지 않을 수 있음
  • → 모든 중요한 정리를 Destroyed()에만 의존하면 놓치는 케이스가 생길 수 있음

정리할 자원 예시

  • 수동 할당한 메모리: new 또는 동적 할당한 오브젝트가 있다면 여기서 delete하거나 해제
  • 스폰된 자식 액터: 이 액터가 생성한 다른 액터나 컴포넌트 중, 자동으로 해제되지 않는 것이 있다면 제거 처리
  • Delegate / Event 바인딩: 게임 전역적 또는 외부 클래스에 바인딩해둔 델리게이트가 있다면 해제
  • 사운드/파티클 등: 필요 시 이 액터가 재생 중인 사운드나 파티클을 수동으로 정리

6. EndPlay(const EEndPlayReason::Type EndPlayReason)

  • 액터가 더 이상 월드에서 활동하지 않게 될 때 호출 (파괴, 레벨 전환, 게임 종료 등)
  • EEndPlayReason::Type으로 어떤 이유로 EndPlay가 호출되었는지(파괴, 레벨 언로드, 게임 종료 등)를 구분
  • 게임 종료나 레벨 언로드 같은 상황에서도 EndPlay()는 상대적으로 호출 보장 ↑ / Destroyed()는 건너뛸 수 있음
  • 중요한 정리 로직 (자원 해제, Timer 해제, 상태 저장 등)은 EndPlay()에 넣는 것이 보다 안전

정리할 자원 예시

  • 타이머: GetWorldTimerManager().ClearTimer(…) 와 같이 타이머를 정리
  • 동적 할당 리소스: 여전히 해제되지 않은 동적 메모리가 남아 있다면 여기서 정리
  • 데이터 저장: 게임 진행 상황 (점수, 인벤토리 등)을 파일/DB에 저장 / 상위 시스템에 콜백을 보내는 로직도 EndPlay 에서 처리 가능

4. 라이프 사이클 함수에 로그 추가하기

헤더에 라이프 사이클 함수 선언

 

Item.h

#pragma once

#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "AItem.generated.h"

DECLARE_LOG_CATEGORY_EXTERN(LogSparta, Warning, All);

UCLASS()
class SPARTAPROJECT_API AItem : public AActor
{
    GENERATED_BODY()

public:
    AItem();

protected:
		USceneComponent* SceneRoot;
		UStaticMeshComponent* StaticMeshComp;

    // 라이프 사이클 함수들
    virtual void PostInitializeComponents() override;
    virtual void BeginPlay() override;
    virtual void Tick(float DeltaTime) override;
    virtual void Destroyed() override;
    virtual void EndPlay(const EEndPlayReason::Type EndPlayReason) override;
};

CPP 파일에서 각 함수 구현 및 로그 출력

 

Item.cpp

 

GetName()

  • 현재 액터의 이름을 문자열로 반환
  • 인스턴스마다 다른 이름이 자동으로 붙기 때문에 어떤 액터가 로그를 찍는지 식별하기 좋음

Tick 

  • Tick 함수는 매 프레임 호출 →  로그를 찍으면 메시지가 많아짐
  • 디버깅이 필요한 순간에만 제한적으로 사용하기
  • 조건문을 통해 특정 상황에서만 로그를 찍는 것이 좋음

Destroyed / EndPlay 

  • Destroyed는 보통 Destroy() 함수가 명시적으로 불렸을 때만 호출
  • 게임 종료나 레벨 언로드 시에는 호출되지 않을 수 있음
  • EndPlay는 액터가 월드에서 사라지는 모든 상황 (게임 종료, 레벨 전환, Destroy 호출 등)에 대해 호출
#include "Item.h"

DEFINE_LOG_CATEGORY(LogSparta);

AItem::AItem()
{
    // Tick을 활성화해, 매 프레임 Tick 함수가 호출되도록 설정
    PrimaryActorTick.bCanEverTick = true;
    
		// Component 설정
		SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot"));
		SetRootComponent(SceneRoot);
	
		StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh"));
		StaticMeshComp->SetupAttachment(SceneRoot);
	
		static ConstructorHelpers::FObjectFinder<UStaticMesh> MeshAsset(TEXT("/Game/Resources/Props/SM_Chair.SM_Chair"));
		if (MeshAsset.Succeeded())
		{
			StaticMeshComp->SetStaticMesh(MeshAsset.Object);
		}
	
		static ConstructorHelpers::FObjectFinder<UMaterial> MaterialAsset(TEXT("/Game/Resources/Materials/M_Metal_Gold.M_Metal_Gold"));
		if (MaterialAsset.Succeeded())
		{
			StaticMeshComp->SetMaterial(0, MaterialAsset.Object);
		}
    
    UE_LOG(LogSparta, Warning, TEXT("%s Constructor"), *GetName());
}

void AItem::PostInitializeComponents()
{
    Super::PostInitializeComponents();
    
    UE_LOG(LogSparta, Warning, TEXT("%s PostInitializeComponents"), *GetName());
}

void AItem::BeginPlay()
{
    Super::BeginPlay();
    
    UE_LOG(LogSparta, Warning, TEXT("%s BeginPlay"), *GetName());
}

void AItem::Tick(float DeltaTime)
{
    Super::Tick(DeltaTime);
    
    // Tick 함수는 매 프레임마다 호출되므로
    // 여기서 로그를 찍으면 방대한 양의 메시지가 쌓일 수 있음.
    // 필요시 디버깅용 코드만 간단히 작성하거나, 별도 조건을 걸어 사용.
}

void AItem::Destroyed()
{
    UE_LOG(LogSparta, Warning, TEXT("%s Destroyed"), *GetName());
    
    Super::Destroyed();
}

void AItem::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
    UE_LOG(LogSparta, Warning, TEXT("%s EndPlay"), *GetName());
    
    Super::EndPlay(EndPlayReason);
}

라이프 사이클 로깅 결과

 

  • LogSparta 로그만 출력되도록 로그 필터링을 설정
  • 게임이 진행되는 동안 매 프레임마다 Tick 함수가 호출
  • 게임을 종료하면 EndPlay 가 호출되는 것을 확인

 

실행 도중 Shift + F1 → Outliner 창에서 Item 액터를 삭제 → Destroyed 로그 출력 / EndPlay가 출력

 

액터가 월드에 등장하여 → 동작하고 → 제거되는 흐름을 로그로 확인 가능