Actor 라이프사이클
생성부터 파괴까지, Actor의 생명주기 완벽 이해
라이프사이클 개요
Actor 생성부터 파괴까지의 흐름
/*
┌──────────────────────────────────────────────────────────────┐
│ ACTOR LIFECYCLE │
├──────────────────────────────────────────────────────────────┤
│ │
│ ┌─────────────┐ │
│ │ Constructor │ CDO 생성, 컴포넌트 생성 │
│ └──────┬──────┘ │
│ │ │
│ v │
│ ┌──────────────────────┐ │
│ │ PostInitProperties │ 프로퍼티 초기화 후 │
│ └──────────┬───────────┘ │
│ │ │
│ v │
│ ┌──────────────────────────┐ │
│ │ PostInitializeComponents │ 모든 컴포넌트 초기화 완료 │
│ └──────────┬───────────────┘ │
│ │ │
│ v │
│ ┌───────────┐ │
│ │ BeginPlay │ 게임 시작, 월드 준비 완료 │
│ └─────┬─────┘ │
│ │ │
│ v │
│ ┌──────┐ │
│ │ Tick │ 매 프레임 업데이트 │
│ └──┬───┘ │
│ │ (반복) │
│ v │
│ ┌─────────┐ │
│ │ EndPlay │ 파괴 전 정리, 타이머/델리게이트 해제 │
│ └────┬────┘ │
│ │ │
│ v │
│ ┌──────────────┐ │
│ │ BeginDestroy │ 메모리 해제 시작 │
│ └──────────────┘ │
│ │
└──────────────────────────────────────────────────────────────┘
*/
생성자 (Constructor)
CDO 생성과 컴포넌트 초기화
AMyCharacter::AMyCharacter()
{
// 틱 활성화 설정
PrimaryActorTick.bCanEverTick = true;
PrimaryActorTick.bStartWithTickEnabled = true;
// 컴포넌트 생성 - 생성자에서만 CreateDefaultSubobject 사용!
CapsuleComp = CreateDefaultSubobject<UCapsuleComponent>(TEXT("Capsule"));
SetRootComponent(CapsuleComp);
MeshComp = CreateDefaultSubobject<USkeletalMeshComponent>(TEXT("Mesh"));
MeshComp->SetupAttachment(RootComponent);
CameraComp = CreateDefaultSubobject<UCameraComponent>(TEXT("Camera"));
CameraComp->SetupAttachment(MeshComp);
// 기본값 설정
MaxHealth = 100.0f;
CurrentHealth = MaxHealth;
bIsAlive = true;
// ⚠️ 주의: 생성자에서 하면 안 되는 것들
// - GetWorld() 호출 (nullptr일 수 있음)
// - 다른 Actor 참조 (아직 존재하지 않을 수 있음)
// - 게임플레이 로직 실행
}
생성자는 Class Default Object (CDO)를 생성할 때도 호출됩니다. CDO는 클래스의 기본값을 저장하는 템플릿 객체로, 실제 게임 월드에 존재하지 않습니다. 따라서 생성자에서 월드 의존적인 코드를 작성하면 안 됩니다.
PostInitializeComponents
모든 컴포넌트 초기화 완료 후
void AMyCharacter::PostInitializeComponents()
{
Super::PostInitializeComponents();
// 모든 컴포넌트가 초기화된 후 호출
// BeginPlay 전에 실행됨
// 네트워크 복제 전에 실행됨
// 컴포넌트 간 의존성 설정에 적합
if (HealthComponent)
{
HealthComponent->SetMaxHealth(MaxHealth);
HealthComponent->OnHealthChanged.AddDynamic(
this, &AMyCharacter::HandleHealthChanged);
}
// 애니메이션 인스턴스 설정
if (MeshComp)
{
AnimInstance = Cast<UMyAnimInstance>(
MeshComp->GetAnimInstance());
}
// ⚠️ 주의: GetWorld()는 사용 가능하지만
// 다른 Actor가 아직 BeginPlay를 호출하지 않았을 수 있음
}
적합한 작업
- 컴포넌트 간 바인딩
- 델리게이트 연결
- 초기 설정 적용
부적합한 작업
- 다른 Actor 검색/참조
- 월드 상태 의존 로직
- 게임플레이 시작 로직
BeginPlay
게임 시작 시 초기화
void AMyCharacter::BeginPlay()
{
Super::BeginPlay(); // 반드시 호출!
// 게임이 시작되고, 모든 Actor가 월드에 준비된 상태
// 월드 컨텍스트가 완전히 유효함
// 다른 Actor 검색
AGameModeBase* GameMode = GetWorld()->GetAuthGameMode();
// 플레이어 컨트롤러 얻기
if (APlayerController* PC = Cast<APlayerController>(GetController()))
{
// 입력 설정
if (UEnhancedInputLocalPlayerSubsystem* Subsystem =
ULocalPlayer::GetSubsystem<UEnhancedInputLocalPlayerSubsystem>(
PC->GetLocalPlayer()))
{
Subsystem->AddMappingContext(DefaultMappingContext, 0);
}
}
// 타이머 시작
GetWorldTimerManager().SetTimer(
RegenTimerHandle,
this,
&AMyCharacter::RegenerateHealth,
1.0f, // 매 1초
true // 반복
);
// 초기 이벤트 발생
OnCharacterSpawned.Broadcast(this);
}
레벨에 배치된 Actor: 레벨 로드 시 호출
SpawnActor로 생성: SpawnActor 직후 호출
스트리밍된 Actor: 스트리밍 완료 후 호출
Tick
매 프레임 업데이트
void AMyCharacter::Tick(float DeltaTime)
{
Super::Tick(DeltaTime);
// 매 프레임 실행되는 로직
// DeltaTime: 이전 프레임부터 경과 시간 (초)
// 이동 처리
if (!MovementInput.IsZero())
{
FVector Movement = MovementInput * MoveSpeed * DeltaTime;
AddActorWorldOffset(Movement, true);
}
// 회전 보간
FRotator TargetRotation = GetControlRotation();
FRotator NewRotation = FMath::RInterpTo(
GetActorRotation(),
TargetRotation,
DeltaTime,
RotationSpeed
);
SetActorRotation(NewRotation);
}
// 틱 그룹 및 의존성 설정
AMyCharacter::AMyCharacter()
{
// 틱 그룹 설정 (기본: TG_PrePhysics)
PrimaryActorTick.TickGroup = TG_PrePhysics;
// 다른 Actor 틱 후에 실행
PrimaryActorTick.AddPrerequisite(OtherActor, OtherActor->PrimaryActorTick);
}
Tick은 성능에 큰 영향을 줍니다. 가능하면 타이머, 이벤트, 또는 틱 간격 조절을 사용하세요.
// 틱 비활성화
PrimaryActorTick.bCanEverTick = false;
// 런타임에 틱 활성화/비활성화
SetActorTickEnabled(false);
SetActorTickEnabled(true);
// 틱 간격 조절 (매 프레임 대신 N초마다)
PrimaryActorTick.TickInterval = 0.1f; // 0.1초마다
EndPlay
파괴 전 정리 작업
void AMyCharacter::EndPlay(const EEndPlayReason::Type EndPlayReason)
{
// EndPlay에서 정리 작업 수행 (OnDestroyed보다 권장!)
// 타이머 정리
GetWorldTimerManager().ClearTimer(RegenTimerHandle);
GetWorldTimerManager().ClearAllTimersForObject(this);
// 델리게이트 해제
if (HealthComponent)
{
HealthComponent->OnHealthChanged.RemoveDynamic(
this, &AMyCharacter::HandleHealthChanged);
}
// 이벤트 알림
OnCharacterDespawned.Broadcast(this);
// 종료 이유에 따른 처리
switch (EndPlayReason)
{
case EEndPlayReason::Destroyed:
// Destroy() 호출로 파괴
break;
case EEndPlayReason::LevelTransition:
// 레벨 전환
break;
case EEndPlayReason::EndPlayInEditor:
// PIE 종료
break;
case EEndPlayReason::RemovedFromWorld:
// 스트리밍으로 제거
break;
case EEndPlayReason::Quit:
// 게임 종료
break;
}
Super::EndPlay(EndPlayReason); // 마지막에 호출
}
EndPlay를 사용하세요! OnDestroyed는 호출되지 않는 경우가 있습니다 (에디터 종료 등). EndPlay는 모든 종료 상황에서 안정적으로 호출됩니다.
핵심 요약
- Constructor — CDO 생성, CreateDefaultSubobject로 컴포넌트 생성, 월드 접근 금지
- PostInitializeComponents — 컴포넌트 초기화 완료 후, 델리게이트 바인딩에 적합
- BeginPlay — 게임 시작, 다른 Actor 참조 안전, 타이머 시작
- Tick — 매 프레임 업데이트, DeltaTime 사용, 성능 주의
- EndPlay — 파괴 전 정리, 타이머/델리게이트 해제, OnDestroyed보다 권장
도전 과제
배운 내용을 직접 실습해보세요
ARPGCharacter에서 Constructor, PostInitializeComponents, BeginPlay, Tick, EndPlay 각 단계에 UE_LOG를 추가하고, 스폰/파괴 시 호출 순서를 콘솔에서 확인하세요. 각 단계에서 GetWorld() 유효성도 확인하세요.
RPG 캐릭터의 EndPlay에서 모든 타이머(GetWorldTimerManager().ClearAllTimersForObject), 델리게이트(RemoveDynamic), 참조를 정리하는 코드를 구현하세요. EEndPlayReason에 따른 분기 처리도 추가하세요.
런타임에 NewObject