언리얼

언리얼C++) 액터 라이프사이클

eclipse2 2026. 4. 8. 22:20

로그 출력 활용법과 액터 생명주기의 중요성

1. UE_LOG를 활용한 디버깅

로그는 코드의 실행 흐름을 파악하고 오류를 잡는 데 필수적

기본 사용법

UE_LOG(LogTemp, Warning, TEXT("My Item appears!!"));
  • 로그 카테고리: LogTemp는 특정 카테고리가 지정되지 않았을 때 쓰는 임시 카테고리
  • 로그 수준(Verbosity):
    • Display: 일반적인 정보 확인용 (흰색)
    • Warning: 잠재적 문제 경고 (노란색)
    • Error: 심각한 오류 발생 (빨간색)
  • TEXT("메시지"): 유니코드 문자열 형식을 사용합니다.

고유 카테고리 정의 (심화)

프로젝트가 커지면 LogTemp 대신 전용 카테고리를 만들어 로그를 필터링하는 것이 좋다.

  1. 헤더 파일(.h): 카테고리 선언
  2. C++
     
    DECLARE_LOG_CATEGORY_EXTERN(LogSparta, Warning, All);
    
  3. 소스 파일(.cpp): 카테고리 정의
  4. C++
     
    DEFINE_LOG_CATEGORY(LogSparta);
    
  5. 사용: UE_LOG(LogSparta, Display, TEXT("Sparta Category Log!"));

2. 액터 라이프 사이클 이해가 중요한 이유

단순히 함수 순서를 외우는 것보다 왜 이 시점에 이 코드를 넣어야 하는가?를 아는 것이 중요

① 초기화 시점 결정 (Initialization Timing)

액터의 상태에 따라 할 수 있는 일이 단계별로 다름.

  • 생성자: 컴포넌트 생성(CreateDefaultSubobject)과 기본 변수 세팅만 수행. (월드 접근 불가)
  • BeginPlay: 월드가 준비된 상태이므로 다른 액터를 참조하거나 게임 플레이 로직 시작.

② 성능 관리 (Performance Optimization)

  • Tick 함수는 매 프레임 호출되므로 CPU 비용이 많이 발생
  • 반드시 실시간 업데이트가 필요한 액터만 bCanEverTick = true를 설정하고, 나머지는 **이벤트 기반(Event-driven)**으로 설계하여 최적화해야 함

③ 리소스 정리 (Resource Management)

액터가 파괴되거나 레벨이 넘어갈 때 정리를 제대로 하지 않으면 **메모리 누수(Memory Leak)**가 발생

  • EndPlay나 Destroyed에서 동적 메모리 해제, 타이머 중지, 이벤트 바인딩 해제 등을 처리하여 안정성을 확보

주요 함수들

1단계: 생성 및 초기화 (Spawn & Initialization)

1. 생성자 (Constructor)

  • 호출 시점: C++ 클래스 객체가 메모리에 생성될 때 단 한 번 호출
  • 특징: 액터가 아직 월드(World)에 완전히 등록되지 않은 상태.
    • 월드 관련 기능이나 다른 액터를 참조하는 로직은 안전하지 않다.
  • 주요 작업: CreateDefaultSubobject를 이용한 컴포넌트 생성.
    • 초기 변수 및 기본값 세팅.

2. PostInitializeComponents()

  • 호출 시점: 액터에 포함된 모든 컴포넌트가 생성 및 초기화된 직후에 호출
  • 특징: 생성자에서는 단순 '할당'만 했다면, 여기서는 컴포넌트들이 준비되었으므로 서로를 참조하거나 상호작용할 수 있다.
  • 주요 작업: 컴포넌트 간 의존성 설정 및 초기화 로직.

2단계: 게임 시작 및 업데이트 (Gameplay)

3. BeginPlay()

  • 호출 시점: 게임이 시작될 때(PIE 포함) 또는 게임 도중 액터가 SpawnActor로 월드에 배치될 때 한 번 호출
  • 특징: * 액터가 월드에 완전히 안착한 상태입니다
    • 다른 액터, 게임 모드, 플레이어 컨트롤러 등 외부 시스템과 자유롭게 소통할 수 있다.
  • 주요 작업: 타이머 설정, 델리게이트(이벤트) 바인딩, 게임 로직의 실질적인 시작.

4. Tick(float DeltaTime)

  • 호출 시점: 매 프레임마다 호출
  • 활성화 조건: PrimaryActorTick.bCanEverTick = true; 설정이 필요
  • 특징: 실시간 연산(이동, 물리 계산 등)이 필요할 때 사용하지만, 너무 많은 액터가 Tick을 쓰면 성능이 저하
  • Tip: 가능하다면 이벤트 기반(Event-driven) 로직으로 대체하는 것이 성능 최적화에 유리

3단계: 소멸 및 정리 (Destruction & Cleanup)

5. Destroyed()

  • 호출 시점: Destroy() 함수를 호출하여 액터를 수동으로 제거할 때 직전에 호출
  • 주의사항: 레벨 전환이나 게임 종료 시에는 호출되지 않을 수 있으므로, 모든 정리 로직을 여기에만 의존해서는 안 됨.
  • 주요 작업: 수동 할당 메모리 해제, 자식 액터 제거, 사운드/파티클 중지.

6. EndPlay(const EEndPlayReason::Type EndPlayReason)

  • 호출 시점: 액터가 월드에서 사라지는 모든 순간에 호출 (액터 파괴, 레벨 전환, 게임 종료 등)
  • 특징: EEndPlayReason 파라미터를 통해 왜 종료되는지 이유를 구분할 수 있어 매우 유연
  • 주요 작업: (가장 권장되는 정리 장소)
    • 타이머 해제 (ClearTimer)
    • 데이터 저장 (점수, 상태 등)
    • 남아있는 동적 리소스 정리

 

단계 함수명 주요 역할 비고
생성 Constructor 컴포넌트 생성, 기본값 설정 월드 참조 불가
초기화 PostInitializeComponents 컴포넌트 간 상호작용 설정 컴포넌트 준비 완료
시작 BeginPlay 실질적인 게임 로직 시작 월드/액터 참조 가능
실행 Tick 매 프레임 업데이트 성능 유의
제거 Destroyed 수동 제거 시 호출 종료/레벨 전환 시 미호출 가능
종료 EndPlay 최종 자원 정리, 타이머 해제 가장 안전한 정리 시점

 

코드

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);
}
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);
	
}

 

Transform 조작과 프레임 독립적인 움직임

1. Transform(트랜스폼)의 이해

액터가 월드에 존재하기 위한 3가지 필수 데이터

  • 위치 (Location): 월드 내의 X, Y, Z 좌표 (FVector 사용)
  • 회전 (Rotation): 액터가 바라보는 방향 (FRotator 사용)
    • Roll: X축 기준 회전 (좌우 기울기)
    • Pitch: Y축 기준 회전 (위아래 기울기)
    • Yaw: Z축 기준 회전 (좌우 회전)
  • 스케일 (Scale): 액터의 크기 비율 (1.0이 기본 크기)

2. 좌표계: 월드(World) vs 로컬(Local)

  1. 월드 좌표계 (World Space): 게임 전체 세계의 절대적인 중심(0,0,0)을 기준으로 함
  2. 로컬 좌표계 (Local Space): 액터 자신이나 부모 컴포넌트를 기준으로 한 상대적 좌표
    • 부모-자식 관계: 부모가 움직이면 자식은 자신의 '상대적 위치'를 유지하며 함께 움직인다.
    • SetRelativeLocation() 등을 통해 부모 기준 위치를 조정할 수 있다.

3. C++로 Transform 조작하기

주요 함수

  • GetActorLocation() / SetActorLocation(): 위치 가져오기 및 설정
  • SetActorRotation(): 회전 설정
  • SetActorScale3D(): 크기 설정 (FVector(2.0f) 처럼 단일 값 입력 시 모든 축 적용)

적용 예시 (BeginPlay)

게임이 시작될 때 액터의 초기 위치와 크기를 강제로 설정하는 코드

void AItem::BeginPlay()
{
    Super::BeginPlay();
    SetActorLocation(FVector(300.f, 200.f, 100.f)); // 특정 위치로 이동
    SetActorRotation(FRotator(0.f, 45.f, 0.f));     // 45도 회전
    SetActorScale3D(FVector(2.0f));                 // 2배 크기로 확대
}

 

4. Tick 함수와 DeltaTime

Tick(float DeltaTime)이란?

액터가 매 프레임마다 실행하는 함수입니다. 실시간으로 변하는 로직(회전하는 아이템, 움직이는 캐릭터 등)을 구현할 때 사용

  • 활성화 방법: 생성자에서 PrimaryActorTick.bCanEverTick = true; 설정 필수.

왜 DeltaTime을 곱해야 할까? (프레임 독립성)

컴퓨터마다 성능(FPS)이 다름

  • 60 FPS: 1초에 Tick이 60번 호출
  • 120 FPS: 1초에 Tick이 120번 호출

단순히 위치 += 1을 하면 고사양 컴퓨터에서 캐릭터가 2배 더 빨리 움직이게 됨 이를 방지하기 위해 마지막 프레임에서 현재 프레임까지 걸린 시간인 DeltaTime을 곱해줌

공식: 이동 거리 = 속도 * DeltaTime

 

코드

// 생성자에서 Tick 활성화
AItem::AItem() {
    PrimaryActorTick.bCanEverTick = true;
    RotationSpeed = 90.0f; // 초당 90도 회전 설정
}

void AItem::Tick(float DeltaTime) {
    Super::Tick(DeltaTime);

    // DeltaTime을 곱해 사양에 관계없이 초당 RotationSpeed만큼 회전
    if (!FMath::IsNearlyZero(RotationSpeed)) {
        AddActorLocalRotation(FRotator(0.0f, RotationSpeed * DeltaTime, 0.0f));
    }
}

 

  • AddActorLocalRotation: 현재 회전 값에 특정 값을 더하는 함수.
  • FMath::IsNearlyZero: 부동소수점 오차를 고려해 안전하게 0인지 확인

 

주의: 회전 시 gizmo를 기준으로 해서 원하는 회전 형식이 아닐 수 있음. 따라서 Modeling Mode에 들어가 edit pivot을 눌러 조정 가능

 

리플렉션 시스템과 C++ & 블루프린트

1. 블루프린트 vs C++ : 어떤 것을 써야 할까?

실무에서는 두 방식을 섞어서 사용하는 하이브리드 워크플로우가 일반적

구분 블루프린트 (Blueprint) C++ (Native Code)
특징 노드 기반 시각적 스크립팅 텍스트 기반 네이티브 프로그래밍
장점 빠른 프로토타이핑, 직관적, 쉬운 수정 최고의 성능, 복잡한 연산 최적화, 외부 라이브러리 확장
단점 복잡해지면 가독성 저하(스파게티 코드), 성능 병목 컴파일 대기 시간, 높은 진입 장벽
용도 UI 로직, 간단한 이벤트 연출, 기획자 수정용 변수 게임 엔진 확장, 물리/AI 연산, 핵심 프레임워크 설계

2. 리플렉션 시스템(Reflection System)이란?

C++는 원래 자신이 어떤 변수와 함수를 가졌는지 스스로 알지 못함(런타임 기준). 하지만 언리얼 엔진은 리플렉션 시스템을 통해 C++ 클래스 정보를 메타데이터로 저장하여, 에디터와 블루프린트가 이 정보를 읽고 조작할 수 있게 만듦

필수 구성 요소

  • UCLASS(): 클래스를 리플렉션 시스템에 등록.
  • GENERATED_BODY(): 리플렉션에 필요한 필수 코드를 자동 생성하여 삽입.
  • #include "FileName.generated.h": 반드시 헤더 파일의 가장 마지막에 위치해야 하는 자동 생성 파일.

3. 변수를 에디터에 노출하기: UPROPERTY()

변수 위에 매크로를 붙여 에디터에서의 노출 범위와 권한을 설정.

주요 지정자

  1. 편집 권한
    • EditAnywhere: 클래스 기본값과 배치된 인스턴스 모두 수정 가능.
    • VisibleAnywhere: 보기만 가능하고 수정 불가(주로 컴포넌트 참조 변수에 사용).
    • EditDefaultsOnly: 원본 블루프린트(클래스 기본값)에서만 수정 가능.
  2. 블루프린트 접근
    • BlueprintReadWrite: 블루프린트 노드에서 읽기/쓰기 가능.
    • BlueprintReadOnly: 읽기 전용 Getter만 생성.
  3. 기타
    • Category="이름": 디테일 패널에서 변수를 그룹화하여 정리.
    • meta=(AllowPrivateAccess="true"): private 변수여도 에디터 노출 허용.

4. 함수를 블루프린트에서 호출하기: UFUNCTION()

함수 또한 리플렉션에 등록하여 블루프린트 노드로 사용할 수 있다.

주요 지정자

  • BlueprintCallable: 블루프린트에서 호출할 수 있는 일반적인 실행 노드로 생성.
  • BlueprintPure: 실행 핀(하얀색 선) 없이 값만 반환하는 노드(Getter 등).
  • BlueprintImplementableEvent: C++에서 선언만 하고, 실제 구현은 블루프린트에서 담당. (C++에서 특정 시점에 이벤트를 발생시킬 때 유용)


5. 실전 코드 예시

C++
 
// Item.h
UCLASS(Blueprintable)
class AItem : public AActor {
    GENERATED_BODY()

protected:
    // 에디터에서 메시를 선택할 수 있게 노출
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category="Item|Components")
    UStaticMeshComponent* StaticMeshComp;

    // 회전 속도 (기본값만 수정 가능)
    UPROPERTY(EditDefaultsOnly, Category="Item|Properties")
    float RotationSpeed;

    // C++에서 호출하지만 동작은 블루프린트에서 정의
    UFUNCTION(BlueprintImplementableEvent, Category="Item|Events")
    void OnItemPickedUp();

    // 블루프린트에서 호출 가능한 함수
    UFUNCTION(BlueprintCallable, Category="Item|Actions")
    void ResetPosition();
};