Game Development/Unreal Engine

[UE5] Actor Lifecycle (액터 생애주기)

Dlaiml 2024. 5. 17. 13:21

Actor Lifecycle

언리얼 엔진에서 기능을 추가할 때, 어느 클래스에 해당 기능을 추가할 것인가, 해당 클래스의 어느 함수를 Override하고 그 함수의 호출 시점은 어떻게 되는가는 항상 고민하는 부분이다.

 

레벨에 배치할 수 있는 오브젝트인 Actor 클래스의 사전 정의된 함수의 호출, 목적에 대한 이해가 필요하면 이를 결정하는 것이 더 수월해질 것이라고 생각하여 이번 포스팅은  Actor의 Lifecycle에 대한 포스팅이다.

 

 

언리얼 엔진 공식문서의 Actor의 Lifecycle을 도식화한 사진이다.

시작점이 Play in Editor, LoadMap(AddToWorld), SpawnActor, SpawnActorDeferred로 4개가 있다.

Empty Level을 만들고 Actor를 상속한 C++ Class에서 Log를 찍어보며 하나씩 자세히 알아보자.

UE documentation Actor Lifecycle

 

호출 함수의 이름, 객체의 주소값을 로깅할 수 있도록 커스텀 로그 매크로를 정의하였다.

// ActorLifecycle.h
#pragma once

#include "CoreMinimal.h"

DECLARE_LOG_CATEGORY_EXTERN(ActorLifeCycle, Log, All); 
#define CLog(Msg, Addr) UE_LOG(ActorLifeCycle, Warning, TEXT("[%s](%s): %s"), *FString(__FUNCTION__), *FString::Printf(TEXT("%p"), Addr), *FString(Msg));


// ActorLifecycle.cpp

#include "ActorLifecycle.h"
#include "Modules/ModuleManager.h"

IMPLEMENT_PRIMARY_GAME_MODULE( FDefaultGameModuleImpl, ActorLifecycle, "ActorLifecycle" );

DEFINE_LOG_CATEGORY(ActorLifeCycle);

 

Play in Editor

 

1. Editor에 있는 Actor들은 New World로 복사되고 UObject::PostDuplicate가 호출된다.

2. 이후 Actor 들이 gameplay를 시작하기 위한 UAISystemBase::InitializeActorsForPlay가 world에 의해 호출된다.

3. 초기화되지 않은 Actor에 ULevel::RouteActorInitialize를 호출

  3.1 AActor::PreInitializeComponents -> UActorComponent::InitalizeComponent -> AActor::PostInitializeComponents

4. 레벨이 시작되었다는 AActor::BeginPlay를 호출한다.

 

 

이제 아무 Actor가 배치되지 않은 Empty Level에서 Actor 클래스를 상속하여 만든 LifecycleBreakDown Actor의 Constructor, PostDuplicate, BeginPlay에 아래와 같이 함수 시작, 끝에 로깅하도록 하고 빌드를 시작해보자

void ALifecycleBreakDown::PostDuplicate(bool bDuplicateForPIE)
{
	CLog("START", this);
	Super::PostDuplicate(bDuplicateForPIE);
	CLog("END",this);
}

 

 

빌드 후 에디터를 실행하자 로그에 이미 Actor의 생성자 함수가 호출된 것을 볼 수 있었다.

ActorLifecycle: Warning: [ALifecycleBreakDown::ALifecycleBreakDown](0000091767DEAA00): START
ActorLifecycle: Warning: [ALifecycleBreakDown::ALifecycleBreakDown](0000091767DEAA00): END

 

이는 Class Default Object(CDO)와 관련이 있다. 이번 포스팅에서 다루기에는 너무 방대하여 간단하게 설명하자면 언리얼 엔진의 Object인 UObject는 Default Object인 기본 세팅으로 초기화 된 CDO를 미리 만들어 두고 이를 복사하는 방식으로 다른 Instance를 만든다.

 

위 로그에 찍힌 주소는 에디터를 초기화하면서 Actor의 CDO의 주소값으로 이를 확인하기 위해 아래 코드를 BeginPlay 함수 가장 윗부분에 추가하였다.

* 0000091767DEAA00 주소를 기억하자

UE_LOG(LogTemp, Error, TEXT("[%s]: %s"), TEXT("CDO"), *FString::Printf(TEXT("%p"), GetDefault<ALifecycleBreakDown>()));

 

만든 Actor를 Viewport로 Drag&Drop 후 로그는 아래와 같다

ActorLifeCycle: Warning: [ALifecycleBreakDown::ALifecycleBreakDown](000009177AA8DC00): START
ActorLifeCycle: Warning: [ALifecycleBreakDown::ALifecycleBreakDown](000009177AA8DC00): END
ActorLifeCycle: Warning: [ALifecycleBreakDown::ALifecycleBreakDown](000009177AADCD00): START
ActorLifeCycle: Warning: [ALifecycleBreakDown::ALifecycleBreakDown](000009177AADCD00): END

 

Viewport로 Drag 할 때, 생성자가 한 번 호출되고 Drop 하면 다른 Instance가 생성된다.

https://forums.unrealengine.com/t/spawning-an-actor-in-postactorcreated-creates-two-of-them/340109/11 

이에 대해 찾아보니 Preview Asset과 배치하였을 때 Asset이 다르기 때문이라고 한다. 이에 대해 더 자세히 보려면 Editor 내에서 동작에 따라 호출되는 함수들에 Log를 찍어보면 확인이 가능하다.

 

ActorLifeCycle: Warning: [ALifecycleBreakDown::ALifecycleBreakDown](000009177D157300): START
ActorLifeCycle: Warning: [ALifecycleBreakDown::ALifecycleBreakDown](000009177D157300): END
ActorLifeCycle: Warning: [ALifecycleBreakDown::PostDuplicate](000009177D157300): START
ActorLifeCycle: Warning: [ALifecycleBreakDown::PostDuplicate](000009177D157300): END
ActorLifeCycle: Warning: [ALifecycleBreakDown::PreInitializeComponents](000009177D157300): START
ActorLifeCycle: Warning: [ALifecycleBreakDown::PreInitializeComponents](000009177D157300): END
ActorLifeCycle: Warning: [ALifecycleBreakDown::PostInitializeComponents](000009177D157300): START
ActorLifeCycle: Warning: [ALifecycleBreakDown::PostInitializeComponents](000009177D157300): END
LogTemp: Error: [CDO]: 0000091767DEAA00
ActorLifeCycle: Warning: [ALifecycleBreakDown::BeginPlay](000009177D157300): START
ActorLifeCycle: Warning: [ALifecycleBreakDown::BeginPlay](000009177D157300): END

 

공식문서의 설명대로 생성자 -> PostDuplicate -> PreInitialize Components -> PostInitializeComponents -> BeginPlay 순으로 실행되는 것을 확인할 수 있다.

추가로 Actor CDO의 주소가 앞서 에디터를 실행할 때 확인한 주소와 같은 것을 볼 수 있다.

 

앞서 에디터에서 배치한 Actor와 에디터에서 레벨을 플레이하고 나서 생성된 Actor가 다른 주소를 가진 것으로 Editor에 있는 Actor들이 New World로 복사된 것을 확인할 수 있다.

 

Load from Disk

이미 레벨에 있는 Actor에 대해 발생하는 루트로 UEngine::LoadMap 또는 동적으로 맵의 일부분을 로드하고 언로드하는 레벨 스트리밍에서 UWorld::AddToWorld를 호출할 때 발생한다.

 

패키지/레벨에 있는 Actor가 Disk에서 로드되고 PostLoad를 호출. 전체적인 흐름은 에디터에서 플레이와 매우 유사하기에 따로 다루지는 않으려한다.

 

Spawn & Deferred Spawn

미리 레벨에 배치되지 않은 Actor를 UWorld::SpawnActor로 Spawn할 때의 Lifecycle이다.

 

1. Actor가 World에 Spawn된 이후 AActor::PostSpawnInitialize를 호출

2. Spawn된 Actor의 생성 이후 AActor::PostActorCreated를 호출. 생성자와 관련된 구현을 담는 함수로 PostLoad와 배타적이다 (Disk에서 로드하거나 생성되거나)

3. AActor::ExecuteConstruction -> AActor::OnConstruction(블루프린트 Actor의 컴포넌트 생성, 변수 초기화 시점) -> AActor::PostActorConstruction

4. 미리 배치된 Actor와 마찬가지로 컴포넌트 초기화 

  4.1 AActor::PreInitializeComponents -> UActorComponent::InitalizeComponent -> AActor::PostInitializeComponents

5. UWorld::OnActorSpawned로 Delegate Broadcast 이후 AActor::BeginPlay를 호출한다.

 

주요 함수들을 Override하여 마찬가지로 로깅하도록 하였고 블루프린트로 아래와 같이 Spawner Actor를 만들고 Level에 배치 후 실행하였다.

LogBlueprintUserMessages: Spawner PreSpawnActor 
ActorLifeCycle: Warning: [ALifecycleBreakDown::ALifecycleBreakDown](0000066595360000): START
ActorLifeCycle: Warning: [ALifecycleBreakDown::ALifecycleBreakDown](0000066595360000): END
LogBlueprintUserMessages: Construction Script
ActorLifeCycle: Warning: [ALifecycleBreakDown::PreInitializeComponents](0000066595360000): START
ActorLifeCycle: Warning: [ALifecycleBreakDown::PreInitializeComponents](0000066595360000): END
ActorLifeCycle: Warning: [ALifecycleBreakDown::PostInitializeComponents](0000066595360000): START
ActorLifeCycle: Warning: [ALifecycleBreakDown::PostInitializeComponents](0000066595360000): END
LogTemp: Error: [CDO]: 00000665A639A000
ActorLifeCycle: Warning: [ALifecycleBreakDown::BeginPlay](0000066595360000): START
ActorLifeCycle: Warning: [ALifecycleBreakDown::BeginPlay](0000066595360000): END
LogBlueprintUserMessages: Spawner PostSpawnActor

 

블루프린트에서 Construction Scirpt에 Log String 노드를 추가하여 Construction 시점도 공식문서의 도식과 일치함을 확인하였다.

 

Deferred Spawn은 Spawn과 거의 동일하지만 PostActorCreated 이후, 블루프린트 Consturction 전에 초기화 함수를 구성한 뒤 AActor::FinishingSpawning을 호출하여 Spawn을 마무리하는 형태이다. 변수의 초기화 값의 변경이 동적으로 필요한 Actor를 Spawn할 때 유용하다.

 

 

End of Actor Lifecycle

Destroy, Lifetime 종료, Level Transition, Editor에서 플레이 종료, Game End, application closing 에 의해 EndPlay가 호출되고 Actor는 RF_PendingKill로 마킹된다.

 

이후 ULevel의 Actor Array에서 해당 Actor는 제거되고 다음 garbage collection cycle에 마킹된 Actor의 메모리를 할당해제 한다.

 

garbage collection의 과정에서 UObject::BeginDestroy, UObject::IsReadyForFinishDestroy(준비가 되지 않았다면 다음 cycle에 다시 진행한다), UObject::FinishDestroy 의 함수가 추가로 호출된다. 

 

 

 

Actor Lifecycle에 대해 찾아보고 학습하고나서 Actor의 다른 클래스와 상호작용 관련 로직을 생성자에 넣지 않는 이유(넣을 수 없는 이유),  Actor의 Component를 다루는 로직을 BeginPlay 또는 PostInitializeComponents를 Override하는 방식으로 구현하는지에 대해 어느정도 알게 되었다.