나만의 작은 도서관
[TIL][C++] 251114 MMO 서버 개발 137일차: [언리얼] 다시 정리하는 액터의 Life Cycle, Broadcast한 델리게이트와 바인딩된 함수들은 언제 실행되는가?, 호출된 이후에 Tick이 실행될 가능성? 본문
Today I Learn
[TIL][C++] 251114 MMO 서버 개발 137일차: [언리얼] 다시 정리하는 액터의 Life Cycle, Broadcast한 델리게이트와 바인딩된 함수들은 언제 실행되는가?, 호출된 이후에 Tick이 실행될 가능성?
pledge24 2025. 11. 15. 02:42주의사항: 해당 글은 일기와 같은 기록용으로, 다듬지 않은 날것 그대로인 글입니다.
[언리얼] 다시 정리하는 액터의 Life Cycle

액터의 생명 주기(Life Cycle)이란?
- 레벨에 Actor가 생성, 초기화, 플레이, 소멸되는 전체과정
- 각 단계마다 특정 이벤트 함수들이 자동으로 호출된다.
- 각 이벤트 함수들이 어느 시점에 호출되는지 이해하면 적절한 시점에 로직을 채워 넣을 수 있음
액터 생성 단계
Construction Script
- 액터가 처음 생성될 때 실행.
- 에디터에서 액터의 속성(Property)을 수정할 때마다 호출되므로, 이곳에 게임플레이 로직을 작성하거나, 무거운 작업을 추가하면 안 된다.
- 여기서 액터의 속성은 에디터에서 액터의 디테일 패널에 노출되는 값들이 대표적이다. 따라서, 액터를 레벨에 배치한 다음 액터를 드래그로 이동시키면 조금만 이동해도 수십 번 Construction Script가 호출된다.
- 채워 넣는 로직 종류
- 액터의 초기 설정과 구성을 자동화할 때 사용한다.
- 에디터는 Construction Script가 실행된 후의 결과를 표시하므로, Construction Script에 작성한 로직의 결과를 에디터에서 시각적 피드백으로 받을 수 있다.
Event BeginPlay
- “월드에 배치된” 액터는 게임이 Play상태로 진입했을 때, 또는 Play상태인 게임에서 액터가 스폰될 때 딱 한 번만 호출
- 액터의 모든 초기화 작업이 완료된 시점과 액터가 레벨에서 Tick을 돌기 시작하는 시점 사이에 호출된다고 보면 된다.
- 채워 넣는 로직 종류
- 레벨에 등장한 시점에서 처리할 로직.
액터 루프 단계
Event Tick
- 게임의 매 프레임(틱)마다 호출되는 이벤트
- Delta Seconds 매개변수를 제공
Event Possessed(액터가 Pawn 또는 Pawn의 하위 클래스인 경우)
- 플레이어 컨트롤러(APlayerController)가 해당 액터를 대상으로 Possess()를 호출했을 때 호출됨.
Event UnPossessed(액터가 Pawn 또는 Pawn의 하위 클래스인 경우)
- 플레이어 컨트롤러(APlayerController)가 해당 액터를 대상으로 UnPossess()를 호출했을 때 호출됨.
액터 제거 단계

Event EndPlay
- 액터가 월드에서 제거되거나 파괴될 때(Ex. Destroy() 호출, 레벨 전환, PIE 종료) 호출되는 함수.
- Destroy() 함수가 호출되는 시점에 매우 빠르게 그다음으로 호출되는 함수
- 이 시점부터 액터는 더 이상 게임 로직에 참여하지 않으며, 자신이 소유한 리소스를 해제하기에 가장 적절한 게임플레이 단계의 마지막 순간이다.
- 채워 넣는 로직 종류
- 액터의 정리작업과 최종 정리
Event Destroyed
- 액터가 Destroy() 함수 호출 이후, 액터의 실질적 파괴가 일어나기 직전에 발생하는 이벤트.
- 블루프린트에서 발동하는 이벤트.
- C++에서 AActor::Destroyed() 내부에서 호출됨.
- 소멸 직전의 마무리 정리 작업 코드가 들어간다.
void AActor::Destroyed()
{
RouteEndPlay(EEndPlayReason::Destroyed); // EndPlay 호출
ReceiveDestroyed(); // BP의 Event Destroy임
OnDestroyed.Broadcast(this);
}
// 결론
// AActor::Destroyed() -> EndPlay() -> BP's Destroyed
// AActor::EndPlay -> UObject::BeginDestroy -> UObject::FinishDestroy
이벤트 호출 순서

[언리얼] 딱 한 번만 호출되는 노드 Do Once

- Tick에 넣으면 한 번만 발동한다.
AActor의 소멸자 사용은 안전할까?
- UObject에서 파생된 클래스들은 자동으로 언리얼의 GC에 의해 수명 주기가 관리된다. 따라서 UObject 계열의 클래스는 직접적인 소멸자 사용을 지양해야 한다.
- 소멸자 대신, GC가 객체를 정리하기 전에 호출하는 BeginDestroy()와 같은 특화된 UE함수를 사용하여 정리 작업을 수행하는 것이 옳다.
- AActor 또한 UObject의 하위 클래스이므로 같은 규칙이 적용된다.
UObject 계열의 클래스가 아닌 경우
- TSharedPtr이나 TWeakPtr 등을 사용하면 된다. UObject가 아닌 클래스를 UE의 스마트 포인터로 묶은 다음, 해당 클래스의 소멸자에 코드를 작성한다면, 이는 문제가 되지 않는다.
Broadcast 한 델리게이트와 바인딩된 함수들은 언제 실행되는가?
- “즉시, 그리고 동기적으로 실행된다”
- Broadcast()가 호출되는 순간, 프로그램의 실행 흐름은 해당 지점에서 멈추고, 델리게이트에 바인딩된 함수들을 순차적으로 모두 실행한다.(단, 바인딩된 함수들의 실행 순서는 보장 X) 모든 함수의 실행이 완료되면, Broadcast() 다음 코드로 실행 흐름이 넘어간다.
[언리얼] Destroy가 호출된 이후에 Tick이 실행될 가능성?
- “Destroy가 언제 호출되었는가?”가 중요하다.
- 대부분의 경우 UWorld::Destroy함수는 월드가 Play 상태일 때 호출되므로, Destroy를 호출하는 객체의 Tick이 액터의 Tick보다 먼저 호출되는가? 와 연관이 있다.
사전 지식: UWorld::Destory() 함수는 비동기 함수이다.
- 파괴한다는 이름을 가진 함수지만, 실제로 파괴는 그 즉시 일어나지 않는다. Destroy는 파괴할 액터를 “Pending Kill” 상태로 플래그만 세워놓고, 메모리 해제나 컴포넌트 제거는 이루어지지 않는다.
- World→Tick() 과정에서 액터 리스트를 순회하며 액터의 Tick을 호출하는데, 이때 PendingKill 상태인 액터는 Tick 호출 대상에서 제외된다. 그래서 같은 프레임 안에서 벌어지는 일은 아래와 같이 나뉜다.
Destroy 예시: 액터 A의 Tick()에서 액터 B를 Destroy 하는 경우
- 모든 액터의 Tick은 World의 Tick() 함수에 의해 순차적으로 호출된다.
- 만약 World가 액터 B의 Tick을 먼저 호출했다면, B는 해당 프레임에서 방해 없이 그대로 Tick을 돈다.
- 반대로 World가 액터 A의 Tick을 먼저 호출했다면, B는 PendingKill 상태가 되어 같은 프레임 안에서 예정되어 있던 Tick은 건너뛴다.
// 액터 A가 먼저 호출된 경우
1. World의 TickGroup에서 A의 ExcuteTick호출
2. A의 AActor::Tick // B를 Destroy
3. World의 TickGroup에서 B의 ExcuteTick호출
4. B는 pending kill 상태이므로 Tick 호출 안 함
Destroy 예시: 액터 A의 Delay 이후 노드에서 본인을 Destroy 하는 경우
- Delay노드는 Latent Action에 해당하며, Latent Action이 발동되는 시점은 액터의 Tick 내부이다. 정확히는 AActor::Tick에서 실행되는 것이며, AActor::Tick 내부에서 BP의 Tick → Latent Action 순으로 실행되므로, 같은 프레임 내에서 Tick 함수와 Destroy가 호출되는 것이 가능하다.
- 만약 AActor의 하위 클래스이고, 하위 클래스의 Tick의 최상단에 Super::Tick으로 작성했다면, 하위 클래스 Tick에 작성한 로직이 가장 늦게 실행되므로 하위 클래스의 Tick은 Destroy가 호출된 이후에 호출된다. 따라서 순서는 아래와 같다.
1. AActor::Tick
2. BP의 Tick 이벤트
3. Delay노드 뒤에 연결된 노드(Latent Action 발동) // 본인을 Destroy!
4. AActor의 하위 클래스 Tick(Super::Tick을 최상단에 배치한 경우) // pending kill 상태로 진입
- 이 경우 AActor의 하위 클래스의 Tick은 Pending Kill 상태로 진입하며, EndPlay가 호출된 이후의 시점이므로, 만약 EndPlay에서 메모리를 날려버렸다면, Tick에서 해당 메모리에 접근하는 순간 크래시가 난다. 이런 상황을 방지하기 위해 AActor의 하위 클래스의 Tick에서 Super::Tick을 최상단에 두었다면, 아래와 같이 pending kill 상태일 때는 Tick을 실행하지 않도록 처리해야 한다.
// PendingKill 상태인지 확인하는 코드
if (IsPendingKill() || IsActorBeingDestroyed())
return;
// PendingKill 상태라면 IsValid는 false를 반환한다.(이게 좀 더 추천됨)
if (!IsValid(this)) return;
