Files
UnrealEngine/Engine/Source/Runtime/UMG/Private/Slate/SWorldWidgetScreenLayer.cpp
2025-05-18 13:04:45 +08:00

249 lines
7.9 KiB
C++

// Copyright Epic Games, Inc. All Rights Reserved.
#include "Slate/SWorldWidgetScreenLayer.h"
#include "Widgets/Layout/SBox.h"
#include "Components/WidgetComponent.h"
#include "Blueprint/WidgetLayoutLibrary.h"
#include "Blueprint/SlateBlueprintLibrary.h"
#include "Engine/GameViewportClient.h"
#include "Widgets/SViewport.h"
#include "SceneView.h"
#include "Slate/SGameLayerManager.h"
static int32 GSlateWorldWidgetZOrder = 1;
static FAutoConsoleVariableRef CVarSlateWorldWidgetZOrder(
TEXT("Slate.WorldWidgetZOrder"),
GSlateWorldWidgetZOrder,
TEXT("Whether to re-order world widgets projected to screen by their view point distance\n")
TEXT(" 0: Disable re-ordering\n")
TEXT(" 1: Re-order by distance (default, less batching, less artifacts when widgets overlap)"),
ECVF_Default
);
static bool GSlateWorldWidgetIgnoreNotVisibleWidgets = false;
static FAutoConsoleVariableRef CVarSlateWorldWidgetIgnoreNotVisibleWidgets(
TEXT("Slate.WorldWidgetIgnoreNotVisibleWidgets"),
GSlateWorldWidgetIgnoreNotVisibleWidgets,
TEXT("Whether to not update the position of world widgets if they are not visible - to prevent invalidating the whole layer unnecessarily")
);
SWorldWidgetScreenLayer::FComponentEntry::FComponentEntry()
: Slot(nullptr)
{
}
SWorldWidgetScreenLayer::FComponentEntry::~FComponentEntry()
{
Widget.Reset();
ContainerWidget.Reset();
}
void SWorldWidgetScreenLayer::Construct(const FArguments& InArgs, const FLocalPlayerContext& InPlayerContext)
{
PlayerContext = InPlayerContext;
bCanSupportFocus = false;
DrawSize = FVector2D(0, 0);
Pivot = FVector2D(0.5f, 0.5f);
ChildSlot
[
SAssignNew(Canvas, SConstraintCanvas)
];
}
void SWorldWidgetScreenLayer::SetWidgetDrawSize(FVector2D InDrawSize)
{
DrawSize = InDrawSize;
}
void SWorldWidgetScreenLayer::SetWidgetPivot(FVector2D InPivot)
{
Pivot = InPivot;
}
void SWorldWidgetScreenLayer::AddComponent(USceneComponent* Component, TSharedRef<SWidget> Widget)
{
if ( Component )
{
FComponentEntry& Entry = ComponentMap.FindOrAdd(FObjectKey(Component));
Entry.Component = Component;
Entry.WidgetComponent = Cast<UWidgetComponent>(Component);
Entry.Widget = Widget;
Canvas->AddSlot()
.Expose(Entry.Slot)
[
SAssignNew(Entry.ContainerWidget, SBox)
[
Widget
]
];
}
}
void SWorldWidgetScreenLayer::RemoveComponent(USceneComponent* Component)
{
if (ensure(Component))
{
if (FComponentEntry* EntryPtr = ComponentMap.Find(Component))
{
if (!EntryPtr->bRemoving)
{
RemoveEntryFromCanvas(*EntryPtr);
ComponentMap.Remove(Component);
}
}
}
}
void SWorldWidgetScreenLayer::Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime)
{
QUICK_SCOPE_CYCLE_COUNTER(SWorldWidgetScreenLayer_Tick);
if ( APlayerController* PlayerController = PlayerContext.GetPlayerController() )
{
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, /*out*/ ProjectionData);
if (bHasProjectionData)
{
ViewProjectionMatrix = ProjectionData.ComputeViewProjectionMatrix();
}
}
for ( auto It = ComponentMap.CreateIterator(); It; ++It )
{
FComponentEntry& Entry = It.Value();
if ( USceneComponent* SceneComponent = Entry.Component.Get() )
{
FVector WorldLocation = SceneComponent->GetComponentLocation();
FVector2D ScreenPosition2D;
const bool bProjected = [&Entry, bHasProjectionData, &WorldLocation, &ScreenPosition2D, &ProjectionData, &ViewProjectionMatrix]()
{
if (!bHasProjectionData)
{
return false;
}
if (GSlateWorldWidgetIgnoreNotVisibleWidgets &&
Entry.WidgetComponent &&
!Entry.WidgetComponent->IsWidgetVisible())
{
return false;
}
return FSceneView::ProjectWorldToScreen(WorldLocation, ProjectionData.GetConstrainedViewRect(), ViewProjectionMatrix, ScreenPosition2D);
}();
if (bProjected)
{
const double ViewportDist = FVector::Dist(ProjectionData.ViewOrigin, WorldLocation);
const FVector2D RoundedPosition2D(FMath::RoundToDouble(ScreenPosition2D.X), FMath::RoundToDouble(ScreenPosition2D.Y));
// If the root widget has pixel snapping disabled, then don't pixel snap the screen coordinates either otherwise
// it'll always jump between pixels. This saves needing an explicit flag on the widget component, and is probably
// a better delegation of responsibility anyway, since changing the widget type can change the snapping as it wants
bool bDisablePixelSnapping = Entry.Widget->GetPixelSnapping() == EWidgetPixelSnapping::Disabled;
const FVector2D ScreenPositionToUse = bDisablePixelSnapping ? ScreenPosition2D : RoundedPosition2D;
FVector2D ViewportPosition2D;
USlateBlueprintLibrary::ScreenToViewport(PlayerController, ScreenPositionToUse, OUT 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);
if ( Entry.WidgetComponent )
{
LocalPosition = Entry.WidgetComponent->ModifyProjectedLocalPosition(ViewportGeometry, LocalPosition);
FVector2D ComponentDrawSize = Entry.WidgetComponent->GetDrawSize();
FVector2D ComponentPivot = Entry.WidgetComponent->GetPivot();
CanvasSlot->SetAutoSize(ComponentDrawSize.IsZero() || Entry.WidgetComponent->GetDrawAtDesiredSize());
CanvasSlot->SetOffset(FMargin(LocalPosition.X, LocalPosition.Y, ComponentDrawSize.X, ComponentDrawSize.Y));
CanvasSlot->SetAnchors(FAnchors(0, 0, 0, 0));
CanvasSlot->SetAlignment(ComponentPivot);
if (GSlateWorldWidgetZOrder != 0)
{
CanvasSlot->SetZOrder(static_cast<float>(- ViewportPosition.Z));
}
}
else
{
CanvasSlot->SetAutoSize(DrawSize.IsZero());
CanvasSlot->SetOffset(FMargin(LocalPosition.X, LocalPosition.Y, DrawSize.X, DrawSize.Y));
CanvasSlot->SetAnchors(FAnchors(0, 0, 0, 0));
CanvasSlot->SetAlignment(Pivot);
if (GSlateWorldWidgetZOrder != 0)
{
CanvasSlot->SetZOrder(static_cast<float>( - 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 SWorldWidgetScreenLayer::RemoveEntryFromCanvas(SWorldWidgetScreenLayer::FComponentEntry& Entry)
{
// Mark the component was being removed, so we ignore any other remove requests for this component.
Entry.bRemoving = true;
if (TSharedPtr<SWidget> ContainerWidget = Entry.ContainerWidget)
{
Canvas->RemoveSlot(ContainerWidget.ToSharedRef());
}
}
FVector2D SWorldWidgetScreenLayer::ComputeDesiredSize(float) const
{
return FVector2D(0, 0);
}