Gamasutra is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Gamasutra: The Art & Business of Making Gamesspacer
View All     RSS
June 23, 2021
arrowPress Releases







If you enjoy reading this site, you might also want to check out these UBM Tech sites:


 

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

by Artur Kh on 06/03/21 12:18:00 pm

2 comments Share on Twitter    RSS

The following blog post, unless otherwise noted, was written by a member of Gamasutra’s community.
The thoughts and opinions expressed are those of the writer and not Gamasutra or its parent company.

 

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:


Related Jobs

Treyarch / Activision
Treyarch / Activision — Santa Monica, California, United States
[06.22.21]

UI Engineer
Health Scholars
Health Scholars — Westminster, Colorado, United States
[06.22.21]

Unity Software Engineer
SideFX
SideFX — Toronto, Ontario, Canada
[06.22.21]

3D Software Developer: Game Tools and Pipeline
Genvid Technologies
Genvid Technologies — CA/WA/NY, California, United States
[06.22.21]

Team Lead (Partner Services)





Loading Comments

loader image