#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, 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 Definition; TWeakObjectPtr MarkerSource; FOnWorldMapClicked OnClicked; FName Layer; /** One brush per layer, built on first use and holding the texture's only hard reference. */ mutable TMap> LayerBrushes; mutable TArray> 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 HoverLocal; };