Unreal 게임 개발 과정 TIL

Today's I Learning

본캠프 TIL

06.08 Unreal 본캠프) 언리얼 C++ Log 출력

Sunlight__ 2026. 6. 8. 15:11

 

오늘 배운 것

 

1. BeginPlay() 함수에 추가하기

void AItem::BeginPlay()
{
Super::BeginPlay();
// BeginPlay 호출 시점을 로그로 확인

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

item.cpp

 

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

 : 로그 카테고리 (Log Category) 여기서는 LogTemp라는 임시 카테고리를 사용했다.

   로그 수준 (Log Level) ; Warning을 사용하면, 노란색 글씨로 강조되어 출력된다.

   (이 외에도 Log, Display, Error 등 다양한 수준이 있다.)

      - Display : 일반적인 실행 흐름이나 상태 확인 메시지 (흰색)

      - Warning : 예상치 못한 동작이나 잠재적인 문제 (노란색)

      - Error : 즉시 수정이 필요한 심각한 문제 (빨간색)

 

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

 : 프로젝트 규모가 커질수록 모든 로그를 LogTemp로 찍으면 구분이 어렵다. 

   따라서 고유한 카테고리를 만들어 사용하는 것이 좋다.

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

item.h

 

 - DECLARE_LOG_CATEGORY_EXTERN(LogSparta, Warning, All)

 : 헤터 파일에서 로그 카테고리를 선언한다.

   LogSparta: 카테고리 이름 (사용자 지정)

   Warning : 이 카테고리를 사용할 때 기본적으로 Warning 이상의 로그만 출력하도록 설정

   All : 필요하면 나중에 모든 로그를 활성화 할 수 있도록 허용

 

void AItem::BeginPlay()
{
Super::BeginPlay();

// 기존 LogTemp
UE_LOG(LogTemp, Warning, TEXT("My Item appears!!"));

// 새로 만든 LogSparta
UE_LOG(LogSparta, Error, TEXT("My Log!!"));
}

item.cpp

 

  - DEFINE_LOG_CATEGORY(LogSparta)

 : .cpp 파일에서 로그 카테고리를 구현(정의)한다.

 - LogSparta라는 카테고리를 통해 로그 메시지를 좀 더 체계적으로 구분할 수 있다.

 

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

 : 언리얼 엔진에서 Actor는 게임 도중에 언제든지 생성(Spawn) 될 수 있고, 필요없어지면 파괴(Destroy) 될 수 있다.

   이를 Actor 라이프 사이클이라 부르며, 이 과정을 이해하면 게임 로직을 보다 효율적이고 안정적으로 작성할 수 있다.

 - 액터 라이프 사이클을 알아야 하는 이유
 : 초기화 시점 결정
      - 생성자 (Constructor), PostInitializeComponents, BeginPlay 등이 각각 언제 호출 되는지 알아야 적절한 곳에 코드를 배치 가능하다.
 : 성능 관리
      - 매 프레임마다 호출되는 Tick 함수는 비용이 클 수 있다.
      - 따라서 필요한 액터만 Tick을 활성화 하거나 이벤트 기반으로 전환해 최적화 해야한다.
 : 리소스 정리
      - 액터가 사라질 때 (EndPlay, Destroyed 등) 메모리를 해제하거나 특정 상태를 저장해야 할 수 있다.
      - 적절한 시기에 필요한 정리 작업을 하지 않으면 메모리 누수나 예외상황이 발생할 수 있다.
 - 주요 라이프 사이클 함수
 
: 언리얼 엔진의 Actor는 생성 -> 초기화 -> 월드 배치 -> Tick(실행) -> 제거 순으로 동작한다.
   이를 지원하기 위해 여러 함수가 자동으로 호출된다.
   1. 생성자 (Constructor)
      - 호출 시점 : C++ 클래스 객체가 메모리에 생성될 때 딱 한 번 호출된다.
      - 아직 액터가 월드 (World)에 완전히 등록되지 않은 상태이므로, 다른 액터나 월드 관련 기능을 안전하게 호출하기 어렵다.
      - 주로 CreateDefaultSubobject 등을 사용해 컴포넌트 생성 및 초기 변수 세팅에 활용한다.
   2. PstInitializeComponents()
      - 호출 시점 : 액터의 모든 컴포넌트가 생성/초기화 된 뒤 자동 호출된다.
      - 각 컴포넌트가 이미 준비된 상태이므로, 컴포넌트 간 상호작용이 필요한 코드를 배치하기 좋다.
      - 보통 생성자에서는 단순한 '생성/할당'만 하고, PostInitializeComponents()에서 컴포넌트들 사이의 의존 관계를 설정한다.
   3. BeginPlay()
      - 호출 시점 : Play In Editor (PIE)나 런타임에서 게임이 시작될 때, 혹은 이미 실행 중인 게임에 SpawnActor 등으로 새 액터가 생성될 때 한 번 호출된다.
      - 이 시점에서는 이미 월드와 다른 액터들이 준비된 상태이므로, 자유롭게 상호작요이 가능하다.
      - AI, 게임 모드, 플레이어 컨트롤러 등 다른 시스템과 연동도 주로 BeginPlay에서 초기화 한다.
      - 타이머, Delegate(Event) 바인딩 등을 시작하기에도 적합하다.
   4. Tick (float DeltaTime)
      - 호출 시점 : 매 프레임마다 호출
한다. (액터의 PrimaryActorTick.bCanEverTick = True; 설정 필요)
      
- 실시간 업데이트가 필요한 로직 (캐릭터 이동, 물리 연산, 카메라 추적 등)을 처리한다.
      - 이벤트 (Event) 기반으로 전환할 수 있는 부분은 Tick을 사용하지 않는 편이 성능에 유리하다.
   5. Destroy()
      - 호출 시점 : 
Destroy() 함수를 직접 호출하여 액터를 제거할 때 직전에 호출된다.
      (다만 레벨 전환이나 게임 종료 시에는 종종 건너뛰어지기도 하므로, 절대적으로 보장되는 것은 아니다.)
      - Destroy()가 불린 뒤에는 최종적으로 EndPlay도() 함께 호출된다.
      - 수동으로 액터를 제거할 때, 마지막 정리 코드를 넣을 수 있는 곳이다.
      (하지만, 게임 종료나 레벨 언로드 시에는 호출되지 않을 수 있기에, 모든 중요한 정리를 Destroy()에만 의존하면 놓치는 케이스가 생길 수 있다.)
      - 정리할 자원 예시
         : 수동 할당한 메모리 : new 또는 동적 할당한 오브젝트가 있다면 여기서  delete 하거나 해제한다.
         : 스폰된 자식 액터 : 이 액터가 생성한 다른 액터나 컴포넌트 중, 자동으로 해제되지 않는 것이 있다면 제거 처리한다.
         : Delegate / Event 바인딩 : 게임 전역적 또는 외부 클래스에 바인딩해둔 델리게이트가 있다면 해제된다.
         : 사운드/파티클 등 : 필요시 이 액터가 재생 중인 사운드나 파티클을 수동으로 정리한다.
   6. EndPlay(const EEndPlayReason::Type EndPlayReason)
      - 호출 시점 : 액터가 더이상 월드에서 활동하지 않게 될 때 호출 된다. (파괴, 레벨 전환, 게임 종료 등)
      - EEndPlayReason::Type 으로
어떤 이유로는 EndPlay가 호출 되었는지(파괴, 레벨 언로드, 게임 종료 등)를 구분한다.
      - 게임 종료나 레벨 언로드 같은 상황에서도 EndPlay()는 상대적으로 호출 보장이 높지만, Destroyed()는 건너뛸 수 있다.
      (따라서 중요한 정리 로직은 EndPlay()에 넣는 것이 보다 안전하다.)
      - 정리할 자원 예시
         : 타이머 : GetWorldTimerManager().ClearTimer(_) 와 같이 타이머를 정리한다.
         : 동적 할당 리소스 : 여전히 해제되지 않은 동적 메모리가 남아 있다면 여기서 정리한다.
         : 데이터 저장 : 게임 진행 상황 (점수, 인벤토리 등)파일/DB에 저장하거나, 상위 시스템에 콜백을 보내는 로직도 EndPlay에서 처리 가능하다.

 

추가적인 내용

 - GetName()

 : 현재 액터의 이름을 문자열로 반환해 준다.

   인스턴스마다 다른 이름이 자동으로 붙기 때문에 어떤 액터가 로그를 찍는지 식별하기 좋다.

 

오늘 배운 것에서 개인적으로 중요하다고 생각하는 것

 - DECLARE_LOG_CATEGORY_EXTERN(LogSparta, Warning, All)
: 헤더 파일에서 로그 카테고리를 선언한다.

  - DEFINE_LOG_CATEGORY(LogSparta)
: .cpp 파일에서 로그 카테고리를 구현(정의)한다.

 - 라이프 사이클 순서

 : 생성 -> 초기화 -> 월드 배치 -> Tick(실행) -> 제거
   생성자 (Constructor) -> PstInitializeComponents() -> BeginPlay() -> Tick (float DeltaTime) -> Destroy() / EndPlay()

 - EndPlay와 Destroyed의 차이점
 : EndPlay액터가 월드에서 사라지는 모든 상황 (게임 종료, 레벨 전환, Destroy 호출 등)에 대해 호출된다.
   Destroyed는 보통 Destroy() 함수가 명시적으로 불렸을 때만 호출되며, 게임 종료나 레벨 언로드 시에는 호출되지 않을 수 있다.