How to thorughly capture all live children widgets of a specified root widget in game viewport?

I’m creating a UI layout conversion system which relies on runtime widget references in viewport, the concept behind it is to iterate through all children of a live viewport root widget to execute certain actions to their RenderTransform based on their classes (Text, Image, UserWidget… etc.).

For reference, let’s assume the viewport live widget hierarchy looks like this at runtime, (presented in their abstract classes for simplicity):

GAME VIEWPORT HIERARCHY:

  • UUserWidget ← the root widget
    • UPanelWidget
      • UImage
      • UTextBlock
    • UVerticalBox
      • UUserWidget ← nested user widget.
      • UTextBlock
    • UDynamicEntryBox ← this where I’m struggling :cry:
      • UUserWidget
      • UUserWidget

The goal here is to capture all widget object references as this live hierarchy, including widget entries in DynamicEntryBox, while also keeping this operation as less taxing to performance as possible. So I’ve streamlined some methods from unreal engine source code to achieve the expected result, but each of the methods I tried is either not thorough, or causes major overhead to performance.

It’s somewhat a complex case, so please bear with me as I’ll try to walk you through each approach briefly to give you some context:

UFUNCTION(BlueprintCallable, meta=(DefaultToSelf="Target"))
static TArray<UWidget*> GetChildrenUsingWidgetTree(const UUserWidget* Target)
{
	TArray<UWidget*> Children;
	Children.Empty();
	
	TArray<UWidget*> AllWidgets = Target->WidgetTree->GetAllWidgets(Children);

	for (const auto& Child : AllWidgets)
	{
		Children.Add(Child);

		if (const auto AsUserWidget = Cast<UUserWidget>(Child))
		{
			// Recursively get the children of nested UserWidget children too...
			Children.Append(GetChildrenUsingWidgetTree(AsUserWidget));
		}
	}

	return Children;
}

I’ve tested this implementation against the viewport hierarchy mentioned at the top, and it succeeded in capturing all the children of Target, including those in nested UserWidgets, with an acceptable performance; However, it failed to capture the underlying children (the entires) of the DynamicEntryBox, even after making sure to call this function after all the entries have been constructed; I also tried to use other iterative methods in this class, like: UWidgetTree::ForEachWidgetAndDescendants, UWidgetTree::ForWidgetAndChildren, and UWidgetTree::ForEachWidget, but to no avail. :cry:

TLDR; We could say that iterating children using UWidgetTree is fast and maintains hierarchy order, but not thorough.

So, I tried to look for another apporach and I stumbled upon TObjectIterator and TObjectRange in UObjectIterator.h, which pretty much let you iterate through all objects of the template class you provide; in my usecase, it’s implemented like so:

UFUNCTION(BlueprintPure)
static TArray<UWidget*> GetAllWidgetsUsingObjectChain()
{
	if (GEngine == nullptr)
	{
		return {};
	}

	TArray<UWidget*> LiveWidgets;
	LiveWidgets.Empty();
	
	for (UWidget* Widget : TObjectRange<UWidget>(RF_ClassDefaultObject | RF_ArchetypeObject, true, EInternalObjectFlags::Garbage))
	{
		if (!IsValid(Widget))
		{
			continue;
		}
		
		if (const UWorld* World = Widget->GetWorld())
		{
			if (World->WorldType == EWorldType::PIE || World->WorldType == EWorldType::Game)
			{
				LiveWidgets.Add(Widget);
			}
		}
	}

	return LiveWidgets;
}

I just had to make sure I filter the operation by excluding class default objects and archetype objects, and included only the widgets that are part of a PIE world or Game world so I prevent capturing editor widgets as well.

While this method is very thorough and captures each and every live widget in-game, it was too slow compared to iterating through children in a widget tree, not to mention that this doesn’t maintain any hierarchical order at all, so it could capture a child widget first then a root widget in an indeterministic order.

I ditched this approach all together mainly due to the performance overhead it causes, which is impractical for my use-case.

In the meantime, I’m experimenting with Slate API, and I stumbled upon SWidget::GetAllChildren and SWidget::GetChildren methods, I tired to utilize these method as follows:

static void GetAllChildrenUsingSlate_Internal(SWidget& Target)
{
	Target.GetAllChildren()->ForEachWidget([&](SWidget& Child)
	{
		// Debugging purposes only, will be stripped from Shipping builds...
		if (auto MetaData = Child.GetMetaData<FReflectionMetaData>(); MetaData.IsValid())
		{
			UE_LOG(LogTemp, Display, TEXT("%s"), *MetaData->Name.ToString());
		}
		
		GetAllChildrenUsingSlate_Internal(Child);
	});
}

I also created another entry function to be able to call the above function from blueprints:

UFUNCTION(BlueprintCallable, meta=(DefaultToSelf="Root"))
static void GetAllChildrenUsingSlate(UWidget* Root)
{
	if (!IsValid(Target))
	{
		return;
	}

	GetAllChildrenUsingSlate_Internal(*Root->GetCachedWidget().Get());
}

To my surprise, this approach pretty much succeeded in capturing all children widgets of Root, including entry widgets of the DynamicEntryBox mentioned earlier, but strangely enough, this only works when passing the DynamicEntryBox widget variable directly from the blueprint asset itself, but when passing the parent widget of DynamicEntryBox to the function as the Root argument, the entries are not captured! :melting_face:

It doesn’t make any sense to me why this behaviour happens and I feel I’m pretty much missing something. :confused:

Can anyone point me out to what I’m missing in any of these approaches, or to why SWidget::GetChildren behave this way?

Thank you so much for your time reading this, and I would appreciate any help on this matter. <3

Edit:

Solved by changing GetAllChildrenUsingSlate_Internal to this:

static void GetAllChildrenUsingSlate_Internal(SWidget& Target)
{
	Target.GetAllChildren()->ForEachWidget([&](SWidget& Child)
	{
		// Debugging purposes only, will be stripped from Shipping builds...
		if (auto MetaData = Child.GetMetaData<FReflectionMetaData>(); MetaData.IsValid())
		{
			UE_LOG(LogTemp, Display, TEXT("%s"), *MetaData->Name.ToString());
		}

		// Solved by recursing the function only when the SWidget child have children!
> 		if (Child.GetAllChildren()->Num() > 0)
> 		{
			GetAllChildrenUsingSlate_Internal(Child);
> 		}

});
}

I’m keeping this post for anyone who’d make use of this approach :innocent: