⊙ Actor의 생명주기 학습의 필요성을 이해한다.
⊙ Actor의 생명주기와 차이를 이해한다.
코드스니펫
[Item.h]
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h"
DECLARE_LOG_CATEGORY_EXTERN(LogSparta, Warning, All);
UCLASS()
class CH3_UNREAL3DGAME_API AItem : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AItem();
protected:
USceneComponent* SceneRoot; // Scene Component
UStaticMeshComponent* StaticMeshComp; // Static Mesh Component
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;
};
[Item.cpp]
// Fill out your copyright notice in the Description page of Project Settings.
#include "Item.h"
DEFINE_LOG_CATEGORY(LogSparta);
// Sets default values
AItem::AItem()
{
// Scene Component
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot")); // Scene Component 만들기
SetRootComponent(SceneRoot); // Scene Component를 Root Component로 하기
// Static Mesh Component
StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh")); // Static Mesh Component 생성
StaticMeshComp->SetupAttachment(SceneRoot); // Scene Component에 하위로 붙이기
//'/Game/Resources/Props/SM_Chair.SM_Chair'
//'/Game/Resources/Materials/M_Metal_Gold.M_Metal_Gold'
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(LogTemp, Warning, TEXT("My Log!"));
//UE_LOG(LogSparta, Error, TEXT("My Sparta!"));
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);
}
액터의 생명 주기(Life Cycle)
Actor가 태어나고 죽을 때까지의 과정을 생명 주기, 라이프 사이클(Life Cycle)이라고 한다. 태어나고 죽는다고 하니까 이상하지만 결국 게임 중에 생성(Spawn)되고 파괴(Destroy)되는 과정이라고 생각하면 된다. Unity에서도 생명주기라는 것이 있다. Start, Update 등등 그것과 같다고 생각하면 된다.
액터의 생명주기 학습의 필요성
생명 주기를 이해하면 게임 로직을 보다 효율적이고 안정적으로 작성할 수 있다는 장점이 있다.
- 초기화 시점 결정
: 생성자, PostInitializeComponents, BeginPlay 등이 각각 언제 호출하는지 알아야 적절한 곳에 코드를 배치할 수 있다. 예를 들어 컴포넌트 생성은 생성자에서, 다른 액터 참조나 월드 접근은 BeginPlay에서 처리한다. - 성능 관리
: 매 프레임마다 호출되는 Tick 함수는 비용이 클 수 있다. 따라서 필요한 액터만 Tick을 활성화하거나 이벤트 기반으로 전환해 최적화해야 한다. - 리소스 정리
: 액터가 사라질 때(EndPlay, Destroyed 등) 메모리를 해제하거나 특정 상태를 저장해야할 수 있다. 적절한 시점에 정리 작업을 하지 않으면 메모리 누수나 예외 상황이 발생활 수 있다.
리소스 정리를 제외하면 모두 Unity에서도 겪어본 일들이다. 언제 초기화 되어야 하는지 어떤 순서로 초기화 되어야 하는지, 지금 초기화 하면 값을 가지고 있는지 등등 초기화 시점을 결정하는 일에서 꼬이면 생각보다 오류를 해결하기 어려워진다. 성능 관리도 마찬가지다. 나중에는 되도록 Update(언리얼의 Tick 역할)을 사용하지 않기 위해 노력했다. 성능 저하를 막기 위해서다.
이처럼 액터의 생명 주기를 학습하는 것은 기본중에 기본이다.
액터의 생명주기(Life Cycle)
언리얼 엔진에서의 Actor는 이런 과정으로 살다가 죽는다.
생성 ▶ 초기화 ▶ 월드 배치 ▶ 실행(Tick) ▶ 제거
이 과정을 지원하기 위해 여러 함수가 자동으로 호출된다. 순서는 아래와 같다.
Constructor(생성자)
▶ PostInitializeComponents()
▶ BeginPlay()
▶ Tick(float DeltaTime)
▶ Destroyed()
▶ EndPlay(const EEndPlayReason::Type EndPlayReason
Constructor(생성자) | |
호출 시기 | C++ 클래스 객체가 메모리에 생성될 때 단 한번 호출 아직 월드에 완전 등록된 상태가 아님 |
용도 | 보통 컴포넌트 생성(CreateDefaultSubobject) 및 기본 변수 초기화에 사용 아직 월드에 완전 등록된 상태가 아니라 다른 액터나 월드 관련 기능은 안전하게 호출하기 어려움 |
▼ | |
PostInitializeComponents() | |
호출 시기 | 액터의 모든 컴포넌트가 생성/초기화를 마친 뒤 자동으로 호출 |
용도 | 컴포넌트들이 이미 준비된 상태이므로, 컴포넌트 간 상호작용 초기화 코드를 넣기 좋음 |
▼ | |
BeginPlay() | |
호출 시기 | 게임이 시작(Play 모드)되거나, 런타임 중 액터가 새로 생성(Spawn)되는 순간에 한 번 호출 |
용도 | 월드와 다른 액터들이 준비된 상태이므로, 자유롭게 상호작용 코드 작성 가능 AI, 게임 모드, 플레이어 컨트롤러 등 다른 시스템과의 연동 처리 |
▼ | |
Tick(DeltaTime) | |
호출 시기 | 매 프레임마다 반복 호출 |
용도 | 실시간 업데이트가 필요한 로직(캐릭터 이동, 물리 연산 등) 불필요한 액터는 Tick을 끄고, 이벤트 기반으로 전환하면 성능 절약 가능 |
▼ | |
Destroyed() | |
호출 시기 | Destroy() 함수를 직접 호출해 액터를 제거할 때 직전에 호출 (단, 게임 종료나 레벨 전환 시에는 호출되지 않을 수 있음) |
용도 | EndPlay에서 주요 정리를 마치고, Destroyed에서 메모리 해제나 사운드/파티클 정리 등 최종 작업을 수행 |
▼ | |
EndPlay(const EEndPlayReason::Type EndPlayReason) | |
호출 시기 | 액터가 더 이상 월드에서 활동하지 않을 때(파괴, 게임 종료, 레벨 전환 등) 호출 |
용도 | 자원 해제나 상태 저장을 처리 |
매개 변수 | EEndPlayReason::Type는 언리얼엔진에서 EndPlay 함수가 호출되는 이유를 나타내는 열거형 타입 |
생명 주기(Life Cycle) 함수에 로그 출력
마지막으로 각 함수가 언제 어떤 순서로 호출되는지 확인해보기 위해 각 함수에서 로그를 찍어보자.
* 단 Tick 함수는 매 프레임 호출되므로 로그 메시지가 빗발칠 수 있어 제외
헤더 파일에는 각 라이프 사이클 함수를 선언해준다.
// Fill out your copyright notice in the Description page of Project Settings.
#pragma once
#include "CoreMinimal.h"
#include "GameFramework/Actor.h"
#include "Item.generated.h"
DECLARE_LOG_CATEGORY_EXTERN(LogSparta, Warning, All);
UCLASS()
class CH3_UNREAL3DGAME_API AItem : public AActor
{
GENERATED_BODY()
public:
// Sets default values for this actor's properties
AItem();
protected:
USceneComponent* SceneRoot; // Scene Component
UStaticMeshComponent* StaticMeshComp; // Static Mesh Component
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;
};
구현 파일에는 각각의 라이프 사이클 함수를 구현해주는데 이 때 로그 메시지를 출력하도록 UE_LOG 매크로를 호출한다. Tick은 빗발치는 로그 메시지 이슈로 로그를 출력하지 않으며 Destroyed와 EndPlay는 Super 함수를 호출하기 전에 로그를 출력하도록 한다.
// Fill out your copyright notice in the Description page of Project Settings.
#include "Item.h"
DEFINE_LOG_CATEGORY(LogSparta);
// Sets default values
AItem::AItem()
{
// Scene Component
SceneRoot = CreateDefaultSubobject<USceneComponent>(TEXT("SceneRoot")); // Scene Component 만들기
SetRootComponent(SceneRoot); // Scene Component를 Root Component로 하기
// Static Mesh Component
StaticMeshComp = CreateDefaultSubobject<UStaticMeshComponent>(TEXT("StaticMesh")); // Static Mesh Component 생성
StaticMeshComp->SetupAttachment(SceneRoot); // Scene Component에 하위로 붙이기
//'/Game/Resources/Props/SM_Chair.SM_Chair'
//'/Game/Resources/Materials/M_Metal_Gold.M_Metal_Gold'
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(LogTemp, Warning, TEXT("My Log!"));
//UE_LOG(LogSparta, Error, TEXT("My Sparta!"));
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);
}
로그 출력 결과 확인
UE_LOG로 로그를 출력했기 때문에 Output Log를 확인하면 결과를 확인할 수 있다. 세 가지 상황에서의 로그 상황을 살펴보자.
▷ 언리얼 에디터에서 플레이를 눌렀을 때
- 액터가 생성되고 (Constructor)
- 컴포넌트가 생성과 초기화를 하고 (PostInitializeComponents)
- 월드에 배치까지 되었기 때문에 (BeginPlay)
아래와 같은 결과의 로그 메시지들이 나온다.
▷ 게임을 종료했을 때
Item이 Destroy된 적이 없는데 게임이 갑자기 종료되어 버리니 EndPlay가 호출된다.
▷ 게임 중 Outliner 창에서 Item 액터를 삭제할 때
Item을 Destroy한 것이기 때문에 Destroyed가 출력되고 EndPlay가 출력된 것을 볼 수 있다.
여기서 하가지 짚고 넘어가야 하는 것은 Destroyed와 EndPlay의 차이이다.
EndPlay는 액터가 월드에서 사라지는 모든 상황(게임 종료, 레벨 전환, Destroy 호출 등)에 대해 호출된다.
Destroyed는 보통 Destroy() 함수가 명시적으로 불렸을 때만 호출되며, 게임 종료나 레벨 언로드 시에는 호출되지 않을 수 있다.
배운 내용 정리
- 액터의 라이프 사이클은 초기화 시점 결정, 성능 관리, 리소스 정리 등을 위해 꼭 알아야 하는 개념이다.
- Constructor(생성자) → PostInitializeComponents() → BeginPlay() → Tick(float DeltaTime) → Destroyed() → EndPlay(const EEndPlayReason::Type EndPlayReason) 순서로 호출된다.
- EndPlay는 액터가 월드에서 사라지는 모든 상황(게임 종료, 레벨 전환, Destroy 호출 등)에 대해 호출되지만 Destroyed는 보통 Destroy() 함수가 명시적으로 불렸을 때만 호출되며, 게임 종료나 레벨 언로드 시에는 호출되지 않을 수 있으니 차이를 기억한다.
'Unreal Engine 5 > C++와 Unreal Engine으로 3D 게임 개발' 카테고리의 다른 글
레벨 위에 Character 클래스 생성 (0) | 2025.01.24 |
---|---|
Unreal Engine의 GameMode (0) | 2025.01.24 |
OutputLog에 로그 출력 (0) | 2025.01.22 |
Actor 클래스의 코드 구조 (0) | 2025.01.21 |
프로젝트 이주(Migrate) 하기 (0) | 2025.01.21 |