Reexport map textures and fixes for map marker, also fix for seed bed actor

This commit is contained in:
2026-09-26 17:44:03 +02:00
parent 812dea8382
commit 3a3a04b689
71 changed files with 328 additions and 188 deletions

View File

@@ -187,7 +187,7 @@ REM carries, which for this map are 16384 in eight tiles; a piece is baked at th
REM pixels over a quarter of the width is what a piece is.
SET MAP=/Game/Maps/Persistent_GameWorld
SET BUILDER=-run=WorldPartitionBuilderCommandlet -Builder=/Script/ProjectEleriEditor.WorldMapBuilder -AllowCommandletRendering -SCCProvider=None -unattended -Verbose
SET PIECE_SIZE=8192
SET PIECE_SIZE=4096
SET PIECE_TILES=4
SET PIECE_GRID=8
SET PIECE_ROOT=%PROJECT_ROOT%Intermediate\WorldMap

View File

@@ -109,6 +109,8 @@ bSkipMovies=False
+MapsToCook=(FilePath="/Game/Maps/W_Island_1")
+DirectoriesToAlwaysCook=(Path="/Game/GameData")
+DirectoriesToAlwaysCook=(Path="/NNEDenoiser")
+DirectoriesToAlwaysCook=(Path="/Game/Textures/Map")
+DirectoriesToAlwaysCook=(Path="/Game/Maps/Generated")
bRetainStagedDirectory=False
CustomStageCopyHandler=

Binary file not shown.

View File

@@ -604,7 +604,10 @@ FSeedBedSeedSlot& ASeedBedActor::CreateSlot(FIntVector2 SlotCoordinate, bool bRe
DestroySlotComponents(Slot);
}
Slot.SeedBedDecal = CreateSlotDecalComponent(this, GetRootComponent(), GetSlotComponentName(SlotCoordinate, TEXT("Slot")));
if (!IsValid(Slot.SeedBedDecal))
{
Slot.SeedBedDecal = CreateSlotDecalComponent(this, GetRootComponent(), GetSlotComponentName(SlotCoordinate, TEXT("Slot")));
}
if (SeedBedDecalMaterial.IsValid())
{
Slot.SeedBedDynamicMat = UKismetMaterialLibrary::CreateDynamicMaterialInstance(this, SeedBedDecalMaterial.Get(), NAME_None, EMIDCreationFlags::Transient);

View File

@@ -120,9 +120,16 @@ UMapWidget::UMapWidget()
bFollowPlayerByDefault = true;
bCenterOnPlayerByDefault = true;
MarkerSize = 32.f;
// The marker comes down as the map is pulled back, but by less than the map does: readable when the whole
// map is in view, without crowding a map that is zoomed in on a corner of itself.
MarkerZoomScale = -0.5f;
MarkerFacingOffset = 0.f;
// PlayerMarkerTexture is deliberately left empty: what the marker is a picture of is the design's to say,
// and a map with nothing set there draws no marker rather than failing to open.
MapBackgroundColor = FLinearColor(0.03f, 0.04f, 0.05f, 0.9f);
Zoom = 1.f;
@@ -132,13 +139,14 @@ UMapWidget::UMapWidget()
bMapReady = false;
bPiecesRequested = false;
bMarkerRequested = false;
bMarkerPlaced = false;
// Nothing has been turned yet, and there is no map yet to say how the map lies in the world.
MarkerAngle = 0.f;
bMarkerAngleApplied = false;
// Nothing has been said yet about the design having no marker in it.
// Nothing has been said yet about this map having no marker art assigned.
bMissingMarkerReported = false;
MapUVPerWorldX = FVector2D::ZeroVector;
MapUVPerWorldY = FVector2D::ZeroVector;
@@ -172,9 +180,9 @@ TSharedRef<SWidget> UMapWidget::RebuildWidget()
void UMapWidget::EnsureWidgetTree()
{
// Called from RebuildWidget and again from everything that wants the map on screen, so the work is only
// done when there is something to do: a standing tree is the whole test. The marker is not part of it,
// because the design's marker is not the map's to make.
if (MapRoot && MapViewport && MapLayer && PieceGrid && Interaction)
// done when there is something to do: a standing tree is the whole test, and the marker built below is as
// much a part of that tree as the pieces are.
if (MapRoot && MapViewport && MapLayer && PieceGrid && PlayerMarker && Interaction)
{
return;
}
@@ -267,9 +275,25 @@ void UMapWidget::EnsureWidgetTree()
NewPieceImages.Add(PieceImage);
}
// The player's marker is not built here: it is the widget named PlayerMarker in the design, and it is taken
// onto this layer by EnsureMarkerOnMap once the layer is standing. A marker built in code could only ever
// be the one thing this file knew how to draw.
// The player's marker is made here rather than taken from the blueprint: the map is what knows where the
// player is, and the map is what has to size it, place it and turn it. It is one image at one place, so an
// image is the whole of it, and the one thing the design says about it is which art it draws, which
// PlayerMarkerTexture names. It goes on the map's own layer, because that is the thing that moves and zooms
// with the map it marks, and above the pieces, which is what its z order says. It starts hidden and stays
// that way until there is both a place to put it and something to draw there. It is the one widget built
// here that carries a name, because it is the one that gets looked for when the player's place is the thing
// that is wrong, and the name is its own so that nothing the design left in the blueprint answers to it.
UImage* NewMarker = WidgetTree->ConstructWidget<UImage>(UImage::StaticClass(), TEXT("MapPlayerMarker"));
NewMarker->SetVisibility(ESlateVisibility::Hidden);
if (UCanvasPanelSlot* NewMarkerSlot = NewLayer->AddChildToCanvas(NewMarker))
{
NewMarkerSlot->SetAnchors(FAnchors(0.f, 0.f, 0.f, 0.f));
// Alignment centres the widget on the slot's position, so a place on the map is one pair of numbers.
NewMarkerSlot->SetAlignment(FVector2D(0.5f, 0.5f));
NewMarkerSlot->SetAutoSize(false);
NewMarkerSlot->SetZOrder(2);
}
// Everything the map listens for arrives through this last widget: it lies over the whole view, so a drag
// on the map is a drag on the map and never a click on the level behind it. Nothing is drawn for it.
@@ -311,6 +335,7 @@ void UMapWidget::EnsureWidgetTree()
MapBackground = NewBackground;
PieceGrid = NewPieceGrid;
PieceImages = MoveTemp(NewPieceImages);
PlayerMarker = NewMarker;
Interaction = NewInteraction;
// The tree is standing but has not been laid out, so the transform is left to the next tick, which is the
@@ -341,6 +366,7 @@ void UMapWidget::EnsureMapReady()
}
RequestPieceLoads();
RequestMarkerLoad();
}
bool UMapWidget::ResolveMapLayout()
@@ -462,16 +488,6 @@ void UMapWidget::ResolveMapAxes()
MapUVPerWorldX = (CaptureActor->WorldToBakedMapUV(Centre + FVector(Step, 0.0, 0.0)) - CentreUV) / Step;
MapUVPerWorldY = (CaptureActor->WorldToBakedMapUV(Centre + FVector(0.0, Step, 0.0)) - CentreUV) / Step;
// A mapping that moves the map nowhere in one of the two directions has nothing to say about which way the
// map lies, and a marker turned by it would be turned by noise.
// if (MapUVPerWorldX.IsNearlyZero() || MapUVPerWorldY.IsNearlyZero())
// {
// MapUVPerWorldX = FVector2D::ZeroVector;
// MapUVPerWorldY = FVector2D::ZeroVector;
//
// return;
// }
bMapAxesResolved = true;
}
@@ -745,6 +761,74 @@ void UMapWidget::ApplyPieceBrush(UImage* PieceImage, UTexture2D* PieceTexture) c
PieceImage->SetBrush(Brush);
}
void UMapWidget::RequestMarkerLoad()
{
if (bMarkerRequested)
{
return;
}
bMarkerRequested = true;
const FSoftObjectPath MarkerPath = PlayerMarkerTexture.ToSoftObjectPath();
if (!MarkerPath.IsValid())
{
// Said once rather than every frame: a map with no marker art is a map somebody has not filled in, and
// it is still a map.
if (!bMissingMarkerReported)
{
bMissingMarkerReported = true;
UE_LOG(LogEleriMapWidget, Warning,
TEXT("This map has no marker art assigned, so the player's place is not drawn. Point Player Marker Texture at a texture to draw one."));
}
return;
}
// Asked for on its own rather than with the pieces, because it is one texture and there is nothing for it
// to wait for: it can arrive while the bake is still being resolved and be waiting on the layer.
MarkerStreamHandle = UAssetManager::GetStreamableManager().RequestAsyncLoad(
MarkerPath,
FStreamableDelegate::CreateUObject(this, &UMapWidget::ApplyLoadedMarker),
FStreamableManager::AsyncLoadHighPriority);
UE_LOG(LogEleriMapWidget, Verbose, TEXT("Asked for the marker art %s."), *MarkerPath.ToString());
}
void UMapWidget::ApplyLoadedMarker()
{
// Resolved from the path rather than taken from the load's own list of what it brought in, so that art
// something else has already loaded counts as arrived just as much as one this request fetched.
UTexture2D* MarkerTexture = Cast<UTexture2D>(PlayerMarkerTexture.ToSoftObjectPath().ResolveObject());
if (!MarkerTexture || !PlayerMarker)
{
UE_LOG(LogEleriMapWidget, Warning,
TEXT("The marker art %s did not arrive, so the player's place is not drawn."), *PlayerMarkerTexture.ToString());
return;
}
// The brush is replaced whole rather than adjusted, so that the way the marker is drawn is decided in one
// place. Its image size is the art's own, which is also the shape GetMarkerSlotSize reads back out of it,
// so the marker is drawn at the shape it was drawn in rather than squeezed into a square.
const FIntPoint ImportedSize = MarkerTexture->GetImportedSize();
FSlateBrush Brush;
Brush.SetResourceObject(MarkerTexture);
Brush.DrawAs = ESlateBrushDrawType::Image;
Brush.SetImageSize(FVector2D(ImportedSize.X, ImportedSize.Y));
PlayerMarker->SetBrush(Brush);
// The art can arrive after the marker has already been placed, so it is placed and sized again now that it
// has a shape, rather than left square until the next tick happens to notice.
UpdateMarkerView();
UE_LOG(LogEleriMapWidget, Verbose, TEXT("The marker art %s is up."), *PlayerMarkerTexture.ToString());
}
void UMapWidget::ReportMapLayout()
{
const int32 Grid = FMath::Max(1, GridSize);
@@ -905,16 +989,22 @@ bool UMapWidget::GetPlayerMapUV(FVector2D& OutUV) const
FVector2D UMapWidget::GetMarkerSlotSize() const
{
// The one thing about the marker's size the map decides: the longer side of it, in the map's own units.
const float Size = FMath::Max(2.f, MarkerSize);
// MarkerSize is that size at the map's closest zoom, and MarkerZoomScale is how much of the map being pulled
// back from there the marker's size follows: see the property for what its ends mean. The two ends of the
// zoom range are read in the order they are set in, as they are everywhere else here.
const float ClosestZoom = FMath::Max(FMath::Max(MinZoom, MaxZoom), KINDA_SMALL_NUMBER);
const float ZoomShare = FMath::Clamp(Zoom / ClosestZoom, KINDA_SMALL_NUMBER, 1.f);
const float Size = FMath::Max(2.f, MarkerSize * FMath::Pow(ZoomShare, MarkerZoomScale));
FVector2D MarkerShape = FVector2D::ZeroVector;
// An icon says what shape it is through the art it draws: the size the art was imported at is the shape it
// was drawn with, and the size of what is resident is the second choice for a texture with nothing imported
// to give.
if (const UImage* MarkerIcon = Cast<UImage>(PlayerMarker))
// The marker says what shape it is through the art it draws: the size the art was imported at is the shape
// it was drawn with, and the size of what is resident is the second choice for a texture with nothing
// imported to give. Before the art has arrived the brush holds no texture at all, so there is no shape.
if (PlayerMarker)
{
if (const UTexture2D* IconTexture = Cast<UTexture2D>(MarkerIcon->GetBrush().GetResourceObject()))
if (const UTexture2D* IconTexture = Cast<UTexture2D>(PlayerMarker->GetBrush().GetResourceObject()))
{
const FIntPoint ImportedSize = IconTexture->GetImportedSize();
@@ -926,23 +1016,23 @@ FVector2D UMapWidget::GetMarkerSlotSize() const
}
}
// Anything else -- a box, a panel, or an icon with no art pointed at yet -- says what shape it is by the
// size it asks to be drawn at, which is what the art of an image and the contents of a panel are measured
// into. Nothing has a size until it has been laid out, so this is asked again whenever the size is written
// rather than once.
// With no art yet -- or none assigned at all -- the image says what shape it is by the size it asks to be
// drawn at, which is nothing at all for an empty brush; nothing has a size until it has been laid out, so
// this is asked again whenever the size is written rather than once.
if (MarkerShape.X <= 0.0 || MarkerShape.Y <= 0.0)
{
MarkerShape = PlayerMarker ? PlayerMarker->GetDesiredSize() : FVector2D::ZeroVector;
}
// A marker with nothing to say what shape it is is drawn square: a plain box of a marker is a marker.
// A marker with no shape of its own -- no art assigned, or none arrived yet -- is drawn square rather than
// not at all: the map still has a size to place it at, and the art may yet turn up.
if (MarkerShape.X <= 0.0 || MarkerShape.Y <= 0.0)
{
return FVector2D(Size, Size);
}
// The design's own shape, drawn at the size the map asks for: one setting is the size of the marker whether
// the marker is an icon, a box or a panel of several, and none of them is squeezed out of shape by it.
// The art's own shape, drawn at the size the map asks for: one setting is the size of the marker whatever
// shape that art is, and none of it is squeezed out of shape by it.
return MarkerShape * (static_cast<double>(Size) / FMath::Max(MarkerShape.X, MarkerShape.Y));
}
@@ -967,6 +1057,8 @@ bool UMapWidget::ResolveMarkerAngle(float& OutAngle) const
void UMapWidget::UpdateMarkerView()
{
// There is nothing to place until the tree is standing, and building the tree is what makes the marker, so
// this is the test for "not built yet" rather than for a design that has no marker in it.
if (!PlayerMarker)
{
return;
@@ -993,14 +1085,18 @@ void UMapWidget::UpdateMarkerView()
}
// Whether the marker is drawn is the map's to say, because the map is what knows whether there is a place
// to put it; what the design says is what it looks like when there is one. Only the marker itself is taken
// out of the way of the pointer, so a marker made of several widgets keeps whatever they do themselves.
// to put it. Only the marker itself is taken out of the way of the pointer, so a press on the map is never
// a press on the marker.
PlayerMarker->SetVisibility(ESlateVisibility::SelfHitTestInvisible);
if (UCanvasPanelSlot* MarkerSlot = Cast<UCanvasPanelSlot>(PlayerMarker->Slot))
{
// Alignment centres the widget on the slot's position, so a place on the map is one pair of numbers.
MarkerSlot->SetPosition(MarkerMapUV * static_cast<double>(GetMapDesignSize()));
// The size is written here rather than once at build, because it is the art arriving that gives the
// marker a shape, and the art can arrive after the marker has already been placed.
MarkerSlot->SetSize(GetMarkerSlotSize());
}
// The turn is the one thing about the marker that changes without its place on the map changing, and it is
@@ -1033,6 +1129,10 @@ void UMapWidget::SetZoom(float NewZoom)
Zoom = Clamped;
ApplyViewTransform();
// The marker's size is the map's to say, and the map has just changed how large it is drawn at, so the
// marker is sized here rather than at the next tick: a tick later is a frame of it at the old size.
UpdateMarkerView();
}
void UMapWidget::ZoomBy(float Amount)
@@ -1225,16 +1325,23 @@ void UMapWidget::NativeConstruct()
void UMapWidget::NativeDestruct()
{
// The pieces are wanted by this widget and by nothing else, so letting go of the request lets go of whatever
// of them nothing else is holding on to. Asking again on the next show is cheap: what is still in memory
// arrives at once.
// The pieces and the marker art are wanted by this widget and by nothing else, so letting go of the requests
// lets go of whatever of them nothing else is holding on to. Asking again on the next show is cheap: what is
// still in memory arrives at once.
if (PieceStreamHandle.IsValid())
{
PieceStreamHandle->ReleaseHandle();
PieceStreamHandle.Reset();
}
if (MarkerStreamHandle.IsValid())
{
MarkerStreamHandle->ReleaseHandle();
MarkerStreamHandle.Reset();
}
bPiecesRequested = false;
bMarkerRequested = false;
PanPointerIndex = INDEX_NONE;
bPanning = false;

View File

@@ -4,6 +4,7 @@
#include "CoreMinimal.h"
#include "UI/EleriBaseWidget.h"
#include "UObject/SoftObjectPtr.h"
#include "MapWidget.generated.h"
class AOrthoMapCaptureActor;
@@ -118,9 +119,9 @@ struct FEleriMapPieceCell
* 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.
* widget's own tree is added. The player's marker is built here too, because the map is what knows where the
* player is and is what has to place, size and turn it; what the marker is a picture of is the one thing left
* to the design, and PlayerMarkerTexture is what says it.
*
* 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
@@ -216,22 +217,32 @@ public:
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;
/** The art the player's marker is drawn with. The marker itself is made here rather than found in the
* blueprint this map is drawn in: the map is what places it, sizes it and turns it, so the one thing left
* for the design to say is what it is a picture of. Soft, so the art is not loaded until a map is actually
* looked at, and optional, because a map with no art is a map with no marker rather than a map that will
* not open. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Marker")
TSoftObjectPtr<UTexture2D> PlayerMarkerTexture;
/** 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. */
/** How long the marker's longer side is drawn, in the map's own design units, at the map's closest zoom: see
* MarkerZoomScale for what it is drawn at further back than that. What shape it is comes from the art
* PlayerMarkerTexture names, which is the shape that art was imported at, 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;
/** How much of the map being pulled back the marker's size follows, as the power the zoom relative to
* MaxZoom is raised to: the marker is drawn at MarkerSize when the map is at its closest, whatever this
* says, and this is what it does below that. At nought the marker is that size in the map's own units at
* every zoom, so it comes down in the view exactly as fast as the map does. At one it comes down in step
* with the zoom as well, which is twice as fast as the map does on screen, and soon leaves a marker too
* small to read. Below nought the marker grows in map units as the map is pulled back instead, so it comes
* down in the view more slowly than the map does: at -1 it holds one size on screen however far out the
* map goes, which stays readable but stops describing the world. */
UPROPERTY(EditDefaultsOnly, BlueprintReadOnly, Category = "Eleri|Map|Marker", meta = (ClampMin = "-1", ClampMax = "1"))
float MarkerZoomScale;
/** 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")
@@ -381,6 +392,12 @@ protected:
/** Puts every arrived piece on its image, and reports the ones that did not arrive. */
void ApplyLoadedPieces();
/** Asks for PlayerMarkerTexture, once. */
void RequestMarkerLoad();
/** Puts the arrived marker texture on the marker, and reports it once if it did not arrive. */
void ApplyLoadedMarker();
/** 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;
@@ -412,9 +429,9 @@ protected:
* 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. */
/** The size the marker is drawn at: the shape of the art PlayerMarkerTexture names, with its longer side at
* MarkerSize, so that a wide or a tall marker is not squeezed into a square. Before the art has arrived,
* or with no art assigned at all, the marker has no shape to give and 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
@@ -465,6 +482,11 @@ protected:
UPROPERTY(Transient)
TArray<TObjectPtr<UImage>> PieceImages;
/** The player's place on the map, made here and put on MapLayer above the pieces. An image, because the
* marker is one texture at one place; everything about how it looks comes from PlayerMarkerTexture. */
UPROPERTY(Transient)
TObjectPtr<UImage> PlayerMarker;
/** Takes every pointer event the map needs, so that a drag is never also a click on the level behind
* the map. */
UPROPERTY(Transient)
@@ -491,6 +513,12 @@ protected:
/** What does the loading, held so that the pieces stay wanted until they have arrived. */
TSharedPtr<FStreamableHandle> PieceStreamHandle;
/** Whether PlayerMarkerTexture has been asked for, so that asking happens once rather than once per show. */
bool bMarkerRequested;
/** What does the loading, held so that the marker stays wanted until it has arrived. */
TSharedPtr<FStreamableHandle> MarkerStreamHandle;
/** How much the map is zoomed, and the content point held at the centre of the view. */
FVector2D FocusPoint;
@@ -507,7 +535,7 @@ protected:
bool bMarkerAngleApplied;
/** Whether the map has already said that the design has no marker in it, so that a map drawn without one
/** Whether the map has already said that no marker art is assigned, so that a map drawn without a marker
* says so once rather than on every update. */
bool bMissingMarkerReported;