물론 알아서 잘 해주는 언리얼 엔진이지만...
언리얼 엔진의 UMG 를 활용해 게임 UI 를 만드는 것에 대한 강의를 들었다.
예제를 다루며 특정 상황에 위젯 객체를 RemoveFromParent 함수를 호출하여 화면에서 삭제하고 객체를 가리키던 포인터 변수에 nullptr 를 대입하였다.
이 때 메모리가 해제되는 것인지에 대한 언급이 없어서, RemoveFromParent 함수에서 메모리 해제가 되는 것이 맞는지 확인하고 싶어졌다.
(생성자에서 초기화하는 것 외에, 런타임에 생성되는 객체를 가리키는 raw pointer 변수에 특정 함수에서 nullptr 를 직접 assign 하는 것에 대한 두려움이 있다 ㅋ... 지난 첫 팀프로젝트에서 메모리 릭을 잔뜩 만들어냈던 장본인으로숴...^^ 다행히 고쳤지만...)
CreateWidget
UUserWidget* 타입의 WidgetInstance 변수에 CreateWidget<UUserWidget> 함수를 통해 생성한 객체를 가리키도록 할당하였으므로, 우선 CreateWidget 이 어떤 방식으로 객체를 생성하고 있는지 살펴보았다.
template <typename WidgetT = UUserWidget, typename OwnerType = UObject>
WidgetT* CreateWidget(OwnerType OwningObject, TSubclassOf<UUserWidget> UserWidgetClass = WidgetT::StaticClass(), FName WidgetName = NAME_None)
{
static_assert(TIsDerivedFrom<WidgetT, UUserWidget>::IsDerived, "CreateWidget can only be used to create UserWidget instances. If creating a UWidget, use WidgetTree::ConstructWidget.");
static_assert(TIsDerivedFrom<TPointedToType<OwnerType>, UWidget>::IsDerived
|| TIsDerivedFrom<TPointedToType<OwnerType>, UWidgetTree>::IsDerived
|| TIsDerivedFrom<TPointedToType<OwnerType>, APlayerController>::IsDerived
|| TIsDerivedFrom<TPointedToType<OwnerType>, UGameInstance>::IsDerived
|| TIsDerivedFrom<TPointedToType<OwnerType>, UWorld>::IsDerived, "The given OwningObject is not of a supported type for use with CreateWidget.");
SCOPE_CYCLE_COUNTER(STAT_CreateWidget);
FScopeCycleCounterUObject WidgetObjectCycleCounter(UserWidgetClass, GET_STATID(STAT_CreateWidget));
if (OwningObject)
{
return Cast<WidgetT>(UUserWidget::CreateWidgetInstance(*OwningObject, UserWidgetClass, WidgetName));
}
return nullptr;
}
1. OwningObject 에 대한 타입 체크 (assert)
- 위젯을 만들 수 있는 OwningObject 의 타입 : UWidget, UWidgetTree, 플레이어 컨트롤러, 게임 인스턴스, 월드
- UWidgetTree : The widget tree manages the collection of widgets in a blueprint widget.
- UWidget : This is the base class for all wrapped Slate controls that are exposed to UObjects.
(출처: WidgetTree.h / Widget.h 엔진 코드)
2. CreateWidgetInstance 호출
UUserWidget::CreateWidgetInstance
- 강의 예제의 경우 PlayerController 에서 위젯 객체를 생성하므로 오버로드된 함수 중 해당 함수를 살펴보았다.
UUserWidget* UUserWidget::CreateWidgetInstance(APlayerController& OwnerPC, TSubclassOf<UUserWidget> UserWidgetClass, FName WidgetName)
{
if (!OwnerPC.IsLocalPlayerController())
{
const FText FormatPattern = LOCTEXT("NotLocalPlayer", "Only Local Player Controllers can be assigned to widgets. {PlayerController} is not a Local Player Controller.");
FFormatNamedArguments FormatPatternArgs;
FormatPatternArgs.Add(TEXT("PlayerController"), FText::FromName(OwnerPC.GetFName()));
FMessageLog("PIE").Error(FText::Format(FormatPattern, FormatPatternArgs));
}
else if (!OwnerPC.Player)
{
const FText FormatPattern = LOCTEXT("NoPlayer", "CreateWidget cannot be used on Player Controller with no attached player. {PlayerController} has no Player attached.");
FFormatNamedArguments FormatPatternArgs;
FormatPatternArgs.Add(TEXT("PlayerController"), FText::FromName(OwnerPC.GetFName()));
FMessageLog("PIE").Error(FText::Format(FormatPattern, FormatPatternArgs));
}
else if (UWorld* World = OwnerPC.GetWorld())
{
UGameInstance* GameInstance = World->GetGameInstance();
UObject* Outer = GameInstance ? StaticCast<UObject*>(GameInstance) : StaticCast<UObject*>(World);
return CreateInstanceInternal(Outer, UserWidgetClass, WidgetName, World, CastChecked<ULocalPlayer>(OwnerPC.Player));
}
return nullptr;
}
1. Player Controller 가 로컬인지, 플레이어를 possess 했는지 validate
2. 게임 인스턴스 포인터를 Outer 라는 UObject* 로 캐스팅하여 (없으면 월드 포인터)
3. CreateInstanceInternal 함수 호출 / Outer 인자로 전달
UUserWidget::CreateInstanceInternal
UUserWidget* UUserWidget::CreateInstanceInternal(UObject* Outer, TSubclassOf<UUserWidget> UserWidgetClass, FName InstanceName, UWorld* World, ULocalPlayer* LocalPlayer)
{
LLM_SCOPE_BYTAG(UI_UMG);
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
// Only do this on a non-shipping or test build.
if (!CreateWidgetHelpers::ValidateUserWidgetClass(UserWidgetClass))
{
return nullptr;
}
#else
if (!UserWidgetClass)
{
UE_LOG(LogUMG, Error, TEXT("CreateWidget called with a null class."));
return nullptr;
}
#endif
#if !UE_BUILD_SHIPPING
// Check if the world is being torn down before we create a widget for it.
if (World)
{
// Look for indications that widgets are being created for a dead and dying world.
ensureMsgf(!World->bIsTearingDown, TEXT("Widget Class %s - Attempting to be created while tearing down the world '%s'"), *UserWidgetClass->GetName(), *World->GetName());
}
#endif
if (!Outer)
{
FMessageLog("PIE").Error(FText::Format(LOCTEXT("OuterNull", "Unable to create the widget {0}, no outer provided."), FText::FromName(UserWidgetClass->GetFName())));
return nullptr;
}
LLM_SCOPE_DYNAMIC_STAT_OBJECTPATH(Outer->GetPackage(), ELLMTagSet::Assets);
LLM_SCOPE_DYNAMIC_STAT_OBJECTPATH(UserWidgetClass, ELLMTagSet::AssetClasses);
UE_TRACE_METADATA_SCOPE_ASSET_FNAME(InstanceName, UserWidgetClass->GetFName(), Outer->GetPackage()->GetFName());
UUserWidget* NewWidget = NewObject<UUserWidget>(Outer, UserWidgetClass, InstanceName, RF_Transactional);
if (LocalPlayer)
{
NewWidget->SetPlayerContext(FLocalPlayerContext(LocalPlayer, World));
}
NewWidget->Initialize();
return NewWidget;
}
1. 빌드 세팅에 따라 다른 UserWidgetClass 및 World 의 validation
2. Outer 의 validation
3. NewObject<UUserWidget> 으로 위젯 객체 생성 (드디어...)
4. 위젯의 player context 를 로컬 플레이어로 설정
5. 초기화 및 반환
UWidget::RemoveFromParent
참고: UUserWidget 은 UWidget 을 상속하는 서브클래스로, 엔진 코드의 주석에 따르면 A widget that enables UI extensibility through WidgetBlueprint. 라고 한다.
참고2: AddToViewport 인데 RemoveFromViewport 가 아닌 이유?
엔진 코드를 살펴보니 RemoveFromViewport 함수가 존재하는데, deprecated 된 함수로 RemoveFromParent 를 사용하라고 지시하고 있다.
void UWidget::RemoveFromParent()
{
if (!HasAnyFlags(RF_BeginDestroyed))
{
if (bIsManagedByGameViewportSubsystem)
{
if (UGameViewportSubsystem* Subsystem = UGameViewportSubsystem::Get(GetWorld()))
{
Subsystem->RemoveWidget(this);
}
}
else if (UPanelWidget* CurrentParent = GetParent())
{
CurrentParent->RemoveChild(this);
}
else
{
#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST)
if (GetCachedWidget().IsValid() && GetCachedWidget()->GetParentWidget().IsValid() && !IsDesignTime())
{
FText WarningMessage = FText::Format(LOCTEXT("RemoveFromParentWithNoParent", "UWidget::RemoveFromParent() called on '{0}' which has no UMG parent (if it was added directly to a native Slate widget via TakeWidget() then it must be removed explicitly rather than via RemoveFromParent())"), FText::AsCultureInvariant(GetPathName()));
// @todo: nickd - we need to switch this back to a warning in engine, but info for games
FMessageLog("PIE").Info(WarningMessage);
}
#endif
}
}
}
1. BeginDestroyed flag 를 확인
- BeginDestroy 함수가 asynchronously 객체를 파괴하기 때문에 로직 수행하는 시점에 확인이 필요하다.
- 아래와 같이 BeginDestroy 또한 뷰포트에서 위젯의 삭제를 수행한다.
void UWidget::BeginDestroy()
{
if (bIsManagedByGameViewportSubsystem)
{
if (UGameViewportSubsystem* Subsystem = UGameViewportSubsystem::Get(GetWorld()))
{
Subsystem->RemoveWidget(this);
}
}
Super::BeginDestroy();
}
2-1. 뷰포트 subsystem->RemoveWidget
void UGameViewportSubsystem::RemoveWidget(UWidget* Widget)
{
if (Widget && Widget->bIsManagedByGameViewportSubsystem)
{
FSlotInfo SlotInfo;
TObjectKey<UWidget> WidgetKey = Widget;
ViewportWidgets.RemoveAndCopyValue(WidgetKey, SlotInfo);
RemoveWidgetInternal(Widget, SlotInfo.FullScreenWidget, SlotInfo.LocalPlayer);
OnWidgetRemoved.Broadcast(Widget);
}
}
void UGameViewportSubsystem::RemoveWidgetInternal(UWidget* Widget, const TWeakPtr<SConstraintCanvas>& FullScreenWidget, const TWeakObjectPtr<ULocalPlayer>& LocalPlayer)
{
Widget->bIsManagedByGameViewportSubsystem = false;
if (TSharedPtr<SWidget> WidgetHost = FullScreenWidget.Pin())
{
// If this is a game world remove the widget from the current world's viewport.
UWorld* World = Widget->GetWorld();
if (World && World->IsGameWorld())
{
if (UGameViewportClient* ViewportClient = World->GetGameViewport())
{
TSharedRef<SWidget> WidgetHostRef = WidgetHost.ToSharedRef();
ViewportClient->RemoveViewportWidgetContent(WidgetHostRef);
// We may no longer have access to our owning player if the player controller was destroyed
// Passing nullptr to RemoveViewportWidgetForPlayer will search all player layers for this widget
ViewportClient->RemoveViewportWidgetForPlayer(LocalPlayer.Get(), WidgetHostRef);
}
}
}
}
코드를 보아하니 Widget이 Key 와 SlotInfo , 즉 map으로 관리되는 모양이다.
1. FViewportWidgetList = TMap<TObjectKey<UWidget>, FSlotInfo> 타입의 ViewportWidgets 에서 위젯을 지우고
2. value 를 SlotInfo 에 copy 한 후 RemoveWidgetInternal 함수에 전달한다
3. RemoveWidgetInternal 은 RemoveViewportWidgetContent / RemoveViewportWidgetForPlayer 이 두 함수로 위젯 삭제 요청
RemoveViewportWidgetContent
- 인자로 넘겨 받은 WidgetHostRef 를 SOverlay::RemoveSlot 함수에 전달하여 호출한다.
SOverlay / RemoveSlot
- SOverlay 는 위젯 관리 계층 구조 - 위젯들이 서로의 위/아래로 레이어될 수 있게 Slot 으로 관리하고 있다.
- TPanelChildren<FOverlaySlot> Children 이라는 변수에 SOverlay 의 위젯들이 담긴 slot 들이 저장된다.
- 위에서 호출된 RemoveSlot 함수는 Children 에 Remove(SlotWidget) 를 호출하여 삭제한다.
TPanelChildren / Remove
/** Removes the corresponding widget from the set of children if it exists. Returns the index it found the child at, INDEX_NONE otherwise. */
int32 Remove(const TSharedRef<SWidget>& SlotWidget)
{
for (int32 SlotIdx = 0; SlotIdx < Num(); ++SlotIdx)
{
if (SlotWidget == Children[SlotIdx]->GetWidget())
{
Children.RemoveAt(SlotIdx);
return SlotIdx;
}
}
return INDEX_NONE;
}
- TArray<TUniquePtr<SlotType>> Children 에 RemoveAt 으로 해당하는 위젯을 삭제한다!
드디어!!!!!! 각 Slot 이 TUniquePtr 로 관리되고 있기 때문에, RemoveAt 을 통해 레퍼런스 카운터가 0이 되어 메모리가 해제되겠구나 안심할 수 있었다.
RemoveWidgetInternal / RemoveViewportWidgetForPlayer
- GameLayerManager 에 RemoveWidgetForPlayer 호출
SGameLayerManager::RemoveWidgetForPlayer
- PlayerLayer->Widget->RemoveSlot(ViewportContent) 로 위젯 삭제 함수를 호출한다.
이 RemoveSlot 은 위에서 살펴본 RemoveSlot 과 동일하다. (즉 PlayerLayer 의 Widget 은 SOverlay 타입의 위젯)
2-2. 뷰포트 섭시스템에서 관리되지 않는 경우, UPanelWidget 타입의 parent 객체에서 RemoveChild 호출
UPanelWidget::RemoveChild / RemoveChildAt(Index)
- 인자로 넘겨받은 위젯을 기반으로 인덱스를 GetChildIndex --> RemoveChildAt(해당 인덱스) 호출
bool UPanelWidget::RemoveChildAt(int32 Index)
{
if ( Index < 0 || Index >= Slots.Num() )
{
return false;
}
UPanelSlot* PanelSlot = Slots[Index];
Slots.RemoveAt(Index);
if (PanelSlot)
{
if (PanelSlot->Content)
{
PanelSlot->Content->Slot = nullptr;
}
OnSlotRemoved(PanelSlot);
const bool bReleaseChildren = true;
PanelSlot->ReleaseSlateResources(bReleaseChildren);
PanelSlot->Parent = nullptr;
PanelSlot->Content = nullptr;
}
else
{
return false;
}
InvalidateLayoutAndVolatility();
return true;
}
- TArray<TObjectPtr<UPanelSlot>> Slots 에서 Slots[Index] 로 삭제할 슬롯의 포인터를 변수에 저장하고
- Slots.RemoveAt(Index); 로 삭제한다.
- 이후 PanelSlot 포인터가 nullptr 가 아닌지 validate 하고
- ReleaseSlateResources 리소스 해제하는 듯 하다.
예상했던 것보다 훠어어얼씬 여러 단계를 거쳐 위젯이 관리되고 있구나... 깨닫게 되었다
그리고... 앞으로 Remove 어쩌구 하는 엔진의 코드는 그냥 메모리 해제가 포함되어 있다고 굳게 믿으면 될 것 같다...!
'언리얼_엔진_게임개발_공부 > 언리얼 C++' 카테고리의 다른 글
[언리얼/C++] 굴러다니며 벽에 부딪힐 때 방향 바뀌는 바위 (반사 벡터) (0) | 2025.02.14 |
---|---|
[언리얼/C++] 디버프 - 코드로 UTextBlock 동적 생성 및 추가 (0) | 2025.02.13 |
[언리얼/C++] ApplyDamage / TakeDamage 파헤쳐보자 (1) | 2025.02.07 |
[언리얼/C++] TSubclassOf 와 TSoftClassPtr - Get() 함수 비교하기 (0) | 2025.02.06 |
[언리얼/C++] LineTraceSingleByChannel 로 충돌 감지 (0) | 2025.02.04 |