Files
2026-09-25 17:02:24 +03:00

177 lines
8.0 KiB
C++

#pragma once
#include "CoreMinimal.h"
#include "Styling/SlateBrush.h"
#include "UObject/StrongObjectPtr.h"
#include "Widgets/SLeafWidget.h"
#include "World/WorldMapProjection.h"
class UTexture2D;
class UWorldMapDefinition;
class UWorldMapSubsystem;
/** A place on the ground was clicked, in world centimetres. XY only; the map is a plan and knows no Z. */
DECLARE_DELEGATE_OneParam(FOnWorldMapClicked, FVector2D /* WorldCm */);
/**
* The world map. All of it: the drawing, the panning, the zooming, the markers and the scale bar.
*
* It is a Slate widget rather than a UUserWidget because it has two hosts that have nothing else in common -
* UWorldMapWidget puts it in UMG for the game, and the editor's World Map tab hosts it directly - and a Slate
* widget is the one shape both can take. The editor tab in particular has no UWorld, so anything that needed
* one to exist could not be shared.
*
* It draws a rectangle of a picture, and the whole trick is that the picture and the world are the same
* rectangle (see FWorldMapProjection). There is no scene capture and no render target anywhere near this.
*/
class SALTY_API SWorldMap : public SLeafWidget
{
public:
SLATE_BEGIN_ARGS(SWorldMap)
: _Definition(nullptr)
, _Layer(NAME_None)
, _ShowGrid(true)
, _ShowScaleBar(true)
, _ShowMarkers(true)
, _ShowLocalPlayer(true)
, _AllowInput(true)
{}
/** What to draw. Null draws the empty state and says so rather than drawing nothing. */
SLATE_ARGUMENT(UWorldMapDefinition*, Definition)
/** Which layer; NAME_None takes the definition's default. */
SLATE_ARGUMENT(FName, Layer)
SLATE_ARGUMENT(bool, ShowGrid)
SLATE_ARGUMENT(bool, ShowScaleBar)
SLATE_ARGUMENT(bool, ShowMarkers)
SLATE_ARGUMENT(bool, ShowLocalPlayer)
/** False makes it a picture: no pan, no zoom, no click. For a minimap. */
SLATE_ARGUMENT(bool, AllowInput)
/** Where registered markers and the local player come from. Absent in the editor tab, which has neither. */
SLATE_ARGUMENT(TWeakObjectPtr<UWorldMapSubsystem>, MarkerSource)
SLATE_EVENT(FOnWorldMapClicked, OnClicked)
SLATE_END_ARGS()
void Construct(const FArguments& InArgs);
void SetDefinition(UWorldMapDefinition* InDefinition);
UWorldMapDefinition* GetDefinition() const { return Definition.Get(); }
void SetMarkerSource(UWorldMapSubsystem* InSource);
/** An unknown id is ignored rather than blanking the map, so a stale saved setting is survivable. */
void SetLayer(FName InLayer);
FName GetLayer() const { return Layer; }
void NextLayer();
void SetShowGrid(bool bValue) { bShowGrid = bValue; }
void SetShowScaleBar(bool bValue) { bShowScaleBar = bValue; }
void SetShowMarkers(bool bValue) { bShowMarkers = bValue; }
void SetShowLocalPlayer(bool bValue) { bShowLocalPlayer = bValue; }
void SetAllowInput(bool bValue) { bAllowInput = bValue; }
/** Centre the view on a place, in world centimetres. Does not change the zoom. */
void FocusOnWorld(const FVector2D& WorldCm);
FVector2D GetViewCentreWorldCm() const;
/**
* Keep the view centred on the local player as it moves. Panning by hand turns this off, because a map that
* snaps back the moment you let go of it cannot be read.
*/
void SetFollowLocalPlayer(bool bValue) { bFollowLocalPlayer = bValue; }
bool IsFollowingLocalPlayer() const { return bFollowLocalPlayer; }
/**
* Go back to the player and start following again. The way out of the corner panning puts you in: having
* dropped the follow to look somewhere else, there was otherwise no way back to it but closing the map.
* Does nothing when there is no local body, which is the honest answer rather than jumping to the origin.
*/
bool FocusOnLocalPlayer();
/** Ground metres to one screen pixel. Smaller is closer in. */
void SetMetresPerPixel(double InMetresPerPixel);
double GetMetresPerPixel() const { return MetresPerPixel; }
/** Zoom back out until the whole world fits, and centre it. */
void ZoomToFit();
// SWidget
/** The first fit needs a size, which a widget has only after it has been laid out; following the player
* needs to happen whether or not anything else changed. Both live here rather than in OnPaint, which is
* const and has no business moving the view. */
virtual void Tick(const FGeometry& AllottedGeometry, const double InCurrentTime, const float InDeltaTime) override;
virtual int32 OnPaint(const FPaintArgs& Args, const FGeometry& AllottedGeometry, const FSlateRect& MyCullingRect,
FSlateWindowElementList& OutDrawElements, int32 LayerId, const FWidgetStyle& InWidgetStyle, bool bParentEnabled) const override;
virtual FVector2D ComputeDesiredSize(float LayoutScaleMultiplier) const override;
virtual FReply OnMouseButtonDown(const FGeometry& Geometry, const FPointerEvent& Event) override;
virtual FReply OnMouseButtonUp(const FGeometry& Geometry, const FPointerEvent& Event) override;
virtual FReply OnMouseMove(const FGeometry& Geometry, const FPointerEvent& Event) override;
virtual FReply OnMouseWheel(const FGeometry& Geometry, const FPointerEvent& Event) override;
virtual FReply OnMouseButtonDoubleClick(const FGeometry& Geometry, const FPointerEvent& Event) override;
virtual void OnMouseLeave(const FPointerEvent& Event) override;
virtual FCursorReply OnCursorQuery(const FGeometry& Geometry, const FPointerEvent& Event) const override;
private:
/** The view, in normalised map coordinates, derived from the zoom and the widget's own size. */
struct FView
{
FVector2D Centre = FVector2D(0.5, 0.5);
double SpanU = 1.0;
double SpanV = 1.0;
FVector2D SizePx = FVector2D(1.0, 1.0);
bool bValid = false;
};
FView ComputeView(const FVector2D& LocalSizePx) const;
FVector2D LocalToNormalised(const FView& View, const FVector2D& LocalPx) const;
FVector2D NormalisedToLocal(const FView& View, const FVector2D& Normalised) const;
/** Fold the centre back into what can actually be seen. X wraps on a whole cylinder; Y never does. */
void ClampCentre(const FVector2D& LocalSizePx);
/** Widest the map may be zoomed out at this size: the whole thing, fitted. */
double FitMetresPerPixel(const FVector2D& LocalSizePx) const;
const FSlateBrush* BrushForCurrentLayer() const;
int32 PaintMap(const FGeometry& Geometry, const FView& View, FSlateWindowElementList& Out, int32 LayerId) const;
int32 PaintGrid(const FGeometry& Geometry, const FView& View, FSlateWindowElementList& Out, int32 LayerId) const;
int32 PaintMarkers(const FGeometry& Geometry, const FView& View, FSlateWindowElementList& Out, int32 LayerId) const;
int32 PaintOneMarker(const FGeometry& Geometry, const FView& View, const struct FWorldMapMarker& Marker,
FSlateWindowElementList& Out, int32 LayerId) const;
int32 PaintScaleBar(const FGeometry& Geometry, const FView& View, FSlateWindowElementList& Out, int32 LayerId) const;
int32 PaintReadout(const FGeometry& Geometry, const FView& View, FSlateWindowElementList& Out, int32 LayerId) const;
int32 PaintEmptyState(const FGeometry& Geometry, FSlateWindowElementList& Out, int32 LayerId) const;
const FWorldMapProjection& GetProjection() const;
TStrongObjectPtr<UWorldMapDefinition> Definition;
TWeakObjectPtr<UWorldMapSubsystem> MarkerSource;
FOnWorldMapClicked OnClicked;
FName Layer;
/** One brush per layer, built on first use and holding the texture's only hard reference. */
mutable TMap<FName, TSharedPtr<FSlateBrush>> LayerBrushes;
mutable TArray<TStrongObjectPtr<UTexture2D>> LoadedTextures;
/** Normalised. X may be anywhere on a wrapping map and is folded when it is used. */
FVector2D ViewCentre = FVector2D(0.5, 0.5);
double MetresPerPixel = 0.0; // 0 means "not fitted yet"; the first paint fits it to the widget
mutable bool bNeedsFit = true;
bool bShowGrid = true;
bool bShowScaleBar = true;
bool bShowMarkers = true;
bool bShowLocalPlayer = true;
bool bAllowInput = true;
bool bFollowLocalPlayer = false;
bool bDragging = false;
bool bDraggedFarEnoughToNotBeAClick = false;
FVector2D DragLastLocal = FVector2D::ZeroVector;
FVector2D PressLocal = FVector2D::ZeroVector;
/** Where the pointer is, for the readout. Unset when it is not over the map. */
mutable TOptional<FVector2D> HoverLocal;
};