액터 라이프 사이클을 알아야 하는 이유 :
1. 초기화 시점 결정 :
각각의 메서드들이 각각 언제 호출되는지 알아야 적절한 곳에 코드를 배치할 수 있습니다.
예시 ) 컴포넌트와 타이머 그리고 메모리 해제는 어디에서? 미래의 나야.. 기억하고 있지?ㅠ
2. 성능 관리 :
매 프레임마다 호출되는 Tick함수는 비용이 큽니다.
따라서 필요한 액터만 Tick을 활성화하거나 타이머로 대신할 수 있는 기능이라면
타이머로 대신 하거나
이벤트 기반으로 전환해서 최적화 해야합니다.
3. 리소스 정리 :
액터가 사라질 때 메모리를 해제하거나 특정 상태를 저장해야할 수 있습니다.
cf )루트 컴포넌트란?
액터의 최상위 컴포넌트로 액터의 위치,회전,스케일은 항상 루트 컴포넌트의 트랜스폼에 의해 결정됩니다.
루트 컴포넌트가 변하면 자식 컴포넌트도 이에 따라 변합니다.
Scene Component는 직접적인 시각적 출력을 가지진 않지만 다른 컴포넌트들의 계층적 트랜스폼을 정의하는 기준점 역할을 합니다. 또, 다른 시각적 컴포넌트들을 아래에 붙여 관리할 수 있습니다
cf ) 그렇다면 왜 액터의 루트로 USceneComponent를 사용해야 하는가?
1) 액터의 기본적인 공간 정보 관리
모든 액터는 위치,회전,크기를 필요로 합니다.
UsceneComponent는 이러한 트랜스폼 정보를 관리하는 기본적인 역할을 제공합니다.
2) 다양한 컴포넌트 추가 및 계층 구조 관리
UScenecomponent는 다른 컴포넌트를 자식으로 설정할 수 있는 부모 역할을 합니다.
이를 통해 컴포넌트 계층 구조를 형성하고, 자식 컴포넌트는 루트 컴포넌트의 트랜스폼을 상속받아 동작합니다.
3) 루트 컴포넌트로 적합한 구조
루트 컴포넌트는 반드시 위치, 회전, 크기를 처리할 수 있어야 합니다.
USceneComponent는 이 세 가지를 모두 처리할 수 있는 기본적인 컴포넌트입니다.
반면, 다른 유형의 컴포넌트(예: UStaticMeshComponent, USkeletalMeshComponent)는 트랜스폼 정보 외에 추가적인 기능이 있어서 루트로 사용하면 불필요한 제약이 생길 수 있습니다.
4) 확장성과 유연성
USceneComponent를 루트로 설정하면 필요에 따라 다른 컴포넌트를 자식으로 추가하거나 교체하기가 더 쉽습니다.
특정 상황에서 루트 컴포넌트를 변경해야 할 때, USceneComponent는 가장 가벼운(추가적인 기능이 없는) 트랜스폼 관리용 컴포넌트로 적합합니다.
cf) USceneComponent를 루트로 하지 않을 때의 문제
1) 컴포넌트 제약
만일 루트 컴포넌트로 UStaticMeshComponent나 USkeletalMeshComponent를 설정하면, 해당 컴포넌트의 추가적인 기능(예: 렌더링 관련 속성)이 모든 자식 컴포넌트에 영향을 줄 수 있습니다.
예를 들어, UStaticMeshComponent를 루트로 설정하면 비주얼 관련 속성이 루트 트랜스폼과 얽혀서 예상치 못한 동작을 유발할 수 있습니다.
2) 비효율적인 구조
루트 컴포넌트는 트랜스폼만 관리하는 역할로 사용하는 것이 가장 효율적입니다.
불필요하게 기능이 많은 컴포넌트를 루트로 사용하면 성능상 불리할 수 있습니다.
언리얼 엔진의 Actor는 생성 → 초기화 → 월드 배치 → Tick(실행) → 제거 순으로 동작하며, 이를 지원하기 위해 여러 함수가 자동 호출됩니다.
1. 생성자 (Constructor)
C++과 비슷하게 메모리에 생성될 때 딱 한번 호출됩니다.
아직 월드에 완전히 등록된 상태가 아니므로, 다른 액터나 월드 관련 기능은
안전하게 호출하기 어렵습니다. (2025/01/24 TIL참고)
보통 컴포넌트 생성 및 기본 변수 초기화에 사용합니다.
2. PostInitializeComponents()
액터의 모든 컴포넌트가 생성및 초기화를 마친 뒤 자동으로 호출됩니다.
컴포넌트들이 이미 준비된 상태이므로, 컴포넌트 간 상호작용 초기화 코드를 넣기 좋습니다.
3. BeginPlay()
게임이 시작되거나 런타임 중 액터가 새로 생성(Spawn)되는 순간에 한번 호출됩니다.
이 시점에서는 월드와 다른 액터들이 준비된 상태이므로, 자유롭게 상호작용 코드를 작성할 수 있습니다.
AI,게임모드,타이머,플레이어 컨드롤러 등 다른 시스템과 연동을 초기화하는 단계입니다.
4.Tick(float DeltaTime)
매 프레임마다 반복 호출되며, 실시간 업데이트가 필요한 로직(캐릭터 이동, 물리연산)
을 넣습니다.
불필요한 액터는 Tick을 끄고, 이벤트 기반으로 전환하면 성능을 절약할 수 있습니다.
5.Destroyed()
Destroy()함수를 직접 호출해 액터를 제거할 때 직전에 호출됩니다.
보통 EndPlay에서 주요 정리를 마치고, Destroyed()에서 메모리 해제나 컴포넌트 정리를 합니다.
만일 Destroyed()가 호출되면 마지막에 EndPlay도 같이 호출됩니다.
6. EndPlay(const EEndPlayReason::Type EndPlayReason)
액터가 더 이상 월드에서 활동하지 않을 때 호출됩니다.
EEndPlayReason::Type 은 언리얼 엔진에서 EndPlay 함수가 호출되는 이유를 나타내는
열거형타입입니다. 이 함수에서 자원 해제나 상태 저장을 처리합니다.
얼핏보면 EnumClass같지만 아닙니다. 이유는 아래에서 설명하겠습니다.
Destroy는 선택사항이지만 EndPlay는 아니기 때문에 가장 중요한 자원을 정리합니다
해당 라이프사이클 함수 중에서 먼저 생성자와 BeginPlay()를 먼저 보겠습니다.
AItem::AItem()
{
SceneRoot = CreateDefaultSubobject<USceneComponent>(Text("..."));
SetRootComponent(SceneRoot);
StaticMeshComponent = CreateDefaultSubobject<UStaticMeshComponent>
(TEXT("..."));
StaticMeshComponent->SetupAttachment(SceneRoot)
}
void AItem::BeginPlay()
{
Super::BeginPlay();
UE_LOG(LogTemp, Warning, TEXT("Hello im Item!"));
}
생성자에서는 루트 컴포넌트를 USceneComponent를 붙여주고
매시 컴포넌트를 그 아래에 붙여주는 장면입니다.
다음처럼 컴포넌트 생성(CreateDefaultSubobject) 및 기본 변수 초기화는 생성자에서 사용합니다.
저는 BeginPlay()내부의 Super::BeginPlay() 를 보고 다음과 같은 생각을 하게 되었습니다.
왜 super::을 붙여서 부모에서 가져와야하는가?
=>호출을 안할경우 많은 경우 뒤에서 문제가 생기게 된다.
예를들어서 비긴플레이를 호출했는데 뒤에서 EndPlay가 호출안되는 경우가 있다라는 것을 알게 되었고
이 소리는 저에게 다음과 같이 들렸습니다.
부모의 BeginPlay와 자식의 BeginPlay는 수행하는 기능이 다르다.
따라서 AActor의 BeginPlay가 하는 역할을 알아보기 위해 고군분투 해보겠습니다 !!
가장 먼저 Gpt와 언리얼 공식 문서를 살펴보고자 합니다.
먼저 언리얼엔진 공식 문서를 보시면 다음과 같은 플로우차트가 있습니다.
액터가 스폰되고, BeginPlay를 시작하기 까지 각 단계에서 엔진이 필요한 초기화 로직을 처리하고,
오버라이드 한 함수가 있다면 그 타이밍에 함께 호출하는 구조라는 것을 알게 되었습니다.
AActor::BeginPlay()를 실행하게 된다면 기본 초기화 로직을 먼저 실행하게 됩니다.
부모가 해줘야 할 각종 초기화 과정(내부 타이머, 컴포넌트 관련 초기 로직, 블루 프린트 연동 )등등을
해주는 기능을 가지고 있기 때문에 상속받은 BeginPlay에서는 꼭 Super을 통해 부모를 호출해줘야합니다.
ex ) 컴포넌트를 배열에 가지고 있습니다 // 배열의 용량을 초과하면 힙메모리로 가게 됩니다.
ex2 ) 자동으로 소멸되어야 하는 액터의 경우 BeginPlay에서 체크를 해줘야합니다
ex3 ) 컴포넌트들을 배열에 연결한 후 BeginPlay를 시작해줍니다.
/**2025/01/24 TODO : 이거 스폰액터랑 언리얼엔진상에서의 액터배치랑 같은건가? */
/**스폰액터는 매니저가 관리하는 스폰액터인가?*/
/**이거 사실검증 한번만 해보자 */
언리얼엔진 공식문서에 따라, 좀 더 자세하게 알아보겠습니다.
액터 인스턴스를 스폰할 때 따라가는 경로는 다음과 같습니다.
UWorld::SpawnActor를 호출합니다.
액터가 월드에 스폰된 이후 AActor::PostSpawnInitialize가 호출됩니다.
AActor::PostActorCreated는 생성 이후 스폰된 액터에 대해 호출되며,
모든 생성자 구현 동작은 여기로 이동해야 합니다. PostActorCreated는 PostLoad와 상호 배타적입니다.
AActor::ExecuteConstruction:
AActor::OnConstruction - 액터 생성, 블루프린트 액터의 컴포넌트 생성 및 블루프린트 변수가 초기화됩니다.
AActor::PostActorConstruction:
액터의 컴포넌트에서 InitializeComponent를 호출하기 전에 AActor::PreInitializeComponents를 호출합니다.
UActorComponent::InitializeComponent는 액터에 정의된 각 컴포넌트를 생성하는 헬퍼 함수입니다.
AActor::PostInitializeComponents는 액터의 컴포넌트가 초기화된 후에 호출됩니다.
UWorld::OnActorSpawned가 UWorld에서 브로드캐스트됩니다.
AActor::BeginPlay가 호출됩니다.
그리고 EndPlay와 Destroyed() 함수에 대해 알아보겠습니다.
먼저 EndPlay의 소스코드를 살펴보면 다음과 같은데,
void AItem::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
}
EEndPlayReason::Type을 쓴 이유(기초문법 부족..)와 EndPlayReason에는 어떤것이 있을까?가 궁금했습니다.
먼저 EndPlayReason을 간단히 살펴보겠습니다.
언리얼엔진 공식문서를 살펴보면 좀 더 이해하기가 쉽습니다.
그니깐 각자 다른 방식으로 EndPlay로 귀결이 되는데 그 이유는 6가지 입니다. 후에 좀 더 자세하게 작성하겠습니다.
이제 우리는 EndPlayReason이 다음과 같은 6개가 있다는 것을 알게 되었습니다.
그렇다면 EEndPlayReason :: Type EndPlayReason은 왜 EnumClass가 아니고, Enum이며
해당 PlayReason이 어떻게 구현되어 있는가?를 알아보러 가겠습니다 !
에픽엔진에서 프로젝트를 하나 만들고 컨트롤 + 쉬프트 + F를 통해 엔진속 EEndPlayReason 을 찾아보면 다음과 같이 정의 되어 있습니다.
UENUM(BlueprintType)
namespace EEndPlayReason
{
enum Type : int
{
/** When the Actor or Component is explicitly destroyed. */
Destroyed,
/** When the world is being unloaded for a level transition. */
LevelTransition,
/** When the world is being unloaded because PIE is ending. */
EndPlayInEditor,
/** When the level it is a member of is streamed out. */
RemovedFromWorld,
/** When the application is being exited. */
Quit,
};
}
EnumClass::Type이 아닌 namespace가 EEndPlayReason인 이넘 타입이였습니다.
아마도 이유는 Enum은 값을 컴파일러가 암시적으로 변환할수있기 때문에
블루 프린트와 호환이 되게 하려는 것이 아닐까 추측이 됩니다.
찾는데에 어려움이 있어 추측으로 적었습니다.
이제 구체적으로 Reason을 살펴보겠습니다.
EEndPlayReason:: Destroyed
Actor 또는 Component가 명시적으로 Destroy() 함수에 의해 파괴될 때 사용됩니다.
예: 게임플레이 중 Actor를 수동으로 제거했을 때.
EEndPlayReason::LevelTransition
레벨 전환이 발생했을 때 사용됩니다.
예: 레벨이 언로드되거나 다른 레벨로 전환될 때.
EEndPlayReason::EndPlayInEditor
에디터에서 "Play in Editor (PIE)" 세션이 종료되었을 때 사용됩니다.
에디터에서만 발생하며 게임 실행 환경에서는 해당하지 않습니다.
EEndPlayReason::RemovedFromWorld
Actor 또는 Component가 월드에서 제거될 때 사용됩니다.
예: 언로드된 서브 레벨에 속한 Actor가 제거될 때.
EEndPlayReason::Quit
게임이 종료될 때 사용됩니다.
예: 플레이어가 게임을 종료하거나 애플리케이션이 닫힐 때.
따라서 다음과 같이 상속받은 AYourActor의 EndPlay를 재정의한다면 , 해당하는 값에 따라 로그가 설정될 것입니다.
#include "YourActor.h"
void AYourActor::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
Super::EndPlay(EndPlayReason);
switch (EndPlayReason)
{
case EEndPlayReason::Destroyed:
UE_LOG(LogTemp, Warning, TEXT("Actor destroyed."));
break;
case EEndPlayReason::LevelTransition:
UE_LOG(LogTemp, Warning, TEXT("Level transition."));
break;
case EEndPlayReason::EndPlayInEditor:
UE_LOG(LogTemp, Warning, TEXT("Play in Editor session ended."));
break;
case EEndPlayReason::RemovedFromWorld:
UE_LOG(LogTemp, Warning, TEXT("Actor removed from world."));
break;
case EEndPlayReason::Quit:
UE_LOG(LogTemp, Warning, TEXT("Game is quitting."));
break;
default:
UE_LOG(LogTemp, Warning, TEXT("Unknown EndPlayReason."));
break;
}
}
의아한 것은 액터의 생존주기가 초과될때는 어떤 EndPlayReason인지 모르겠습니다.
공부를 목적으로 만든 블로그입니다. 틀린내용은 말씀해주시면 감사하겠습니다.
출처 : ) Actor의 생존주기
https://dev.epicgames.com/documentation/ko-kr/unreal-engine/unreal-engine-actor-lifecycle
'Unreal > Unreal 공부 내용' 카테고리의 다른 글
GetOverlappingActors(TArray<AActor*> Array) , IsA (0) | 2025.02.09 |
---|---|
TSubClassOf , UClass* , StaticClass, 클래스,인스턴스,메타데이터(미완성) (0) | 2025.02.05 |
Timer(SetTimer...) (0) | 2025.01.24 |
구면선형보간법(SLERP) (0) | 2025.01.22 |
선형 보간 (0) | 2025.01.22 |