|
|
|
|
@@ -0,0 +1,535 @@
|
|
|
|
|
// Fill out your copyright notice in the Description page of Project Settings.
|
|
|
|
|
|
|
|
|
|
#pragma once
|
|
|
|
|
|
|
|
|
|
#include "CoreMinimal.h"
|
|
|
|
|
#include "UI/EleriBaseWidget.h"
|
|
|
|
|
#include "MapWidget.generated.h"
|
|
|
|
|
|
|
|
|
|
class AOrthoMapCaptureActor;
|
|
|
|
|
class UBorder;
|
|
|
|
|
class UCanvasPanel;
|
|
|
|
|
class UImage;
|
|
|
|
|
class UOverlay;
|
|
|
|
|
class UTexture2D;
|
|
|
|
|
class UWidget;
|
|
|
|
|
|
|
|
|
|
/** What a request for a set of assets to be loaded is held by. Only used through a shared pointer here, so the
|
|
|
|
|
* full declaration is left to the file that actually asks for the pieces. */
|
|
|
|
|
struct FStreamableHandle;
|
|
|
|
|
|
|
|
|
|
/** How the widget decides which piece asset belongs in which cell of the map. */
|
|
|
|
|
UENUM(BlueprintType)
|
|
|
|
|
enum class EMapPieceLayout : uint8
|
|
|
|
|
{
|
|
|
|
|
/** Ask the capture actor's own world to map mapping where each piece's world box lands. This is the
|
|
|
|
|
* default because it is the same mapping the player marker is placed through, so the picture and the
|
|
|
|
|
* marker cannot describe two different maps. Falls back to the filename order when there is no actor
|
|
|
|
|
* to ask, or when the answer is not a lattice. */
|
|
|
|
|
SampledFromMapping UMETA(DisplayName = "Sampled From The Mapping"),
|
|
|
|
|
|
|
|
|
|
/** Put the piece whose filename says column C, row R in the cell C across and R down, and ask nothing.
|
|
|
|
|
* What is left when the mapping cannot be consulted. */
|
|
|
|
|
FilenameColumnRow UMETA(DisplayName = "Filename Column Row")
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** Whether each piece's own picture is mirrored inside its cell. The actor's bFlipU and bFlipV mirror the
|
|
|
|
|
* whole map, and a piece baked as a map of its own carries that mirroring either inside the asset or not
|
|
|
|
|
* at all, depending on how the bake wrote it. Nothing the widget can ask, so it is a setting, and off is
|
|
|
|
|
* right for the usual bake: the one that reads the way the world does. */
|
|
|
|
|
UENUM(BlueprintType)
|
|
|
|
|
enum class EMapPieceMirroring : uint8
|
|
|
|
|
{
|
|
|
|
|
/** Draw the piece as the asset has it. */
|
|
|
|
|
None UMETA(DisplayName = "As The Asset Has It"),
|
|
|
|
|
/** Mirror every piece left to right, the counterpart of the actor's bFlipU. */
|
|
|
|
|
Horizontal UMETA(DisplayName = "Horizontal"),
|
|
|
|
|
/** Mirror every piece top to bottom, the counterpart of the actor's bFlipV. */
|
|
|
|
|
Vertical UMETA(DisplayName = "Vertical"),
|
|
|
|
|
/** Both, for a bake that was made with both flags on. */
|
|
|
|
|
Both UMETA(DisplayName = "Both")
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** Which way the player is said to be facing, which is the direction the marker is turned to point along. A map
|
|
|
|
|
* is usually asked which way the player is going, and a pawn and a camera can disagree about that: this is the
|
|
|
|
|
* setting that says which of them the map should believe. */
|
|
|
|
|
UENUM(BlueprintType)
|
|
|
|
|
enum class EMapMarkerFacing : uint8
|
|
|
|
|
{
|
|
|
|
|
/** The way the pawn is turned to, which is what the player's own forward is for everything that moves
|
|
|
|
|
* them. The default, because the pawn is what the marker's place on the map comes from. */
|
|
|
|
|
PawnRotation UMETA(DisplayName = "Pawn Rotation"),
|
|
|
|
|
|
|
|
|
|
/** The way the controller is looking, which is the camera's own yaw and can be a long way from the pawn's
|
|
|
|
|
* while the pawn is moving sideways or backwards. */
|
|
|
|
|
ControlRotation UMETA(DisplayName = "Control Rotation"),
|
|
|
|
|
|
|
|
|
|
/** The way the pawn is moving. A pawn standing still points nowhere by its velocity, so the way it is
|
|
|
|
|
* turned to stands in. */
|
|
|
|
|
Velocity UMETA(DisplayName = "Velocity"),
|
|
|
|
|
|
|
|
|
|
/** Not the player's facing at all: the marker is turned by MarkerFacingOffset and nothing else, which is
|
|
|
|
|
* what a marker that should sit still, or art that has to be squared up, is for. */
|
|
|
|
|
None UMETA(DisplayName = "Not Turned To The Player")
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/** One cell of the map: which piece of the bake was asked for it, and where the widget put it. Kept so that
|
|
|
|
|
* ReportMapLayout can say what the widget decided without the decision being worked out again. */
|
|
|
|
|
USTRUCT(BlueprintType)
|
|
|
|
|
struct FEleriMapPieceCell
|
|
|
|
|
{
|
|
|
|
|
GENERATED_BODY()
|
|
|
|
|
|
|
|
|
|
/** Column in the piece's own filename. */
|
|
|
|
|
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map")
|
|
|
|
|
int32 SourceColumn = 0;
|
|
|
|
|
|
|
|
|
|
/** Row in the piece's own filename. */
|
|
|
|
|
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map")
|
|
|
|
|
int32 SourceRow = 0;
|
|
|
|
|
|
|
|
|
|
/** Column the piece is drawn in, counted across from the left of the map. */
|
|
|
|
|
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map")
|
|
|
|
|
int32 CellColumn = 0;
|
|
|
|
|
|
|
|
|
|
/** Row the piece is drawn in, counted down from the top of the map. */
|
|
|
|
|
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map")
|
|
|
|
|
int32 CellRow = 0;
|
|
|
|
|
|
|
|
|
|
/** The asset this cell is showing, as a long object path. Reported even when the load fails, because a
|
|
|
|
|
* path that does not exist is the first thing worth knowing about a cell that came back empty. */
|
|
|
|
|
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map")
|
|
|
|
|
FString ObjectPath;
|
|
|
|
|
|
|
|
|
|
/** The texture the cell is showing, or null while its piece is still arriving, which is the normal state
|
|
|
|
|
* for a moment after the map is first opened. */
|
|
|
|
|
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map")
|
|
|
|
|
TObjectPtr<UTexture2D> Texture = nullptr;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* The world map, as a grid of the baked map's pieces.
|
|
|
|
|
*
|
|
|
|
|
* The bake writes one texture per piece, each a map of its own region, and this widget lays them out in a
|
|
|
|
|
* grid, one cell per piece, with the player marked where their world position falls. Nothing here is drawn
|
|
|
|
|
* from the level at runtime: the pieces are the bake, and the level only says where a world position lands
|
|
|
|
|
* in them, which is what the capture actor's world to map mapping is for.
|
|
|
|
|
*
|
|
|
|
|
* The map itself is built in code, in RebuildWidget, because a user widget's slate widget is made from the root
|
|
|
|
|
* of its widget tree and there is no tree at all until the widget has been initialized. A blueprint subclass of
|
|
|
|
|
* this therefore needs to hold nothing: it may be empty, or its root may be a panel, which is where this
|
|
|
|
|
* widget's own tree is added. The one part of the map that is not built here is the player's marker, which is
|
|
|
|
|
* the widget named PlayerMarker in that blueprint: what a marker looks like is the design's business, and only
|
|
|
|
|
* where it sits on the map is the map's.
|
|
|
|
|
*
|
|
|
|
|
* The pieces are 16384 pixel virtual textures, so loading all 64 of them eagerly would be a stall and a
|
|
|
|
|
* virtual texture pool full of a map nobody is looking at. They are asked for lazily, the first time the
|
|
|
|
|
* widget is shown, in one batched asynchronous load, and the renderer decides from there what of them is
|
|
|
|
|
* resident.
|
|
|
|
|
*/
|
|
|
|
|
UCLASS()
|
|
|
|
|
class PROJECTELERI_API UMapWidget : public UEleriBaseWidget
|
|
|
|
|
{
|
|
|
|
|
GENERATED_BODY()
|
|
|
|
|
|
|
|
|
|
public:
|
|
|
|
|
UMapWidget();
|
|
|
|
|
|
|
|
|
|
//----- Map source -----
|
|
|
|
|
|
|
|
|
|
/** Map the pieces were baked from. Spelled out rather than taken from the world, because in PIE the
|
|
|
|
|
* world's own name is prefixed (UEDPIE_0_Persistent_GameWorld) and the assets are not. */
|
|
|
|
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Source")
|
|
|
|
|
FString MapName;
|
|
|
|
|
|
|
|
|
|
/** Folder the baked map was saved into, as a long package path: the same one the capture actor's
|
|
|
|
|
* OutputPackageFolder names. */
|
|
|
|
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Source")
|
|
|
|
|
FString PiecePackageFolder;
|
|
|
|
|
|
|
|
|
|
/** Name of one piece, with {Map}, {Grid}, {Col} and {Row} standing in for the parts that vary. The
|
|
|
|
|
* pieces are named after the map they came from, so a map and its pieces can be re-baked without this
|
|
|
|
|
* having to be edited. */
|
|
|
|
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Source")
|
|
|
|
|
FString PieceAssetPattern;
|
|
|
|
|
|
|
|
|
|
/** Pieces per axis: the grid the map was cut into, and the side of the grid that is drawn. */
|
|
|
|
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Source", meta = (ClampMin = "1", ClampMax = "64"))
|
|
|
|
|
int32 GridSize;
|
|
|
|
|
|
|
|
|
|
/** How the widget works out which piece goes where. */
|
|
|
|
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Source")
|
|
|
|
|
EMapPieceLayout PieceLayout;
|
|
|
|
|
|
|
|
|
|
/** Whether each piece is mirrored inside its cell. Only a map baked with the capture actor's bFlipU or
|
|
|
|
|
* bFlipV needs anything other than the default, and the widget warns at load when one of those flags is
|
|
|
|
|
* on while this still says the pieces are drawn as the assets have them. */
|
|
|
|
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Source")
|
|
|
|
|
EMapPieceMirroring PieceMirroring;
|
|
|
|
|
|
|
|
|
|
/** Whether the pieces are asked for as soon as the widget is constructed rather than the first time it
|
|
|
|
|
* is shown. Off is the lazy default: the map is not usually the first thing a player opens. */
|
|
|
|
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Loading")
|
|
|
|
|
bool bPreloadPiecesOnConstruct;
|
|
|
|
|
|
|
|
|
|
//----- View settings -----
|
|
|
|
|
|
|
|
|
|
/** Zoom of one fits the whole map across the shorter side of the view, so the map is as large as it can
|
|
|
|
|
* be and still be seen whole. Everything else is relative to that. */
|
|
|
|
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View", meta = (ClampMin = "0.01"))
|
|
|
|
|
float DefaultZoom;
|
|
|
|
|
|
|
|
|
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View", meta = (ClampMin = "0.01"))
|
|
|
|
|
float MinZoom;
|
|
|
|
|
|
|
|
|
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View", meta = (ClampMin = "0.01"))
|
|
|
|
|
float MaxZoom;
|
|
|
|
|
|
|
|
|
|
/** How much one step of zoom changes the zoom: one notch of the wheel, and one call to ZoomIn or
|
|
|
|
|
* ZoomOut. */
|
|
|
|
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View", meta = (ClampMin = "1.0001"))
|
|
|
|
|
float ZoomStep;
|
|
|
|
|
|
|
|
|
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View", meta = (ClampMin = "1.0001"))
|
|
|
|
|
float MoveStep;
|
|
|
|
|
|
|
|
|
|
/** Design units one piece is drawn at, which fixes how large the whole map is before zoom is applied.
|
|
|
|
|
* It sets the size the pieces are laid out at, not how large they appear: zoom one still fits the map
|
|
|
|
|
* to the view. */
|
|
|
|
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View", meta = (ClampMin = "16"))
|
|
|
|
|
float PieceCellSize;
|
|
|
|
|
|
|
|
|
|
/** Whether opening the map starts with the whole map in view and centred on the player, which is what
|
|
|
|
|
* the map is for. Off leaves whatever the player had zoomed and panned to. */
|
|
|
|
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View")
|
|
|
|
|
bool bResetViewWhenShown;
|
|
|
|
|
|
|
|
|
|
//----- Marker settings -----
|
|
|
|
|
|
|
|
|
|
/** Whether the marker follows the player as they move. Off freezes it where it was, which is what a
|
|
|
|
|
* player comparing two places wants. */
|
|
|
|
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Marker")
|
|
|
|
|
bool bFollowPlayerByDefault;
|
|
|
|
|
|
|
|
|
|
/** Whether the view keeps the player at its centre as they move. Dragging the map turns this off, so
|
|
|
|
|
* that looking somewhere else is not fought by the marker being pulled back every frame. */
|
|
|
|
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Marker")
|
|
|
|
|
bool bCenterOnPlayerByDefault;
|
|
|
|
|
|
|
|
|
|
/** The player's place on the map: the widget of this name in the blueprint this map is drawn in, found by
|
|
|
|
|
* name rather than made here. Nothing in this file draws it, so what the marker looks like -- its brush,
|
|
|
|
|
* its colours, its corner radii -- belongs to the design. The map takes it onto its own layer, which is
|
|
|
|
|
* what a marker has to be on to move and zoom with the map it marks, and then sizes it, places it and
|
|
|
|
|
* turns it. Any kind of widget will do, from an image to a panel holding several. Optional, because a
|
|
|
|
|
* design with no marker in it is a map that says so once rather than a map that will not open. */
|
|
|
|
|
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map|Marker", meta = (BindWidgetOptional))
|
|
|
|
|
TObjectPtr<UWidget> PlayerMarker;
|
|
|
|
|
|
|
|
|
|
/** How long the marker's longer side is drawn, in the map's own design units, which is the one thing about
|
|
|
|
|
* the marker's look the map has to know, because the map is what places it and is measured in these units.
|
|
|
|
|
* What shape it is comes from the design: an icon's shape is the shape of its art, and anything else is
|
|
|
|
|
* drawn at the shape it asks to be, so a wide or a tall marker is not squeezed into a square. */
|
|
|
|
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Marker", meta = (ClampMin = "2"))
|
|
|
|
|
float MarkerSize;
|
|
|
|
|
|
|
|
|
|
/** Degrees added to the turn the marker is given, for art that does not point up: an arrow drawn pointing
|
|
|
|
|
* right wants -90 here, and one drawn pointing down wants 180. */
|
|
|
|
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Marker")
|
|
|
|
|
float MarkerFacingOffset;
|
|
|
|
|
|
|
|
|
|
/** Colour the map's own rectangle is filled with, so that a piece still arriving reads as a part of the
|
|
|
|
|
* map that has not been drawn yet rather than as nothing at all. */
|
|
|
|
|
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|View")
|
|
|
|
|
FLinearColor MapBackgroundColor;
|
|
|
|
|
|
|
|
|
|
//----- View state -----
|
|
|
|
|
|
|
|
|
|
/** Zoom relative to fitting the map in the view, held between MinZoom and MaxZoom. */
|
|
|
|
|
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map|View")
|
|
|
|
|
float Zoom;
|
|
|
|
|
|
|
|
|
|
/** Whether the marker is following the player. */
|
|
|
|
|
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map|Marker")
|
|
|
|
|
bool bFollowPlayer;
|
|
|
|
|
|
|
|
|
|
/** Whether the view is keeping the player at its centre. */
|
|
|
|
|
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map|Marker")
|
|
|
|
|
bool bCenterOnPlayer;
|
|
|
|
|
|
|
|
|
|
/** Whether the map is being dragged right now, which is worth knowing outside the widget because a drag
|
|
|
|
|
* is not a click on whatever is behind the map. */
|
|
|
|
|
UPROPERTY(BlueprintReadOnly, Category = "Eleri|Map|View")
|
|
|
|
|
bool bPanning;
|
|
|
|
|
|
|
|
|
|
//----- View API -----
|
|
|
|
|
|
|
|
|
|
/** Sets the zoom, held between MinZoom and MaxZoom. One is the whole map in view. */
|
|
|
|
|
UFUNCTION(BlueprintCallable, Category = "Eleri|Map|View")
|
|
|
|
|
void SetZoom(float NewZoom);
|
|
|
|
|
|
|
|
|
|
/** Zooms by a signed number of steps: positive zooms in, negative zooms out, 0 does nothing.
|
|
|
|
|
* A magnitude of 1 is one full ZoomStep, so this can be fed the raw value from a wheel or
|
|
|
|
|
* gamepad trigger axis directly. Fractional or >1 magnitudes scale accordingly. */
|
|
|
|
|
UFUNCTION(BlueprintCallable, Category = "Eleri|Map|View")
|
|
|
|
|
void ZoomBy(float Amount);
|
|
|
|
|
|
|
|
|
|
/** Fits the whole map in the view, centres it on the player, and holds it there again. */
|
|
|
|
|
UFUNCTION(BlueprintCallable, Category = "Eleri|Map|View")
|
|
|
|
|
void ResetView();
|
|
|
|
|
|
|
|
|
|
//----- Marker API -----
|
|
|
|
|
|
|
|
|
|
UFUNCTION(BlueprintCallable, Category = "Eleri|Map|Marker")
|
|
|
|
|
void SetFollowPlayer(bool bNewFollow);
|
|
|
|
|
|
|
|
|
|
UFUNCTION(BlueprintPure, Category = "Eleri|Map|Marker")
|
|
|
|
|
bool IsFollowingPlayer() const { return bFollowPlayer; }
|
|
|
|
|
|
|
|
|
|
/** Whether the view is centred on the player, which it is until the map is dragged. */
|
|
|
|
|
UFUNCTION(BlueprintCallable, Category = "Eleri|Map|Marker")
|
|
|
|
|
void SetCenterOnPlayer(bool bNewCenter);
|
|
|
|
|
|
|
|
|
|
UFUNCTION(BlueprintPure, Category = "Eleri|Map|Marker")
|
|
|
|
|
bool IsCenteringOnPlayer() const { return bCenterOnPlayer; }
|
|
|
|
|
|
|
|
|
|
//----- The bake this widget is showing -----
|
|
|
|
|
|
|
|
|
|
/** What the widget has worked out about the bake so far: which piece the pattern asks for, where it was
|
|
|
|
|
* put, and whether it has arrived. */
|
|
|
|
|
UFUNCTION(BlueprintPure, Category = "Eleri|Map|Source")
|
|
|
|
|
const TArray<FEleriMapPieceCell>& GetPieceCells() const { return PieceCells; }
|
|
|
|
|
|
|
|
|
|
/** The capture actor the region and the mapping came from, or null when there is none in the world. Null
|
|
|
|
|
* means the player cannot be placed and the lattice could not be checked, which is reported at load
|
|
|
|
|
* rather than treated as an error. */
|
|
|
|
|
UFUNCTION(BlueprintPure, Category = "Eleri|Map|Source")
|
|
|
|
|
AOrthoMapCaptureActor* GetMapCaptureActor() const { return CaptureActor; }
|
|
|
|
|
|
|
|
|
|
/** Does the load again: works the layout out from scratch and asks for the pieces, whether they were
|
|
|
|
|
* asked for before or not. The thing to call after re-baking the map without leaving the editor. */
|
|
|
|
|
UFUNCTION(BlueprintCallable, Category = "Eleri|Map|Source")
|
|
|
|
|
void RefreshPieces();
|
|
|
|
|
|
|
|
|
|
/** Logs the bake this widget is showing: the pattern and folder it asked, whether the mapping could be
|
|
|
|
|
* consulted, how many pieces arrived, and every cell's piece and place. The thing to look at when the
|
|
|
|
|
* map is blank, or whole but wrong. */
|
|
|
|
|
UFUNCTION(BlueprintCallable, Category = "Eleri|Map|Source")
|
|
|
|
|
void ReportMapLayout();
|
|
|
|
|
|
|
|
|
|
/** Everything the widget does when it is shown: resolves the bake, asks for the pieces the first time,
|
|
|
|
|
* and fits the map to the view. Exposed so that a blueprint can do the same work for a show this widget
|
|
|
|
|
* did not get to see, and so that the show path is one thing rather than three. */
|
|
|
|
|
UFUNCTION(BlueprintCallable, Category = "Eleri|Map")
|
|
|
|
|
void HandleShown();
|
|
|
|
|
|
|
|
|
|
protected:
|
|
|
|
|
//----- Lifetime, in the form the engine hands it over -----
|
|
|
|
|
|
|
|
|
|
virtual void ToggleWidget_Implementation(bool bActive) override;
|
|
|
|
|
virtual void SetVisibility(ESlateVisibility InVisibility) override;
|
|
|
|
|
virtual void NativeConstruct() override;
|
|
|
|
|
virtual void NativeDestruct() override;
|
|
|
|
|
virtual void NativeTick(const FGeometry& MyGeometry, float InDeltaTime) override;
|
|
|
|
|
|
|
|
|
|
/** Where the tree has to be built: a user widget's slate widget is made by taking the root of its widget
|
|
|
|
|
* tree, and there is no tree until the widget has been initialized, which this call makes sure of. */
|
|
|
|
|
virtual TSharedRef<SWidget> RebuildWidget() override;
|
|
|
|
|
|
|
|
|
|
//----- Input, in the form the engine hands it over -----
|
|
|
|
|
|
|
|
|
|
virtual void HandleUiInputAction_Implementation(EEleriUIInputAction InputAction, FInputActionValue Value) override;
|
|
|
|
|
|
|
|
|
|
//----- Building and loading -----
|
|
|
|
|
|
|
|
|
|
/** Builds the widget tree if it is not there already, so that calling this twice is nothing. */
|
|
|
|
|
void EnsureWidgetTree();
|
|
|
|
|
|
|
|
|
|
/** Resolves the bake and asks for its pieces, once. Called on the way to being shown rather than at
|
|
|
|
|
* construction, so that the world is up and the map is only paid for when it is wanted. Doing it twice
|
|
|
|
|
* is nothing. */
|
|
|
|
|
void EnsureMapReady();
|
|
|
|
|
|
|
|
|
|
/** Works out which piece belongs in which cell and fills PieceCells with it. Reports the reason and
|
|
|
|
|
* returns false when the mapping could not be consulted, which leaves the caller to say so. */
|
|
|
|
|
bool ResolveMapLayout();
|
|
|
|
|
|
|
|
|
|
/** Works out how the baked map lies in the world: what a step of one world unit along X does to the map's
|
|
|
|
|
* own UV, and what a step along Y does, both taken from the capture actor's own mapping. That is what the
|
|
|
|
|
* marker is turned by, and taking it from the mapping is what makes a bake that was captured turned, or
|
|
|
|
|
* mirrored with the actor's flip flags, come out right without anything being assumed about it. */
|
|
|
|
|
void ResolveMapAxes();
|
|
|
|
|
|
|
|
|
|
/** The mapping route to a lattice: every piece's world box centre put through the capture actor's own
|
|
|
|
|
* world to map mapping, which is the mapping the marker is placed through as well. False with a reason
|
|
|
|
|
* when there is no actor, when a piece lands off the map, or when two pieces land in one cell. */
|
|
|
|
|
bool TrySamplePieceCells(FText& OutReason);
|
|
|
|
|
|
|
|
|
|
/** Where a piece's own cell of the region lands in the baked map, as a cell of the grid. */
|
|
|
|
|
bool GetPieceCellFromMapping(int32 SourceColumn, int32 SourceRow, FIntPoint& OutCell) const;
|
|
|
|
|
|
|
|
|
|
/** The other route: the piece whose filename says column C, row R goes in the cell C across and R down,
|
|
|
|
|
* without asking anything. */
|
|
|
|
|
void FillPieceCellsFromFilenames();
|
|
|
|
|
|
|
|
|
|
/** One piece's box in the world, from the region the pieces were cut out of: columns run along X and
|
|
|
|
|
* rows along Y from its min corner, the way round the bake's own piece boxes run. */
|
|
|
|
|
FBox GetPieceWorldBox(int32 SourceColumn, int32 SourceRow) const;
|
|
|
|
|
|
|
|
|
|
/** Asks for every piece in PieceCells in one batched asynchronous load. */
|
|
|
|
|
void RequestPieceLoads();
|
|
|
|
|
|
|
|
|
|
/** Puts every arrived piece on its image, and reports the ones that did not arrive. */
|
|
|
|
|
void ApplyLoadedPieces();
|
|
|
|
|
|
|
|
|
|
/** The brush one piece image is drawn with: the piece's own texture, and the mirroring the bake needs,
|
|
|
|
|
* which is the one thing about a piece that cannot be asked for and has to be told. */
|
|
|
|
|
void ApplyPieceBrush(UImage* PieceImage, UTexture2D* PieceTexture) const;
|
|
|
|
|
|
|
|
|
|
//----- View -----
|
|
|
|
|
|
|
|
|
|
/** Grid side of the drawn map in design units: GridSize pieces of PieceCellSize. */
|
|
|
|
|
float GetMapDesignSize() const;
|
|
|
|
|
|
|
|
|
|
/** Side of the view the map is fit into, in slate units, or zero before the widget has been laid out. */
|
|
|
|
|
FVector2D GetViewSize() const;
|
|
|
|
|
|
|
|
|
|
/** Slate units one design unit of the map is drawn at: the fit-relative zoom, or zero before the widget
|
|
|
|
|
* has been laid out. Everything that turns a screen distance into a map distance goes through it. */
|
|
|
|
|
float GetMapScale() const;
|
|
|
|
|
|
|
|
|
|
/** The content point held at the centre of the view, clamped to the map so that it cannot be dragged
|
|
|
|
|
* off into the void around it. */
|
|
|
|
|
FVector2D GetClampedFocusPoint(const FVector2D& InFocusPoint) const;
|
|
|
|
|
|
|
|
|
|
/** Puts a content position at the centre of the view, clamped. */
|
|
|
|
|
void FocusOnContent(const FVector2D& ContentPosition);
|
|
|
|
|
|
|
|
|
|
/** Hands the zoom and the focus point to the map layer, and does nothing when they have not changed, so
|
|
|
|
|
* that following the player does not invalidate a transform that is already right. */
|
|
|
|
|
void ApplyViewTransform();
|
|
|
|
|
|
|
|
|
|
/** Where the player is in the baked map, or false when there is nothing to say: no pawn, or no capture
|
|
|
|
|
* actor to put their position through. */
|
|
|
|
|
bool GetPlayerMapUV(FVector2D& OutUV) const;
|
|
|
|
|
|
|
|
|
|
/** The size the marker is drawn at: the shape the design gave it, with its longer side at MarkerSize, so
|
|
|
|
|
* that a wide or a tall marker is not squeezed into a square. A marker with nothing to say what shape it
|
|
|
|
|
* is -- a box with nothing in it, or a panel with nothing in that -- is drawn square. */
|
|
|
|
|
FVector2D GetMarkerSlotSize() const;
|
|
|
|
|
|
|
|
|
|
/** The angle the marker is turned to on screen, in degrees clockwise, or false when there is nothing to
|
|
|
|
|
* say: a mapping that cannot say how the map lies in the world, or a facing that cannot be had. */
|
|
|
|
|
bool ResolveMarkerAngle(float& OutAngle) const;
|
|
|
|
|
|
|
|
|
|
/** Puts the marker where the player is, and hides it when they are off the map. */
|
|
|
|
|
void UpdateMarkerView();
|
|
|
|
|
|
|
|
|
|
//----- Drag and wheel -----
|
|
|
|
|
|
|
|
|
|
/** Starts a drag, from the screen position it started at. One pointer at a time: a second finger while a
|
|
|
|
|
* drag is running is ignored rather than added, because the map is dragged and not pinched. */
|
|
|
|
|
void BeginPan(int32 PointerIndex, const FVector2D& ScreenPosition);
|
|
|
|
|
|
|
|
|
|
/** Moves the map under the pointer, which is also what turns the view loose from the player. */
|
|
|
|
|
void UpdatePan(const FVector2D& Delta);
|
|
|
|
|
|
|
|
|
|
/** Ends the drag this pointer started, and does nothing for any other pointer. */
|
|
|
|
|
void EndPan(int32 PointerIndex);
|
|
|
|
|
|
|
|
|
|
//----- Widgets the map builds -----
|
|
|
|
|
|
|
|
|
|
/** Root of the tree this widget builds. */
|
|
|
|
|
UPROPERTY(Transient)
|
|
|
|
|
TObjectPtr<UOverlay> MapRoot;
|
|
|
|
|
|
|
|
|
|
/** What the map is looked at through: everything outside it is clipped, and it is what the map is fit
|
|
|
|
|
* into. */
|
|
|
|
|
UPROPERTY(Transient)
|
|
|
|
|
TObjectPtr<UOverlay> MapViewport;
|
|
|
|
|
|
|
|
|
|
/** What the map is drawn on: the map's own rectangle, the pieces, and the marker, moved and scaled by
|
|
|
|
|
* one transform rather than 64. */
|
|
|
|
|
UPROPERTY(Transient)
|
|
|
|
|
TObjectPtr<UCanvasPanel> MapLayer;
|
|
|
|
|
|
|
|
|
|
/** The region the map covers, drawn so that a piece still arriving is a pale part of the map rather than
|
|
|
|
|
* a hole in it. */
|
|
|
|
|
UPROPERTY(Transient)
|
|
|
|
|
TObjectPtr<UBorder> MapBackground;
|
|
|
|
|
|
|
|
|
|
/** The 64 pieces, each at the cell the layout put it in. */
|
|
|
|
|
UPROPERTY(Transient)
|
|
|
|
|
TObjectPtr<UCanvasPanel> PieceGrid;
|
|
|
|
|
|
|
|
|
|
/** One image per piece, in cell order: row by row from the top left. */
|
|
|
|
|
UPROPERTY(Transient)
|
|
|
|
|
TArray<TObjectPtr<UImage>> PieceImages;
|
|
|
|
|
|
|
|
|
|
/** Takes every pointer event the map needs, so that a drag is never also a click on the level behind
|
|
|
|
|
* the map. */
|
|
|
|
|
UPROPERTY(Transient)
|
|
|
|
|
TObjectPtr<UBorder> Interaction;
|
|
|
|
|
|
|
|
|
|
//----- State -----
|
|
|
|
|
|
|
|
|
|
/** The bake as the widget worked it out: one entry per cell, whether the piece has arrived or not. */
|
|
|
|
|
UPROPERTY(Transient)
|
|
|
|
|
TArray<FEleriMapPieceCell> PieceCells;
|
|
|
|
|
|
|
|
|
|
/** The capture actor the region and the mapping came from: cached because it is asked a great deal, and
|
|
|
|
|
* found again whenever it has gone away. */
|
|
|
|
|
UPROPERTY(Transient)
|
|
|
|
|
TObjectPtr<AOrthoMapCaptureActor> CaptureActor;
|
|
|
|
|
|
|
|
|
|
/** Whether the layout and the piece requests have been worked out. False means a later call still has
|
|
|
|
|
* work to do, which is what happens when the map is shown before the world has its capture actor. */
|
|
|
|
|
bool bMapReady;
|
|
|
|
|
|
|
|
|
|
/** Whether the pieces have been asked for, so that asking happens once rather than once per show. */
|
|
|
|
|
bool bPiecesRequested;
|
|
|
|
|
|
|
|
|
|
/** What does the loading, held so that the pieces stay wanted until they have arrived. */
|
|
|
|
|
TSharedPtr<FStreamableHandle> PieceStreamHandle;
|
|
|
|
|
|
|
|
|
|
/** How much the map is zoomed, and the content point held at the centre of the view. */
|
|
|
|
|
FVector2D FocusPoint;
|
|
|
|
|
|
|
|
|
|
/** Where the player is on the map, as the map's own UV, and whether the marker has ever been placed.
|
|
|
|
|
* Held rather than asked for every frame so that the marker can be frozen where the player was, which
|
|
|
|
|
* is what turning following off is for. */
|
|
|
|
|
FVector2D MarkerMapUV;
|
|
|
|
|
|
|
|
|
|
bool bMarkerPlaced;
|
|
|
|
|
|
|
|
|
|
/** The angle the marker is turned to on screen, in degrees clockwise, and whether it has been written yet.
|
|
|
|
|
* Held so that a tick which leaves the facing alone writes nothing. */
|
|
|
|
|
float MarkerAngle;
|
|
|
|
|
|
|
|
|
|
bool bMarkerAngleApplied;
|
|
|
|
|
|
|
|
|
|
/** Whether the map has already said that the design has no marker in it, so that a map drawn without one
|
|
|
|
|
* says so once rather than on every update. */
|
|
|
|
|
bool bMissingMarkerReported;
|
|
|
|
|
|
|
|
|
|
/** How the baked map's own UV runs through the world: what a step of one world unit along X moves it, and
|
|
|
|
|
* what a step along Y moves it. Both are taken from the capture actor's own mapping, and both being zero
|
|
|
|
|
* is what bMapAxesResolved says no to. */
|
|
|
|
|
FVector2D MapUVPerWorldX;
|
|
|
|
|
|
|
|
|
|
FVector2D MapUVPerWorldY;
|
|
|
|
|
|
|
|
|
|
/** Whether the two above have been worked out: false means the marker is placed but not turned. */
|
|
|
|
|
bool bMapAxesResolved;
|
|
|
|
|
|
|
|
|
|
/** The transform last handed to the map layer, so that a tick which changes nothing writes nothing. */
|
|
|
|
|
FWidgetTransform AppliedMapTransform;
|
|
|
|
|
|
|
|
|
|
/** Whether the transform above is worth comparing against yet. */
|
|
|
|
|
bool bMapTransformApplied;
|
|
|
|
|
|
|
|
|
|
/** Which pointer is dragging, or INDEX_NONE. */
|
|
|
|
|
int32 PanPointerIndex;
|
|
|
|
|
|
|
|
|
|
/** Where the pointer was when the drag last moved, in screen space. */
|
|
|
|
|
FVector2D LastPanScreenPosition;
|
|
|
|
|
};
|