177 lines
8.0 KiB
C++
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;
|
|
};
|