PART 1 · 강의 5/7

Actor 라이프사이클

생성부터 파괴까지, Actor의 생명주기 완벽 이해

01

라이프사이클 개요

Actor 생성부터 파괴까지의 흐름

Actor 라이프사이클 순서 /* ┌──────────────────────────────────────────────────────────────┐ │ ACTOR LIFECYCLE │ ├──────────────────────────────────────────────────────────────┤ │ │ │ ┌─────────────┐ │ │ │ Constructor │ CDO 생성, 컴포넌트 생성 │ │ └──────┬──────┘ │ │ │ │ │ v │ │ ┌──────────────────────┐ │ │ │ PostInitProperties │ 프로퍼티 초기화 후 │ │ └──────────┬───────────┘ │ │ │ │ │ v │ │ ┌──────────────────────────┐ │ │ │ PostInitializeComponents │ 모든 컴포넌트 초기화 완료 │ │ └──────────┬───────────────┘ │ │ │ │ │ v │ │ ┌───────────┐ │ │ │ BeginPlay │ 게임 시작, 월드 준비 완료 │ │ └─────┬─────┘ │ │ │ │ │ v │ │ ┌──────┐ │ │ │ Tick │ 매 프레임 업데이트 │ │ └──┬───┘ │ │ │ (반복) │ │ v │ │ ┌─────────┐ │ │ │ EndPlay │ 파괴 전 정리, 타이머/델리게이트 해제 │ │ └────┬────┘ │ │ │ │ │ v │ │ ┌──────────────┐ │ │ │ BeginDestroy │ 메모리 해제 시작 │ │ └──────────────┘ │ │ │ └──────────────────────────────────────────────────────────────┘ */
02

생성자 (Constructor)

CDO 생성과 컴포넌트 초기화

C++ 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 참조 (아직 존재하지 않을 수 있음) // - 게임플레이 로직 실행 }
⚠️ 생성자 vs CDO

생성자는 Class Default Object (CDO)를 생성할 때도 호출됩니다. CDO는 클래스의 기본값을 저장하는 템플릿 객체로, 실제 게임 월드에 존재하지 않습니다. 따라서 생성자에서 월드 의존적인 코드를 작성하면 안 됩니다.

03

PostInitializeComponents

모든 컴포넌트 초기화 완료 후

C++ 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 검색/참조
  • 월드 상태 의존 로직
  • 게임플레이 시작 로직
04

BeginPlay

게임 시작 시 초기화

C++ 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); }
📌 BeginPlay 호출 시점

레벨에 배치된 Actor: 레벨 로드 시 호출
SpawnActor로 생성: SpawnActor 직후 호출
스트리밍된 Actor: 스트리밍 완료 후 호출

05

Tick

매 프레임 업데이트

C++ 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 최적화

Tick은 성능에 큰 영향을 줍니다. 가능하면 타이머, 이벤트, 또는 틱 간격 조절을 사용하세요.

틱 최적화 기법 // 틱 비활성화 PrimaryActorTick.bCanEverTick = false; // 런타임에 틱 활성화/비활성화 SetActorTickEnabled(false); SetActorTickEnabled(true); // 틱 간격 조절 (매 프레임 대신 N초마다) PrimaryActorTick.TickInterval = 0.1f; // 0.1초마다
06

EndPlay

파괴 전 정리 작업

C++ 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 vs OnDestroyed

EndPlay를 사용하세요! OnDestroyed는 호출되지 않는 경우가 있습니다 (에디터 종료 등). EndPlay는 모든 종료 상황에서 안정적으로 호출됩니다.

SUMMARY

핵심 요약

  • Constructor — CDO 생성, CreateDefaultSubobject로 컴포넌트 생성, 월드 접근 금지
  • PostInitializeComponents — 컴포넌트 초기화 완료 후, 델리게이트 바인딩에 적합
  • BeginPlay — 게임 시작, 다른 Actor 참조 안전, 타이머 시작
  • Tick — 매 프레임 업데이트, DeltaTime 사용, 성능 주의
  • EndPlay — 파괴 전 정리, 타이머/델리게이트 해제, OnDestroyed보다 권장
PRACTICE

도전 과제

배운 내용을 직접 실습해보세요

실습 1: 라이프사이클 이벤트 로깅

ARPGCharacter에서 Constructor, PostInitializeComponents, BeginPlay, Tick, EndPlay 각 단계에 UE_LOG를 추가하고, 스폰/파괴 시 호출 순서를 콘솔에서 확인하세요. 각 단계에서 GetWorld() 유효성도 확인하세요.

실습 2: EndPlay 정리 패턴 구현

RPG 캐릭터의 EndPlay에서 모든 타이머(GetWorldTimerManager().ClearAllTimersForObject), 델리게이트(RemoveDynamic), 참조를 정리하는 코드를 구현하세요. EEndPlayReason에 따른 분기 처리도 추가하세요.

심화 과제: 동적 컴포넌트 생성 및 Tick 최적화

런타임에 NewObject로 RPG 장비 비주얼 컴포넌트를 동적 생성하고, RegisterComponent()로 등록하세요. 또한 PrimaryActorTick.TickInterval을 조절하여 원거리 NPC의 틱 빈도를 줄이는 최적화를 구현하세요.