Post

Actor Life Cycle의 만료

Actor Life Cycle의 만료

출처 및 참고 자료
https://unreal.gg-labs.com/wiki-archives/common-pitfalls/how-to-prevent-crashes-due-to-dangling-actor-pointers https://dev.epicgames.com/documentation/ko-kr/unreal-engine/unreal-engine-actor-lifecycle https://algorfati.tistory.com/75

Actor Life Cycle (액터 생명 주기)

공식 문서를 보면 액터의 생명주기를 자세히 알 수 있다. 간단히 요약하면 게임 시작 시 여러 함수들을 거쳐 AActor::BeginPlay()까지 호출된다. 액터의 라이프사이클이 종료될 때는 마찬가지로 AActor::EndPlay()가 호출되면서 마무리된다.

런타임에서 액터를 없앨 때, AActor::Destroy()를 호출하게 되는데, 문제는 AActor::Destroy()를 호출한다고 바로 액터가 소멸되는 것이 아니고 RF_PendingKill 플래그가 표시되어 다음 가비지 컬렉션에서 사라지게 된다. 이 동안은 해당 액터는 nullptr이 아니기 때문에 null 체크만 하면 크래시가 나기 십상이다.

null, valid, weakptr 체크

언리얼 메모리관리와 스마트포인터, 순환 참조 문제는 위 블로그에 아주 잘 나와있다. 간단하게 액터를 스폰하고 파괴하는 코드를 짜본다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
public:
	UFUNCTION(BlueprintCallable)
	void CreateActor();
	UFUNCTION(BlueprintCallable)
	bool CheckActor();
	UFUNCTION(BlueprintCallable)
	void DestroyActor();

public:
	UPROPERTY(EditAnywhere, BlueprintReadWrite)
	TSubclassOf<AActor> ActorClass;

private:
	AActor* ActorInstance;
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
void UPointerCheckComponent::CreateActor()
{
	FActorSpawnParameters param;
	ActorInstance = GetWorld()->SpawnActor<AActor>(ActorClass,  param);
	WeakPtr = ActorInstance;
}

bool UPointerCheckComponent::CheckActor()
{
	bool returnal = true;
  //null 체크
	if (ActorInstance == nullptr)
	{
		UE_LOG(LogTemp, Warning, TEXT("1. NULL Check: null"));
		returnal = false;
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("1. NULL Check: NOT null"));
	}
  //valid 체크
	if (IsValid(ActorInstance))
	{
		UE_LOG(LogTemp, Warning, TEXT("2. Valid Check: valid"));
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("2. Valid Check: NOT valid"));
		returnal = false;
	}
  //pending kill 체크
	if (ActorInstance && ActorInstance->IsPendingKill() == true)
	{
		UE_LOG(LogTemp, Warning, TEXT("3. PendingKill Check: true"));
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("3. PendingKill Check: false"));
	}
  //lowlevel valid 체크
	if (ActorInstance->IsValidLowLevel() == true)
	{
		UE_LOG(LogTemp, Warning, TEXT("4. Low Valid Check: valid"));
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("4. Low Valid Check: NOT valid"));
		returnal = false;
	}
	return returnal;
}

void UPointerCheckComponent::DestroyActor()
{
	if (CheckActor() == true)
	{
		UE_LOG(LogTemp, Warning, TEXT("Destroying Actor.. is valid.."));
		ActorInstance->Destroy();
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("Destroying Actor.. is not valid.."));
	}
}

이렇게 해서 CreateActor() -> DestroyActor() -> CheckActor()를 호출하면 아래와 같이 출력된다.

img-description

AActor::Destroy()를 호출했지만 여전히 null이 아니며 valid하진 않지만 valid in lowlevel에서는 여전히 유효하다. 이 상황에서 다른 오브젝트가 참조할 때 null만 체크해버리면 크래시가 난다. PendingKill이 true가 되었는데, 이것은 다음 가비지 컬렉션에서 메모리 할당 해제되는 플래그이다.

가장 깔끔한 방법: TWeakObjectPtr

해당 오브젝트가 유효한지 아닌지 알아보는 안전한 방법은 TWeakObjectPtr을 사용하는 것이다.

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
void UPointerCheckComponent::CreateActor()
{
	FActorSpawnParameters param;
	ActorInstance = GetWorld()->SpawnActor<AActor>(ActorClass,  param);
	WeakPtr = ActorInstance;
}

bool UPointerCheckComponent::CheckActor()
{
	if (WeakPtr.IsValid() == true)
	{
		UE_LOG(LogTemp, Warning, TEXT("5. WeakPtr Check: valid"));
	}
	else
	{
		UE_LOG(LogTemp, Warning, TEXT("5. WeakPtr Check: NOT valid"));
		returnal = false;
	}
}

이렇게 하게되면 액터가 파괴되었을 때 즉각 유효하지 않다고 반환해준다.

결론

이러나 저러나 자주 생성하고 사라지는 액터들을 참조하는 경우는 언리얼 공식 문서에 나와있는대로
발생 과정과 상관없이, 액터는 RF_PendingKill 로 표시되어 다음 가비지 컬렉션 주기 동안 UE가 메모리에서 할당 해제합니다. 또한, 보류 중인 킬을 수동으로 확인하는 대신 FWeakObjectPtr<AActor> 를 사용하는 것이 더 깔끔합니다.
강한 참조는 필요한 부분에만 쓰고 나머진 WeakObjectPtr로 참조하면 참조 그래프에 부담도 덜하고 크래시도 피할 수 있다.

This post is licensed under CC BY 4.0 by the author.