C++/Unreal Engine

[UE4] 디버깅 기록 Delegate / BeginDestroy

로파이 2021. 10. 9. 14:50

콜백을 지정하는 Delegate를 쓸 때 주의사항

  • Delegate는 함수가 바인딩 되어 있는지 확인하는 IsBound()함수가 있다.
  • 바인드 되지 않은 델리게이트를 Execute()혹은 Broadcast() 시 런타임 에러가 발생한다. (어설션 실패)
  • Clear()는 바인드 된 함수를 모두 제거하는 데 바인드된 함수가 없어도 런타임 에러가 발생하지 않는다.
  • Delegate로 설정할 바탕 함수(실질적인 함수)는 반드시 UFUNCTION으로 등록해 엔진이 찾을 수 있게한다.

※ 2021-10-09 가끔씩 델리게이트 함수 오류로 보이는 가비지 컬렉터가 메모리를 지우거나 pure virtual function is calle와 같은 크래시가 발생하는데 델리게이트 함수를 사용하는 클래스에서 BeginDestroy() 함수에서 바인드를 모두 제거해보는 것으로 실험 중에 있다.

 

BeginDestroy()

일반적으로 클래스의 액터 포인터나 컴포넌트를 UPROPERTY()로 선언해야 자동 TWeakObjectPtr로 파싱해서 메모리를 관리하는 것으로 알고 있다. 

BeginDestroy()에서 컴포넌트를 접근할 때 Super::BeginDestroy() 이전이 됐든 이후가 됐든 해당 컴포넌트를 접근할 때 댕글링 포인터로 인식되어 크래시가 나는 경우가 발생하는 것 같다.

댕글링 포인터 크래시

논리상 액터의 파괴 이전에 컴포넌트가 파괴되는 것이 이상하다라고 생각되는데... 액터가 파괴될 것이라고 마킹된 이상 가비지 컬렉터의 메모리 해제 시점이 멀티 스레딩을 이용한 처리 방식으로 언제 정확히 파괴되는 지 알 수 없고 크래시 리포트를 보니 BeginDestroy()에서 댕글링 포인터의 컴포넌트를 접근하게 되는 것같다.

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "../GameCore.h"
#include "Blueprint/UserWidget.h"
#include "FPSHUDWidget.generated.h"

/**
 * 
 */
UCLASS()
class GAMECORE_API UFPSHUDWidget : public UUserWidget
{
	GENERATED_BODY()

	friend class AMainPlayerController;
public:
	virtual void NativeConstruct() override;

	virtual void BeginDestroy() override;
private:
	void BindPlayerStatComponent(class UStatComponent* Comp);

	void UpdatePlayerStat();

private:
	UPROPERTY(VisibleAnywhere, Category = UI, Meta = (PrivateAccess = true));
	class UProgressBar* ArmorBar = nullptr;

	UPROPERTY(VisibleAnywhere, Category = UI, Meta = (PrivateAccess = true));
	class UProgressBar* HealthBar = nullptr;
};
void UFPSHUDWidget::BeginDestroy()
{
	Super::BeginDestroy();

	ArmorBar->PercentDelegate.Clear();
	HealthBar->PercentDelegate.Clear();
}

클래스에서 UPROPERTY 매크로로 선언하고 nullptr 초기화를 사용함에도 BeginDestroy()의 ArmorBar, HealthBar 접근이 오류를 발생하는 이유가 BeginDestroy() 이전에 가비지 컬렉터에 등록되어 메모리 해제가 시작된 것으로 보인다.

FORCEINLINE bool IsValid(const UObject *Test)
{
	return Test && !Test->IsPendingKill();
}

IsValid라는 널 포인터 체크 + 킬 펜딩 체크를 하고 유효할 때만 델리게이트를 지우는 로직은 크래시를 발생시키지 않는것으로 보아 IsPendingKill()에서 걸러지는 것 같다. 아직은 델리게이트를 꼭 삭제때 Unbind() 해야하는 지는 아직 연구중 이다...

void UFPSHUDWidget::BeginDestroy()
{
	Super::BeginDestroy();

	if (::IsValid(ArmorBar))
	{
		ArmorBar->PercentDelegate.Clear();
	}

	if (::IsValid(HealthBar))
	{
		HealthBar->PercentDelegate.Clear();
	}
}