#include "UI/DescentHUD.h" #include "Engine/GameViewportClient.h" #include "Engine/LocalPlayer.h" #include "SceneView.h" #include "UnrealClient.h" #include "Core/Tuning/DescentTune_UI.h" #include "UI/NeuIcons.h" #include "UI/NeuWidgets.h" #include "Camera/PlayerCameraManager.h" #include "Engine/World.h" #include "GameFramework/PlayerController.h" #include "Kismet/GameplayStatics.h" namespace UiTune = Descent::Tune::UI; namespace { FLinearColor HudToastColor(EDescentHudToastKind Kind) { switch (Kind) { case EDescentHudToastKind::Good: return Neu::Green(); case EDescentHudToastKind::Warn: return Neu::Amber(); case EDescentHudToastKind::Bad: return Neu::Red(); default: return Neu::Cyan(); } } /** Compass: an inset strip showing 110 degrees around the heading, ticks every 10, numbers every 30, cardinal letters. */ void HudCompass(FNeuPainter& P, FVector2f Pos, FVector2f Size, float HeadingDeg) { P.Inset(Pos, Size, Size.Y * 0.5f, true); const float Span = 110.f; const float Ppd = (Size.X - 24.f) / Span; const float Cx = Pos.X + Size.X * 0.5f; const float Heading = FMath::Fmod(FMath::Fmod(HeadingDeg, 360.f) + 360.f, 360.f); const int32 First = FMath::FloorToInt((Heading - Span * 0.5f) / 10.f) * 10; const int32 Last = FMath::CeilToInt((Heading + Span * 0.5f) / 10.f) * 10; for (int32 D = First; D <= Last; D += 10) { const float X = Cx + (D - Heading) * Ppd; if (X < Pos.X + 12.f || X > Pos.X + Size.X - 12.f) { continue; } const int32 N = ((D % 360) + 360) % 360; const bool bMajor = (N % 30) == 0; const bool bCardinal = (N % 90) == 0; const float TickH = bMajor ? 6.f : 3.f; P.Line(FVector2f(X, Pos.Y + Size.Y - 3.f), FVector2f(X, Pos.Y + Size.Y - 3.f - TickH), 1.2f, Neu::WithAlpha(Neu::Dim(), bMajor ? 0.9f : 0.5f)); if (bMajor) { const TCHAR* Letter = N == 0 ? TEXT("N") : (N == 90 ? TEXT("E") : (N == 180 ? TEXT("S") : (N == 270 ? TEXT("W") : nullptr))); const FString Label = Letter ? FString(Letter) : FString::FromInt(N); const float Px = bCardinal ? 12.f : 9.f; P.TextBox(Label, FVector2f(X - 14.f, Pos.Y + 1.f), FVector2f(28.f, Size.Y - 8.f), ENeuFont::Bold, Px, bCardinal ? Neu::Txt() : Neu::Dim(), ENeuAlign::Center, ENeuVAlign::Center); } } // The caret that marks the heading. P.Polygon({ FVector2f(Cx - 4.f, Pos.Y + 1.f), FVector2f(Cx + 4.f, Pos.Y + 1.f), FVector2f(Cx, Pos.Y + 6.f) }, Neu::Cyan()); } FLinearColor HudLevelColor(float Pct, float RedBelow, const FLinearColor& Normal) { return Pct < RedBelow ? Neu::Red() : Normal; } } void SDescentHUD::Construct(const FArguments& InArgs) { World = InArgs._World; ModelFn = InArgs._Model; SetVisibility(EVisibility::HitTestInvisible); SetCanTick(false); } int32 SDescentHUD::OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect&, FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const { const FDescentHudModel* M = ModelFn ? ModelFn() : nullptr; if (!M) { return LayerId; } FNeuPainter P(OutDrawElements, AllottedGeometry, LayerId, InWidgetStyle.GetColorAndOpacityTint().A); const FDescentHudLayout L = FDescentHudLayout::Compute(AllottedGeometry.GetLocalSize(), *M); PaintModel(P, *M, L, Args.GetCurrentTime(), World.Get()); return P.Layer + 1; } void SDescentHUD::PaintModel(FNeuPainter& P, const FDescentHudModel& M, const FDescentHudLayout& L, double Time, UWorld* World) { const FVector2f Size = L.Size; const bool bNarrow = L.bNarrow; const float Pulse = 0.65f + 0.35f * FMath::Sin(static_cast(Time) * 6.f); // ---------------- top centre: depth, band, rating, compass ---------------- if (L.Depth.bVisible) { const FVector2f Pos(L.Depth.X, L.Depth.Y); const FVector2f S(L.Depth.W, L.Depth.H); P.Extruded(Pos, S, 16.f); const float H = S.Y; const float DepthPx = bNarrow ? 30.f : (L.bCompact ? 34.f : 44.f); const float Top = H * 0.04f; P.TextBox(M.DepthText.IsEmpty() ? TEXT("--- m") : M.DepthText, Pos + FVector2f(10.f, Top), FVector2f(S.X - 20.f, DepthPx * 1.2f), ENeuFont::Bold, DepthPx, M.Warnings.bCrush ? Neu::Red() : Neu::Txt(), ENeuAlign::Center, ENeuVAlign::Center, DepthPx * 0.6f); float Y = Top + DepthPx * 1.2f; const float LinePx = bNarrow ? 11.f : 13.f; if (!M.Band.IsEmpty()) { P.TextBox(M.Band, Pos + FVector2f(10.f, Y), FVector2f(S.X - 20.f, LinePx * 1.3f), ENeuFont::Bold, LinePx, Neu::Cyan(), ENeuAlign::Center, ENeuVAlign::Center, 9.f); Y += LinePx * 1.3f; } if (!M.RatingText.IsEmpty()) { P.TextBox(M.RatingText, Pos + FVector2f(10.f, Y), FVector2f(S.X - 20.f, LinePx * 1.3f), ENeuFont::Regular, LinePx - 1.f, M.Warnings.bCrush ? Neu::Red() : Neu::Dim(), ENeuAlign::Center, ENeuVAlign::Center, 9.f); } const float StripH = bNarrow ? 18.f : 22.f; HudCompass(P, Pos + FVector2f(14.f, H - StripH - 8.f), FVector2f(S.X - 28.f, StripH), M.CompassHeadingDeg); } // ---------------- top left: objective ---------------- if (L.Objective.bVisible) { const FVector2f Pos(L.Objective.X, L.Objective.Y); const FVector2f S(L.Objective.W, L.Objective.H); P.Extruded(Pos, S, 14.f, true); if (M.bObjectiveFolded) { P.TextBox(TEXT("OBJ"), Pos + FVector2f(12.f, 0.f), FVector2f(34.f, S.Y), ENeuFont::Bold, 11.f, Neu::Cyan(), ENeuAlign::Left, ENeuVAlign::Center); P.TextBox(M.Objective, Pos + FVector2f(48.f, 0.f), FVector2f(S.X - 88.f, S.Y), ENeuFont::Regular, 13.f, Neu::Txt(), ENeuAlign::Left, ENeuVAlign::Center); Neu::PaintChip(P, Pos + FVector2f(S.X - 34.f, (S.Y - 18.f) * 0.5f), 18.f, TEXT("J"), Neu::Dim()); } else { P.TextBox(TEXT("OBJECTIVE"), Pos + FVector2f(14.f, 6.f), FVector2f(S.X - 60.f, 14.f), ENeuFont::Bold, 11.f, Neu::Cyan(), ENeuAlign::Left, ENeuVAlign::Center); Neu::PaintChip(P, Pos + FVector2f(S.X - 34.f, 5.f), 18.f, TEXT("J"), Neu::Dim()); TArray Lines; Neu::WrapText(M.Objective, ENeuFont::Regular, 13.f, S.X - 28.f, 3, Lines); for (int32 I = 0; I < Lines.Num(); ++I) { P.TextBox(Lines[I], Pos + FVector2f(14.f, 24.f + I * 17.f), FVector2f(S.X - 28.f, 17.f), ENeuFont::Regular, 13.f, Neu::Txt(), ENeuAlign::Left, ENeuVAlign::Center); } } } // ---------------- top right: toasts ---------------- if (L.Toasts.bVisible) { const int32 N = FMath::Min(M.Toasts.Num(), FDescentHudModel::MaxToasts); const float Mg = UiTune::HudMarginPx; for (int32 I = 0; I < N; ++I) { const FDescentHudToast& T = M.Toasts[M.Toasts.Num() - N + I]; const float Fade = FMath::Clamp(T.SecondsLeft / UiTune::HudToastFadeSec, 0.f, 1.f); const float OldAlpha = P.Alpha; P.Alpha *= Fade; const FVector2f Pos(L.Toasts.X, L.Toasts.Y + I * (UiTune::HudToastHeightPx + UiTune::HudToastGapPx)); const FVector2f S(L.Toasts.W, UiTune::HudToastHeightPx); P.Extruded(Pos, S, 12.f, true); P.RoundRect(Pos + FVector2f(8.f, 7.f), FVector2f(4.f, S.Y - 14.f), 2.f, HudToastColor(T.Kind)); P.TextBox(T.Text, Pos + FVector2f(20.f, 0.f), FVector2f(S.X - 30.f, S.Y), ENeuFont::Bold, bNarrow ? 12.f : 14.f, Neu::Txt(), ENeuAlign::Left, ENeuVAlign::Center, 10.f); P.Alpha = OldAlpha; } (void)Mg; } // ---------------- bottom left: vitals ---------------- if (L.Vitals.bVisible) { const FVector2f Pos(L.Vitals.X, L.Vitals.Y); const FVector2f S(L.Vitals.W, L.Vitals.H); P.Extruded(Pos, S, 16.f); const float G = L.Gauge; const float Pad = 12.f; Neu::FGaugeStyle St; St.Diameter = G; St.LabelTextPx = bNarrow ? 10.f : 11.f; int32 Index = 0; auto Next = [&]() { return Pos + FVector2f(Pad + Index++ * (G + 10.f) + G * 0.5f, Pad + G * 0.5f); }; const float Health = M.Vitals.Health; Neu::PaintGauge(P, Next(), St, Health / 100.f, FString::FromInt(FMath::RoundToInt(Health)), TEXT("HEALTH"), HudLevelColor(Health, 25.f, Neu::Cyan())); if (M.Vitals.bShowO2) { const float O2 = M.Vitals.O2MaxSeconds > 0.f ? M.Vitals.O2Seconds / M.Vitals.O2MaxSeconds : 0.f; Neu::PaintGauge(P, Next(), St, O2, FString::Printf(TEXT("%ds"), FMath::RoundToInt(M.Vitals.O2Seconds)), TEXT("O2"), (M.Warnings.bLowO2 || O2 < 0.25f) ? Neu::Red() : Neu::Cyan()); } const float Food = M.Vitals.Nutrition; Neu::PaintGauge(P, Next(), St, Food / 100.f, FString::FromInt(FMath::RoundToInt(Food)), TEXT("FOOD"), Food < 10.f ? Neu::Red() : (Food < 25.f ? Neu::Amber() : Neu::Green())); // Info line: the biomass reserve on the left, the temperature on the right. const float InfoY = Pad + G + 22.f; if (M.Vitals.bShowBio) { const float Fu = M.Vitals.BioReserveFu; const FLinearColor BioColor = Fu <= 0.01f ? Neu::Red() : (Fu < 2.f ? Neu::Amber() : Neu::Green()); P.TextBox(FString::Printf(TEXT("BIO %.1f"), Fu), Pos + FVector2f(Pad + 2.f, InfoY), FVector2f(S.X * 0.4f, 18.f), ENeuFont::Bold, 12.f, BioColor, ENeuAlign::Left, ENeuVAlign::Center); } if (!M.TemperatureLabel.IsEmpty()) { P.TextBox(M.TemperatureText, Pos + FVector2f(S.X * 0.38f, InfoY), FVector2f(S.X * 0.62f - Pad - 2.f, 18.f), ENeuFont::Bold, 12.f, Neu::Dim(), ENeuAlign::Right, ENeuVAlign::Center, 9.f); } } // ---------------- bottom right: vehicle panel ---------------- if (L.Vehicle.bVisible) { const FVector2f Pos(L.Vehicle.X, L.Vehicle.Y); const FVector2f S(L.Vehicle.W, L.Vehicle.H); const FDescentHudVehiclePanel& V = M.Vehicle; P.Extruded(Pos, S, 16.f); const float G = bNarrow ? 52.f : 66.f; Neu::FGaugeStyle St; St.Diameter = G; St.LabelTextPx = 10.f; const float Pad = 12.f; Neu::PaintGauge(P, Pos + FVector2f(Pad + G * 0.5f, Pad + G * 0.5f), St, V.HullPct / 100.f, FString::FromInt(FMath::RoundToInt(V.HullPct)), TEXT("HULL"), HudLevelColor(V.HullPct, UiTune::DashHullRedPct, Neu::Cyan())); Neu::PaintGauge(P, Pos + FVector2f(Pad + G * 1.5f + 8.f, Pad + G * 0.5f), St, V.PowerPct / 100.f, FString::FromInt(FMath::RoundToInt(V.PowerPct)), TEXT("POWER"), HudLevelColor(V.PowerPct, UiTune::DashPowerRedPct, Neu::Amber())); const float Tx = Pad + 2.f * G + 22.f; const float Tw = S.X - Tx - Pad; float Y = Pad - 1.f; const float Row = 18.f; if (!V.Title.IsEmpty()) { P.TextBox(V.Title, Pos + FVector2f(Tx, Y), FVector2f(Tw, Row), ENeuFont::Bold, 13.f, Neu::Txt(), ENeuAlign::Left, ENeuVAlign::Center, 10.f); Y += Row + 2.f; } const bool bFull = V.HoldCap > 0.f && V.HoldUsed >= V.HoldCap; if (V.HoldCap > 0.f) { P.TextBox(FString::Printf(TEXT("HOLD %d/%d"), FMath::RoundToInt(V.HoldUsed), FMath::RoundToInt(V.HoldCap)), Pos + FVector2f(Tx, Y), FVector2f(Tw, Row), ENeuFont::Bold, 12.f, bFull ? Neu::Red() : Neu::Dim(), ENeuAlign::Left, ENeuVAlign::Center, 9.f); Y += Row; } P.TextBox(FString::Printf(TEXT("SPD %.1f m/s"), V.SpeedMps), Pos + FVector2f(Tx, Y), FVector2f(Tw, Row), ENeuFont::Bold, 12.f, Neu::Dim(), ENeuAlign::Left, ENeuVAlign::Center, 9.f); Y += Row; if (V.ThrottlePct != 0.f) { P.TextBox(FString::Printf(TEXT("THR %d%%"), FMath::RoundToInt(V.ThrottlePct)), Pos + FVector2f(Tx, Y), FVector2f(Tw, Row), ENeuFont::Bold, 12.f, Neu::Dim(), ENeuAlign::Left, ENeuVAlign::Center, 9.f); } // A throttle bar along the bottom edge of the panel. Neu::PaintTrack(P, Pos + FVector2f(Pad, S.Y - Pad - 12.f - 14.f + (bNarrow ? 8.f : 6.f)), FVector2f(S.X - 2.f * Pad, 10.f), FMath::Abs(V.ThrottlePct) / 100.f, Neu::Cyan()); } // ---------------- bottom centre: hotbar, radio ---------------- if (L.Hotbar.bVisible) { const FVector2f Pos(L.Hotbar.X, L.Hotbar.Y); P.Extruded(Pos, FVector2f(L.Hotbar.W, L.Hotbar.H), 16.f, true); const float Slot = L.Slot; for (int32 I = 0; I < 5; ++I) { const FDescentHudHotbarSlot Empty; const FDescentHudHotbarSlot& HS = M.Hotbar.IsValidIndex(I) ? M.Hotbar[I] : Empty; Neu::PaintSlot(P, Pos + FVector2f(10.f + I * (Slot + 10.f), 10.f), Slot, HS.ItemId, HS.Count, I == M.HotbarSelected, FString::FromInt(I + 1)); } } if (L.Radio.bVisible) { const float Fade = M.RadioSecondsLeft > 0.f ? FMath::Clamp(M.RadioSecondsLeft, 0.f, 1.f) : 1.f; const float OldAlpha = P.Alpha; P.Alpha *= Fade; const FVector2f Pos(L.Radio.X, L.Radio.Y); const FVector2f S(L.Radio.W, L.Radio.H); P.Inset(Pos, S, 16.f, true); P.TextBox(TEXT("RADIO"), Pos + FVector2f(14.f, 0.f), FVector2f(48.f, S.Y), ENeuFont::Bold, 11.f, Neu::Cyan(), ENeuAlign::Left, ENeuVAlign::Center); P.TextBox(M.Radio, Pos + FVector2f(66.f, 0.f), FVector2f(S.X - 80.f, S.Y), ENeuFont::Regular, bNarrow ? 13.f : 15.f, Neu::Txt(), ENeuAlign::Left, ENeuVAlign::Center, 10.f); P.Alpha = OldAlpha; } // ---------------- warning banner ---------------- if (L.Warning.bVisible) { const FVector2f Pos(L.Warning.X, L.Warning.Y); const FVector2f S(L.Warning.W, L.Warning.H); P.Inset(Pos, S, 15.f, true); P.RoundRectOutline(Pos + FVector2f(1.f, 1.f), S - FVector2f(2.f, 2.f), 14.f, 1.5f, Neu::WithAlpha(Neu::Red(), 0.8f * Pulse)); P.TextBox(M.Warnings.Text, Pos + FVector2f(12.f, 0.f), FVector2f(S.X - 24.f, S.Y), ENeuFont::Bold, 14.f, Neu::WithAlpha(Neu::Red(), 0.6f + 0.4f * Pulse), ENeuAlign::Center, ENeuVAlign::Center, 10.f); } // ---------------- centre: crosshair, prompt, build reason, helm reticle ---------------- const FVector2f Centre = Size * 0.5f; if (M.Mode != TEXT("Title")) { if (M.bCrosshair) { P.Disc(Centre, 2.f, Neu::WithAlpha(Neu::Txt(), 0.85f)); P.Arc(Centre, 8.f, 0.f, 360.f, 1.2f, Neu::WithAlpha(Neu::Txt(), 0.35f), false); } if (M.bHelmReticle) { const float Reach = FMath::Min(Size.X, Size.Y) * 0.34f; const FVector2f Aim = Centre + FVector2f(M.HelmAimNormalized.X, M.HelmAimNormalized.Y) * Reach; P.Arc(Aim, 13.f, 0.f, 360.f, 2.f, Neu::Cyan(), false); P.Line(Aim + FVector2f(-22.f, 0.f), Aim + FVector2f(-15.f, 0.f), 2.f, Neu::Cyan()); P.Line(Aim + FVector2f(22.f, 0.f), Aim + FVector2f(15.f, 0.f), 2.f, Neu::Cyan()); P.Line(Aim + FVector2f(0.f, -22.f), Aim + FVector2f(0.f, -15.f), 2.f, Neu::Cyan()); P.Line(Aim + FVector2f(0.f, 22.f), Aim + FVector2f(0.f, 15.f), 2.f, Neu::Cyan()); // The hull-nose marker: where the bow points. P.Polygon({ Centre + FVector2f(0.f, -7.f), Centre + FVector2f(7.f, 0.f), Centre + FVector2f(0.f, 7.f), Centre + FVector2f(-7.f, 0.f) }, Neu::WithAlpha(Neu::Amber(), 0.9f)); } } if (L.Prompt.bVisible) { const FVector2f Pos(L.Prompt.X, L.Prompt.Y); const FVector2f S(L.Prompt.W, L.Prompt.H); P.Extruded(Pos, S, 16.f, true); FString Key, Rest = M.Prompt; int32 Split = M.Prompt.Find(TEXT(" ")); if (Split > 0 && Split <= 4) { Key = M.Prompt.Left(Split); Rest = M.Prompt.Mid(Split).TrimStartAndEnd(); } float X = 16.f; if (!Key.IsEmpty()) { X += Neu::PaintChip(P, Pos + FVector2f(X, (S.Y - 22.f) * 0.5f), 22.f, Key, Neu::Cyan(), true) + 10.f; } P.TextBox(Rest, Pos + FVector2f(X, 0.f), FVector2f(S.X - X - 14.f, S.Y), ENeuFont::Bold, 15.f, Neu::Txt(), ENeuAlign::Left, ENeuVAlign::Center, 11.f); } if (L.BuildReason.bVisible) { const FVector2f Pos(L.BuildReason.X, L.BuildReason.Y); const FVector2f S(L.BuildReason.W, L.BuildReason.H); P.Inset(Pos, S, 14.f, true); P.TextBox(M.BuildReason, Pos + FVector2f(12.f, 0.f), FVector2f(S.X - 24.f, S.Y), ENeuFont::Bold, 13.f, Neu::Red(), ENeuAlign::Center, ENeuVAlign::Center, 10.f); } // ---------------- right centre: proximity ---------------- if (L.Proximity.bVisible) { const FVector2f Pos(L.Proximity.X, L.Proximity.Y); const FVector2f S(L.Proximity.W, L.Proximity.H); P.Extruded(Pos, S, 16.f, true); P.TextBox(TEXT("PROXIMITY"), Pos + FVector2f(14.f, 6.f), FVector2f(S.X - 28.f, 18.f), ENeuFont::Bold, 11.f, Neu::Cyan(), ENeuAlign::Left, ENeuVAlign::Center); const float Row = L.bCompact ? 22.f : UiTune::HudProximityRowPx; for (int32 I = 0; I < M.Proximity.Num(); ++I) { const FDescentHudProximityRow& R = M.Proximity[I]; const FLinearColor C = R.bWarning ? Neu::Red() : Neu::Txt(); const float Y = 30.f + I * Row; P.TextBox(R.Label, Pos + FVector2f(14.f, Y), FVector2f(S.X * 0.5f, Row), ENeuFont::Bold, 12.f, R.bWarning ? Neu::Red() : Neu::Dim(), ENeuAlign::Left, ENeuVAlign::Center); P.TextBox(FString::Printf(TEXT("%.1f m"), R.DistanceM), Pos + FVector2f(S.X * 0.45f, Y), FVector2f(S.X * 0.55f - 14.f, Row), ENeuFont::Bold, 12.f, C, ENeuAlign::Right, ENeuVAlign::Center); } } // ---------------- sonar points (M9): projected once per frame with one matrix ---------------- if (World && M.Sonar.Num() > 0) { APlayerController* PC = World->GetFirstPlayerController(); ULocalPlayer* LP = PC ? PC->GetLocalPlayer() : nullptr; FSceneViewProjectionData Proj; if (LP && LP->ViewportClient && LP->ViewportClient->Viewport && LP->GetProjectionData(LP->ViewportClient->Viewport, Proj)) { const float Scale = FMath::Max(0.01f, P.Geo.GetAccumulatedLayoutTransform().GetScale()); const FMatrix ViewProj = Proj.ComputeViewProjectionMatrix(); const FIntRect Rect = Proj.GetConstrainedViewRect(); for (const FDescentHudSonarPoint& Sp : M.Sonar) { FVector2D Screen; if (!FSceneView::ProjectWorldToScreen(Sp.WorldUU, Rect, ViewProj, Screen)) { continue; } const FVector2f At(static_cast(Screen.X) / Scale, static_cast(Screen.Y) / Scale); if (At.X < 0.f || At.Y < 0.f || At.X > Size.X || At.Y > Size.Y) { continue; } P.Line(At - FVector2f(0.5f * Sp.SizePx, 0.f), At + FVector2f(0.5f * Sp.SizePx, 0.f), Sp.SizePx, Sp.Color); } } } // ---------------- world markers (beacons, carrier, Tender) ---------------- if (World && M.Markers.Num() > 0) { APlayerController* PC = World->GetFirstPlayerController(); const float Scale = FMath::Max(0.01f, P.Geo.GetAccumulatedLayoutTransform().GetScale()); if (PC && PC->PlayerCameraManager) { const FVector Cam = PC->PlayerCameraManager->GetCameraLocation(); for (const FDescentHudMarker& Mk : M.Markers) { FVector2D Screen; if (!UGameplayStatics::ProjectWorldToScreen(PC, Mk.WorldUU, Screen, false)) { continue; } const FVector2f At(static_cast(Screen.X) / Scale, static_cast(Screen.Y) / Scale); if (At.X < 8.f || At.Y < 8.f || At.X > Size.X - 8.f || At.Y > Size.Y - 8.f) { continue; } P.Polygon({ At + FVector2f(0.f, -6.f), At + FVector2f(6.f, 0.f), At + FVector2f(0.f, 6.f), At + FVector2f(-6.f, 0.f) }, Mk.Color); FString Label = Mk.Label; if (Mk.bShowDistance) { Label += FString::Printf(TEXT(" %d m"), FMath::RoundToInt(static_cast(FVector::Dist(Cam, Mk.WorldUU)) / 100.f)); } P.TextBox(Label, At + FVector2f(-80.f, 8.f), FVector2f(160.f, 16.f), ENeuFont::Bold, 12.f, Mk.Color, ENeuAlign::Center, ENeuVAlign::Center, 9.f); } } } }