informa
7 min read
article

UE4Cookery CPP008: Widget Component insides part 1 (Screen space)

Topic: Widget Component insides part 1 (Screen space) Source for UE4.26: https://github.com/klauth86/UE4Cookery/tree/main/CPP008

It is rather common task to render UI elements for some actors and objects in 3D scene. It can be Hit points bar, replicas, tips and so on. For that type of problems we can find UWidgetComponent class just out of box. This class encapsulates two different approaches to render UI elements - it can use Sceen space and World space. Because of that code contains doubled logic and can confuse a little bit. To make it cleaner for understanding we can check each of two logic branches separately. In this part we'll check Screen space logic out, cutting all World space things off (In some moments classes from this article differ from those used by UWidgetComponent, but this otherness is completely in trifles, so most logical parts are identical).

 

Well, first thing to ask will be something like "What classes is Screen space logic based on?". By searching through engine code we will obtain special Slate widget class, that is ticking and updating all child widgets positions with owning Player Controller by calculating Player Projection transform:

 

SWorldWidgetsLayer.h

#pragma once

#include "Widgets/SCompoundWidget.h"
#include "Widgets/Layout/SConstraintCanvas.h"
#include "Layout/Visibility.h"
#include "UObject/ObjectKey.h"

class UObject;
class APlayerController;
class AActor;

class CPP008_API FComponentEntry {

public:
	FComponentEntry() :Slot(nullptr) { bRemoving = false; bDrawAtDesiredSize = false; }
	~FComponentEntry() {
		Widget.Reset();
		ContainerWidget.Reset();
	}

public:

	TWeakObjectPtr<UObject> OwningObject;

	TSharedPtr<SWidget> ContainerWidget;
	TSharedPtr<SWidget> Widget;
	SConstraintCanvas::FSlot* Slot;
	FIntPoint DrawSize;
	FVector2D Pivot;
	FVector WorldLocation;
	TWeakObjectPtr<AActor> Actor;

	uint8 bRemoving : 1;
	uint8 bDrawAtDesiredSize : 1;
};

class CPP008_API SWorldWidgetsLayer : public SCompoundWidget {

	SLATE_BEGIN_ARGS(SWorldWidgetsLayer) {
		_Visibility = EVisibility::SelfHitTestInvisible;
	}
	SLATE_END_ARGS()

public:

	void Construct(const FArguments& InArgs, APlayerController* playerController);

	virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override;

	virtual FVector2D ComputeDesiredSize(float) const override { return FVector2D::ZeroVector; }

	void AddComponent(UObject* owningObject, const FComponentEntry& entry);

	void RemoveComponent(UObject* owningObject);

protected:

	void RemoveEntryFromCanvas(FComponentEntry& Entry);

private:

	TWeakObjectPtr<APlayerController> PlayerControllerPtr;

	TMap<FObjectKey, FComponentEntry> ComponentMap;

	TSharedPtr<SConstraintCanvas> Canvas;
};

 

SWorldWidgetsLayer.cpp

#include "SWorldWidgetsLayer.h"
#include "GameFramework/PlayerController.h"
#include "GameFramework/Actor.h"
#include "Engine/LocalPlayer.h"
#include "Engine/World.h"
#include "Engine/GameViewportClient.h"
#include "Blueprint/SlateBlueprintLibrary.h"
#include "Slate/SGameLayerManager.h"
#include "SceneView.h"

void SWorldWidgetsLayer::Construct(const FArguments& InArgs, APlayerController* playerController) {
	
	PlayerControllerPtr = playerController;

	bCanSupportFocus = false;

	ChildSlot[SAssignNew(Canvas, SConstraintCanvas)];
}

void SWorldWidgetsLayer::AddComponent(UObject* owningObject, const FComponentEntry& entry) {
	if (owningObject) {
		FComponentEntry& Entry = ComponentMap.FindOrAdd(FObjectKey(owningObject));
		Entry.OwningObject = entry.OwningObject;
		Entry.Widget = entry.Widget;
		Entry.DrawSize = entry.DrawSize;
		Entry.Pivot = entry.Pivot;
		Entry.bDrawAtDesiredSize = entry.bDrawAtDesiredSize;
		Entry.WorldLocation = entry.WorldLocation;
		Entry.Actor = entry.Actor;
		Canvas->AddSlot().Expose(Entry.Slot)[SAssignNew(Entry.ContainerWidget, SBox)[Entry.Widget.ToSharedRef()]];
	}
}

void SWorldWidgetsLayer::RemoveComponent(UObject* owningObject) {
	if (ensure(owningObject)) {
		if (FComponentEntry* EntryPtr = ComponentMap.Find(owningObject)) {
			if (!EntryPtr->bRemoving) {
				RemoveEntryFromCanvas(*EntryPtr);
				ComponentMap.Remove(owningObject);
			}
		}
	}
}

void SWorldWidgetsLayer::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) {
	if (auto PlayerController = PlayerControllerPtr.Get()) {
		
		if (UGameViewportClient* ViewportClient = PlayerController->GetWorld()->GetGameViewport()) {
			const FGeometry& ViewportGeometry = ViewportClient->GetGameLayerManager()->GetViewportWidgetHostGeometry();

			// cache projection data here and avoid calls to UWidgetLayoutLibrary.ProjectWorldLocationToWidgetPositionWithDistance
			FSceneViewProjectionData ProjectionData;
			FMatrix ViewProjectionMatrix;
			bool bHasProjectionData = false;

			ULocalPlayer const* const LP = PlayerController->GetLocalPlayer();
			if (LP && LP->ViewportClient) {
				bHasProjectionData = LP->GetProjectionData(ViewportClient->Viewport, eSSP_FULL, /*out*/ ProjectionData);
				if (bHasProjectionData) {
					ViewProjectionMatrix = ProjectionData.ComputeViewProjectionMatrix();
				}
			}

			for (auto It = ComponentMap.CreateIterator(); It; ++It) {
				FComponentEntry& Entry = It.Value();

				if (auto owningObject = Entry.OwningObject.Get()) {

					auto worldLocation = Entry.Actor.IsValid() ? Entry.Actor->GetActorLocation() + Entry.WorldLocation : Entry.WorldLocation;

					FVector2D ScreenPosition2D;
					const bool bProjected = bHasProjectionData ? FSceneView::ProjectWorldToScreen(worldLocation, ProjectionData.GetConstrainedViewRect(), ViewProjectionMatrix, ScreenPosition2D) : false;
					if (bProjected) {
						const float ViewportDist = FVector::Dist(ProjectionData.ViewOrigin, worldLocation);
						const FVector2D RoundedPosition2D(FMath::RoundToInt(ScreenPosition2D.X), FMath::RoundToInt(ScreenPosition2D.Y));
						FVector2D ViewportPosition2D;
						USlateBlueprintLibrary::ScreenToViewport(PlayerController, RoundedPosition2D, ViewportPosition2D);

						const FVector ViewportPosition(ViewportPosition2D.X, ViewportPosition2D.Y, ViewportDist);

						Entry.ContainerWidget->SetVisibility(EVisibility::SelfHitTestInvisible);

						if (SConstraintCanvas::FSlot* CanvasSlot = Entry.Slot) {
							FVector2D AbsoluteProjectedLocation = ViewportGeometry.LocalToAbsolute(FVector2D(ViewportPosition.X, ViewportPosition.Y));
							FVector2D LocalPosition = AllottedGeometry.AbsoluteToLocal(AbsoluteProjectedLocation);

							FIntPoint& drawSize = Entry.DrawSize;

							CanvasSlot->AutoSize(drawSize.SizeSquared() == 0 || Entry.bDrawAtDesiredSize);
							CanvasSlot->Offset(FMargin(LocalPosition.X, LocalPosition.Y, drawSize.X, drawSize.Y));
							CanvasSlot->Anchors(FAnchors(0, 0, 0, 0));
							CanvasSlot->Alignment(Entry.Pivot);

							CanvasSlot->ZOrder(-ViewportPosition.Z);
						}
					}
					else {
						Entry.ContainerWidget->SetVisibility(EVisibility::Collapsed);
					}
				}
				else {
					RemoveEntryFromCanvas(Entry);
					It.RemoveCurrent();
					continue;
				}
			}

			// Done
			return;
		}
	}

	if (GSlateIsOnFastUpdatePath) {
		// Hide everything if we are unable to do any of the work.
		for (auto It = ComponentMap.CreateIterator(); It; ++It) {
			FComponentEntry& Entry = It.Value();
			Entry.ContainerWidget->SetVisibility(EVisibility::Collapsed);
		}
	}
}

void SWorldWidgetsLayer::RemoveEntryFromCanvas(FComponentEntry& Entry) {
	Entry.bRemoving = true;

	if (TSharedPtr<SWidget> ContainerWidget = Entry.ContainerWidget) {
		Canvas->RemoveSlot(ContainerWidget.ToSharedRef());
	}
}

 

This widget is a special container. It is added to Player Screen space by special manager class, manipulating Player Layers and Widgets with SGameLayerManager:

 

WorldWidgetsManager.h

#pragma once

#include "CoreTypes.h"
#include "Templates/SharedPointer.h"
#include "UObject/WeakObjectPtrTemplates.h"

class UWorld;
class UObject;
class SWidget;
class AActor;

class WorldWidgetsManager {

public:

	static void AddWorldWidget(UWorld* world, int32 playerControllerIndex, const FName& layerName, const int32 layerZOrder, UObject* owningObject, TSharedPtr<SWidget> widget, const FIntPoint& drawSize, const FVector2D& pivot, bool bDrawAtDesiredSize, const FVector& worldLocation, AActor* actor = nullptr);

	static void RemoveWorldWidget(UWorld* world, int32 playerControllerIndex, FName& layerName, UObject* owningObject);

	static bool HasWorldWidget(UWorld* world, int32 playerControllerIndex, FName& layerName, UObject* owningObject);

	static void Reset(UWorld* world, int32 playerControllerIndex);
};

 

WorldWidgetsManager.cpp

#include "WorldWidgetsManager.h"
#include "SWorldWidgetsLayer.h"
#include "Engine/World.h"
#include "Engine/GameViewportClient.h"
#include "Slate/SGameLayerManager.h"
#include "Kismet/GameplayStatics.h"

class FWorldWidgetsLayer : public IGameLayer {

public:

	FWorldWidgetsLayer(APlayerController* playerController) { PlayerController = playerController; }

	void AddComponent(UObject* owningObject, TSharedRef<SWidget> Widget, const FIntPoint& drawSize, const FVector2D& pivot, const bool& drawAtDesiredSize, const FVector& worldLocation, AActor* actor) {
		if (Components.Contains(owningObject)) return;
		
		Components.Emplace(owningObject);
		auto& Entry = Components[owningObject];

		Entry.OwningObject = owningObject;
		Entry.Widget = Widget;
		Entry.DrawSize = drawSize;
		Entry.Pivot = pivot;
		Entry.bDrawAtDesiredSize = drawAtDesiredSize;
		Entry.WorldLocation = worldLocation;
		Entry.Actor = actor;

		if (TSharedPtr<SWorldWidgetsLayer> ScreenLayer = ScreenLayerPtr.Pin()) ScreenLayer->AddComponent(owningObject, Entry);

		UE_LOG(LogTemp, Warning, TEXT("!!! AddComponent owningObject[%d] FWorldWidgetsLayer[%d] ScreenLayerPtr[%d]"), owningObject, this, ScreenLayerPtr.Pin().Get());
	}

	void RemoveComponent(UObject* owningObject) {
		if (!Components.Contains(owningObject)) return;

		Components.Remove(owningObject);

		if (TSharedPtr<SWorldWidgetsLayer> ScreenLayer = ScreenLayerPtr.Pin()) ScreenLayer->RemoveComponent(owningObject);

		UE_LOG(LogTemp, Warning, TEXT("!!! RemComponent owningObject[%d] FWorldWidgetsLayer[%d] ScreenLayerPtr[%d]"), owningObject, this, ScreenLayerPtr.Pin().Get());
	}

	bool HasComponent(UObject* owningObject) { return Components.Contains(owningObject); }

	virtual TSharedRef<SWidget> AsWidget() override {
		
		if (TSharedPtr<SWorldWidgetsLayer> ScreenLayer = ScreenLayerPtr.Pin()) return ScreenLayer.ToSharedRef();

		TSharedRef<SWorldWidgetsLayer> NewScreenLayer = SNew(SWorldWidgetsLayer, PlayerController.Get());
		ScreenLayerPtr = NewScreenLayer;

		for (auto componentItem : Components) {

			if (auto component = componentItem.Key.Get()) {
				auto& Entry = componentItem.Value;
				
				if (TSharedPtr<SWorldWidgetsLayer> ScreenLayer = ScreenLayerPtr.Pin()) ScreenLayer->AddComponent(component, Entry);
			}
		}

		return NewScreenLayer;
	}

private:
	TWeakObjectPtr<APlayerController> PlayerController;
	TWeakPtr<SWorldWidgetsLayer> ScreenLayerPtr;
	TMap<TWeakObjectPtr<UObject>, FComponentEntry> Components;
};

void WorldWidgetsManager::AddWorldWidget(UWorld* world, int32 playerControllerIndex, const FName& layerName, const int32 layerZOrder, UObject* owningObject, TSharedPtr<SWidget> widget, const FIntPoint& drawSize, const FVector2D& pivot, bool bDrawAtDesiredSize, const FVector& worldLocation, AActor* actor) {
	
	if (!owningObject) return;

	if (!widget.IsValid()) return;

	if (!world || !world->IsGameWorld()) return;

	auto playerController = UGameplayStatics::GetPlayerController(world, playerControllerIndex);
	auto localPlayer = playerController ? playerController->GetLocalPlayer() : nullptr;

	if (!playerController || !localPlayer) return;

	if (auto ViewportClient = world->GetGameViewport()) {
		auto LayerManager = ViewportClient->GetGameLayerManager();
		if (LayerManager.IsValid()) {
			
			TSharedPtr<FWorldWidgetsLayer> ScreenLayer;
			auto Layer = LayerManager->FindLayerForPlayer(localPlayer, layerName);
			
			if (!Layer.IsValid()) {
				TSharedRef<FWorldWidgetsLayer> NewScreenLayer = MakeShareable(new FWorldWidgetsLayer(playerController));
				LayerManager->AddLayerForPlayer(localPlayer, layerName, NewScreenLayer, layerZOrder);
				ScreenLayer = NewScreenLayer;
			}
			else {
				ScreenLayer = StaticCastSharedPtr<FWorldWidgetsLayer>(Layer);
			}

			ScreenLayer->AddComponent(owningObject, widget.ToSharedRef(), drawSize, pivot, bDrawAtDesiredSize, worldLocation, actor);
		}
	}
}

void WorldWidgetsManager::RemoveWorldWidget(UWorld* world, int32 playerControllerIndex, FName& layerName, UObject* owningObject) {

	if (!owningObject) return;

	if (!world || !world->IsGameWorld()) return;

	auto playerController = UGameplayStatics::GetPlayerController(world, playerControllerIndex);
	auto localPlayer = playerController ? playerController->GetLocalPlayer() : nullptr;

	if (!playerController || !localPlayer) return;

	if (auto ViewportClient = world->GetGameViewport()) {
		TSharedPtr<IGameLayerManager> LayerManager = ViewportClient->GetGameLayerManager();
		if (LayerManager.IsValid()) {
			
			TSharedPtr<IGameLayer> Layer = LayerManager->FindLayerForPlayer(localPlayer, layerName);
			if (Layer.IsValid()) {
				TSharedPtr<FWorldWidgetsLayer> ScreenLayer = StaticCastSharedPtr<FWorldWidgetsLayer>(Layer);
				ScreenLayer->RemoveComponent(owningObject);
			}
		}
	}
}

bool WorldWidgetsManager::HasWorldWidget(UWorld* world, int32 playerControllerIndex, FName& layerName, UObject* owningObject) {
	if (!owningObject) return false;

	if (!world || !world->IsGameWorld()) return false;

	auto playerController = UGameplayStatics::GetPlayerController(world, playerControllerIndex);
	auto localPlayer = playerController ? playerController->GetLocalPlayer() : nullptr;

	if (!playerController || !localPlayer) return false;

	if (auto ViewportClient = world->GetGameViewport()) {
		TSharedPtr<IGameLayerManager> LayerManager = ViewportClient->GetGameLayerManager();
		if (LayerManager.IsValid()) {

			TSharedPtr<IGameLayer> Layer = LayerManager->FindLayerForPlayer(localPlayer, layerName);
			if (Layer.IsValid()) {
				TSharedPtr<FWorldWidgetsLayer> ScreenLayer = StaticCastSharedPtr<FWorldWidgetsLayer>(Layer);
				return ScreenLayer->HasComponent(owningObject);
			}
		}
	}

	return false;
}

void WorldWidgetsManager::Reset(UWorld* world, int32 playerControllerIndex) {

	if (!world || !world->IsGameWorld()) return;

	auto playerController = UGameplayStatics::GetPlayerController(world, playerControllerIndex);
	auto localPlayer = playerController ? playerController->GetLocalPlayer() : nullptr;

	if (!playerController || !localPlayer) return;

	if (auto ViewportClient = world->GetGameViewport()) {
		
		auto LayerManager = ViewportClient->GetGameLayerManager();
		if (LayerManager.IsValid()) LayerManager->ClearWidgets();
	}
}

 

So, what can we say about UWidgetComponent Screen space logic now?

  • Every UWidgetComponent in Screen space mode creates Player Layer for Local Player, named and ZOrdered according to component properties
  • Every Player Layer from previous step creates special Slate widget, that is used as container
  • Both component and Slate widget are ticking

 

Now, we can make some test widgets and test actors and give a try to this piece of implementation:

Latest Jobs

Disbelief

Chicago, Illinois
05.10.22
Producer

Build a Rocket Boy Games

Edinburgh, Scotland
05.12.22
Lead Animation Programmer

Windwalk Games

Austin, Texas
05.16.22
Game Designer

Sucker Punch Productions

Bellevue, Washington
05.10.22
Campaign Director
More Jobs   

CONNECT WITH US

Register for a
Subscribe to
Follow us

Game Developer Account

Game Developer Newsletter

@gamedevdotcom

Register for a

Game Developer Account

Gain full access to resources (events, white paper, webinars, reports, etc)
Single sign-on to all Informa products

Register
Subscribe to

Game Developer Newsletter

Get daily Game Developer top stories every morning straight into your inbox

Subscribe
Follow us

@gamedevdotcom

Follow us @gamedevdotcom to stay up-to-date with the latest news & insider information about events & more