Fixes for removal mode
This commit is contained in:
@@ -4,7 +4,6 @@
|
||||
#include "MoveActorsComponent.h"
|
||||
#include "../System/Subsystem/ObjectPersistenceSubsystem.h"
|
||||
#include "../System/MainBlueprintFunctionLibrary.h"
|
||||
#include "Kismet/KismetMathLibrary.h"
|
||||
#include "Kismet/GameplayStatics.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "../GameObjects/RemovableStaticMeshActor.h"
|
||||
@@ -14,6 +13,17 @@
|
||||
#include "Engine/StaticMeshActor.h"
|
||||
#include "LandscapeProxy.h"
|
||||
|
||||
namespace
|
||||
{
|
||||
/** Snaps a single coordinate onto the grid lattice. The grid lines run through the world origin,
|
||||
* exactly like UKismetMathLibrary::Vector_SnappedToGrid, but on one axis at a time and in double
|
||||
* precision so that the result does not depend on the size of the world. */
|
||||
double SnapCoordinateToGrid(double Coordinate, double GridSnap)
|
||||
{
|
||||
return GridSnap <= 0.0 ? Coordinate : FMath::RoundToDouble(Coordinate / GridSnap) * GridSnap;
|
||||
}
|
||||
}
|
||||
|
||||
// Sets default values for this component's properties
|
||||
UMoveActorsComponent::UMoveActorsComponent()
|
||||
{
|
||||
@@ -66,17 +76,27 @@ void UMoveActorsComponent::TickComponent(float DeltaTime, ELevelTick TickType, F
|
||||
|
||||
// Prefer the per-actor tuning values, falling back to the component defaults.
|
||||
const float Distance = MovingActor->DistanceFromActor > 0.f ? MovingActor->DistanceFromActor : PlacementDistance;
|
||||
const float GridSnap = MovingActor->GridScale > 0.f ? MovingActor->GridScale : GridSnapSize;
|
||||
|
||||
FVector PosVector = PlayerCharacter->GetActorLocation() + (ForwardVec * Distance) + FVector(0.f, 0.f, PlacementHeightOffset);
|
||||
|
||||
if (bGridMovementActive)
|
||||
{
|
||||
PosVector = UKismetMathLibrary::Vector_SnappedToGrid(PosVector, GridSnap);
|
||||
}
|
||||
// The grid pitch is the base cell size of this component grown by the whole number multiplier of the
|
||||
// object that is being moved, so an object that reserves more room still lands on the same lattice.
|
||||
const double GridSnap = GetGridPitchForActor(MovingActor);
|
||||
|
||||
const FVector TargetScale = (MovingActor->ScaleOverride != FVector::ZeroVector) ? MovingActor->ScaleOverride : MovingActor->GetActorScale();
|
||||
MovingActor->SetActorTransform(FTransform(MovingActor->GetActorRotation(), PosVector, TargetScale));
|
||||
|
||||
// The object is only snapped in the grid plane. Its height is decided by the surface probe below, so
|
||||
// snapping the hover position on Z as well would make the object jump in grid sized steps while
|
||||
// aiming, long before it ever touches a surface.
|
||||
FVector PosVector = PlayerCharacter->GetActorLocation() + (ForwardVec * Distance) + FVector(0.f, 0.f, PlacementHeightOffset);
|
||||
|
||||
FRotator TargetRotation = MovingActor->GetActorRotation();
|
||||
if (bGridMovementActive && GridRotationSnapDegrees > 0.f)
|
||||
{
|
||||
// A rotated object does not cover whole cells, which is what pulls it out of sync with the
|
||||
// objects that are placed around it, so its yaw is quantized while the grid is active.
|
||||
TargetRotation.Yaw = SnapCoordinateToGrid(TargetRotation.Yaw, GridRotationSnapDegrees);
|
||||
}
|
||||
|
||||
MovingActor->SetActorLocation(SnapFootprintToGrid(PosVector, TargetScale, TargetRotation, GridSnap));
|
||||
|
||||
// Look for a surface to rest the object on. The probe starts above the object so it can
|
||||
// also land on top of already placed actors (allowing objects to be stacked on each other)
|
||||
@@ -262,6 +282,126 @@ FBoxSphereBounds UMoveActorsComponent::GetMovingActorMeshBounds() const
|
||||
return FBoxSphereBounds(MovingActor->GetActorLocation(), FVector::ZeroVector, 0.0);
|
||||
}
|
||||
|
||||
FVector UMoveActorsComponent::SnapFootprintToGrid(const FVector& InLocation, const FVector& TargetScale, const FRotator& TargetRotation, double GridSnap)
|
||||
{
|
||||
if (!bGridMovementActive || GridSnap <= 0.0 || !IsValid(MovingActor))
|
||||
{
|
||||
return InLocation;
|
||||
}
|
||||
|
||||
// The rotation and the scale are applied before the footprint is measured: both of them change the
|
||||
// bounds that the footprint is derived from.
|
||||
MovingActor->SetActorTransform(FTransform(TargetRotation, InLocation, TargetScale));
|
||||
|
||||
if (GridFitMode == EPlaceableGridFitMode::Pivot)
|
||||
{
|
||||
return FVector(
|
||||
SnapCoordinateToGrid(InLocation.X, GridSnap),
|
||||
SnapCoordinateToGrid(InLocation.Y, GridSnap),
|
||||
InLocation.Z);
|
||||
}
|
||||
|
||||
FBoxSphereBounds MeshBounds = GetMovingActorMeshBounds();
|
||||
const FVector Size = MeshBounds.BoxExtent * 2.0;
|
||||
|
||||
// An object without a mesh has no footprint to align, so it keeps the plain pivot snap.
|
||||
if (Size.IsNearlyZero())
|
||||
{
|
||||
return FVector(
|
||||
SnapCoordinateToGrid(InLocation.X, GridSnap),
|
||||
SnapCoordinateToGrid(InLocation.Y, GridSnap),
|
||||
InLocation.Z);
|
||||
}
|
||||
|
||||
// A mesh that is not an exact multiple of the grid still owns whole cells: 175cm on a 50cm grid is
|
||||
// four cells. Whole cells keep every object on the same lattice, so neighbours are always a whole
|
||||
// number of cells apart and can never drift. Rounding up (instead of to the nearest cell) is what
|
||||
// guarantees that an object fits inside the cells it claims and can never overlap the object next to
|
||||
// it - the price is a symmetric margin inside the cell instead of a different gap for every object.
|
||||
// This is what makes it possible to use meshes that were not modelled to the grid.
|
||||
const double CellsX = GetGridCellCount(Size.X, GridSnap);
|
||||
const double CellsY = GetGridCellCount(Size.Y, GridSnap);
|
||||
|
||||
if (GridFitMode == EPlaceableGridFitMode::FootprintFitted)
|
||||
{
|
||||
// The smaller of the two axis factors is used so that the mesh never spills over a cell and its
|
||||
// proportions are kept. A mesh that would need a bigger change than GridFitMaxScaleDelta keeps
|
||||
// its margin instead of being distorted.
|
||||
const double MaxDelta = GridFitMaxScaleDelta;
|
||||
const double FitScale = FMath::Min(
|
||||
(CellsX * GridSnap) / FMath::Max(Size.X, 1.0),
|
||||
(CellsY * GridSnap) / FMath::Max(Size.Y, 1.0));
|
||||
|
||||
const double ClampedFitScale = FMath::Clamp(FitScale, 1.0 - MaxDelta, 1.0 + MaxDelta);
|
||||
if (!FMath::IsNearlyEqual(ClampedFitScale, 1.0, 0.001))
|
||||
{
|
||||
MovingActor->SetActorScale3D(TargetScale * ClampedFitScale);
|
||||
MeshBounds = GetMovingActorMeshBounds();
|
||||
}
|
||||
}
|
||||
|
||||
// The bounding box is centered in the cells it occupies instead of the pivot being put on a grid
|
||||
// line, so meshes whose pivot sits in a corner line up with the rest of the world. Z is untouched.
|
||||
const double BlockMinX = SnapCoordinateToGrid(MeshBounds.Origin.X - MeshBounds.BoxExtent.X, GridSnap);
|
||||
const double BlockMinY = SnapCoordinateToGrid(MeshBounds.Origin.Y - MeshBounds.BoxExtent.Y, GridSnap);
|
||||
|
||||
const FVector DesiredCenter(
|
||||
BlockMinX + (CellsX * GridSnap) * 0.5,
|
||||
BlockMinY + (CellsY * GridSnap) * 0.5,
|
||||
MeshBounds.Origin.Z);
|
||||
|
||||
return InLocation + (DesiredCenter - MeshBounds.Origin);
|
||||
}
|
||||
|
||||
FVector2D UMoveActorsComponent::GetMovingActorGridFootprint() const
|
||||
{
|
||||
const double GridSnap = GetGridPitchForActor(MovingActor);
|
||||
if (GridSnap <= 0.0)
|
||||
{
|
||||
return FVector2D::ZeroVector;
|
||||
}
|
||||
|
||||
const FVector Size = GetMovingActorMeshBounds().BoxExtent * 2.0;
|
||||
if (Size.IsNearlyZero())
|
||||
{
|
||||
return FVector2D::ZeroVector;
|
||||
}
|
||||
|
||||
return FVector2D(
|
||||
GetGridCellCount(Size.X, GridSnap),
|
||||
GetGridCellCount(Size.Y, GridSnap));
|
||||
}
|
||||
|
||||
double UMoveActorsComponent::GetGridPitchForActor(const ARemovableStaticMeshActor* Actor) const
|
||||
{
|
||||
return GetGridPitch(Actor, GridSnapSize);
|
||||
}
|
||||
|
||||
double UMoveActorsComponent::GetGridPitch(const ARemovableStaticMeshActor* Actor, double BaseGridSnapSize)
|
||||
{
|
||||
// The multiplier is clamped here instead of being trusted from the property: it is writable from
|
||||
// Blueprint, where the editor-only ClampMin does not apply, and a zero multiplier would turn the cell
|
||||
// size into zero, which reads as "no grid" in every bit of maths below.
|
||||
const double Multiplier = Actor ? static_cast<double>(Actor->GetGridSizeMultiplier()) : 1.0;
|
||||
|
||||
return BaseGridSnapSize * Multiplier;
|
||||
}
|
||||
|
||||
double UMoveActorsComponent::GetGridCellCount(double Size, double GridSnap)
|
||||
{
|
||||
if (GridSnap <= 0.0)
|
||||
{
|
||||
return 1.0;
|
||||
}
|
||||
|
||||
// The slack keeps a mesh that is exactly as wide as a whole number of cells (or a hair wider, since
|
||||
// mesh bounds are never exact) from claiming an extra cell, which would leave a gap the size of a
|
||||
// whole cell next to it.
|
||||
constexpr double CellCountTolerance = 0.001;
|
||||
|
||||
return FMath::Max(1.0, FMath::CeilToDouble(Size / GridSnap - CellCountTolerance));
|
||||
}
|
||||
|
||||
void UMoveActorsComponent::ToggleGridMovement()
|
||||
{
|
||||
bGridMovementActive = !bGridMovementActive;
|
||||
|
||||
@@ -22,6 +22,22 @@ enum class ERemovalAction : uint8
|
||||
MAX
|
||||
};
|
||||
|
||||
/** How the object being moved is aligned with the placement grid. Meshes are rarely exact multiples of
|
||||
* the grid pitch, which is what makes placed objects drift out of sync with the objects around them. */
|
||||
UENUM(BlueprintType)
|
||||
enum class EPlaceableGridFitMode : uint8
|
||||
{
|
||||
/** Snap the actor pivot to the grid. Only correct when every pivot sits at the center of its mesh. */
|
||||
Pivot,
|
||||
/** Quantize the mesh' footprint to whole grid cells (rounded up, so the mesh always fits inside the
|
||||
* cells it claims) and center it in them. A 175cm mesh on a 50cm grid claims four cells (200cm) and
|
||||
* keeps a 12.5cm margin on either side, so neighbours can never drift apart. */
|
||||
FootprintCentered,
|
||||
/** As FootprintCentered, but the mesh is also grown (within GridFitMaxScaleDelta) so that it fills
|
||||
* the cells it occupies exactly, which removes the margin. */
|
||||
FootprintFitted
|
||||
};
|
||||
|
||||
class ARemovableStaticMeshActor;
|
||||
class AEleriPlayerController;
|
||||
class AMyCharacter;
|
||||
@@ -53,8 +69,33 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement")
|
||||
float PlacementHeightOffset = 100.f;
|
||||
|
||||
/** Default size of one grid cell, in Unreal units: 50 units is half a metre. The placed actors fall
|
||||
* back on this value when they report their footprint while this component is not around yet (in the
|
||||
* editor), so both use the same base size. */
|
||||
static constexpr float DefaultGridSnapSize = 50.f;
|
||||
|
||||
/** Size of one grid cell, in Unreal units. Every object is placed on the lattice this defines, grown
|
||||
* by the whole number multiplier that the placed actor carries (see ARemovableStaticMeshActor::
|
||||
* GridScale). Keeping the base size in one place is what keeps objects of every size on a single
|
||||
* lattice instead of letting each of them drift on a lattice of its own. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement", meta = (ClampMin = "1.0", UIMin = "1.0"))
|
||||
float GridSnapSize = DefaultGridSnapSize;
|
||||
|
||||
/** How the object being moved is aligned with the grid, see EPlaceableGridFitMode. The pivot based
|
||||
* mode is only correct when every mesh' pivot sits at the center of the mesh. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement")
|
||||
float GridSnapSize = 250.f;
|
||||
EPlaceableGridFitMode GridFitMode = EPlaceableGridFitMode::FootprintCentered;
|
||||
|
||||
/** Yaw steps the object is quantized to while the grid is active. An object that is rotated freely
|
||||
* does not cover whole cells, which pushes it out of sync with the objects placed around it.
|
||||
* Use 0 to keep free rotation. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement", meta = (ClampMin = "0.0"))
|
||||
float GridRotationSnapDegrees = 90.f;
|
||||
|
||||
/** Largest relative size change FootprintFitted is allowed to apply so that a mesh fills its cells
|
||||
* exactly. 0.15 = up to 15%. A mesh that would need more than this keeps its cell margin. */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement", meta = (ClampMin = "0.0"))
|
||||
float GridFitMaxScaleDelta = 0.15f;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement")
|
||||
float GroundTraceLength = 1000.f;
|
||||
@@ -106,6 +147,12 @@ protected:
|
||||
* the detection box and the interaction widget, which do not match the mesh that is placed. */
|
||||
FBoxSphereBounds GetMovingActorMeshBounds() const;
|
||||
|
||||
/** Aligns the footprint of the object being moved with the placement grid and applies the rotation
|
||||
* and the scale it is going to be placed with. Returns the location the actor has to be moved to.
|
||||
* The pivot is not used for the alignment, because it is not guaranteed to sit at the center (or
|
||||
* at the edge) of the mesh. */
|
||||
FVector SnapFootprintToGrid(const FVector& InLocation, const FVector& TargetScale, const FRotator& TargetRotation, double GridSnap);
|
||||
|
||||
|
||||
public:
|
||||
// Called every frame
|
||||
@@ -115,6 +162,28 @@ public:
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure)
|
||||
FORCEINLINE bool IsGridMovementActive() const { return bGridMovementActive; }
|
||||
|
||||
/** Footprint of the object that is currently being moved, in whole grid cells (X = cells along X,
|
||||
* Y = cells along Y). Handy for showing the size of an object in a build UI. Zero when nothing is
|
||||
* being moved. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Placement")
|
||||
FVector2D GetMovingActorGridFootprint() const;
|
||||
|
||||
/** Amount of whole grid cells a size spans. The footprint is rounded up, so that a mesh always fits
|
||||
* inside the cells it claims, with a small tolerance against the float noise that mesh bounds carry.
|
||||
* The placement and the footprint report both use this, so the two can never disagree. */
|
||||
static double GetGridCellCount(double Size, double GridSnap);
|
||||
|
||||
/** Size of one grid cell for an actor: the base cell size of this component scaled by the whole
|
||||
* number GridScale multiplier of the actor, so 1 keeps the base grid and 3 gives 150 units cells on
|
||||
* a 50 units grid. The multiplier is applied here instead of being trusted from the property, so a
|
||||
* stray zero (the property is writable from Blueprint as well) can not shrink the grid to nothing. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure, Category = "Placement")
|
||||
double GetGridPitchForActor(const ARemovableStaticMeshActor* Actor) const;
|
||||
|
||||
/** As GetGridPitchForActor, but against an explicit base cell size. The actors use this to report
|
||||
* their footprint while the component that owns the base size is not available (in the editor). */
|
||||
static double GetGridPitch(const ARemovableStaticMeshActor* Actor, double BaseGridSnapSize);
|
||||
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure)
|
||||
bool IsRemovingModePossible() const { return bRemovalModePossible; }
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure)
|
||||
|
||||
@@ -154,10 +154,44 @@ void ARemovableStaticMeshActor::Place(bool ReturnToOldLocation, bool bForce)
|
||||
void ARemovableStaticMeshActor::Rotate(bool bPositive)
|
||||
{
|
||||
FRotator CurrentRotation = this->GetActorRotation();
|
||||
CurrentRotation.Yaw += bPositive ? 45.f : -45.f;
|
||||
CurrentRotation.Yaw += bPositive ? 90.f : -90.f;
|
||||
this->SetActorRotation(CurrentRotation);
|
||||
}
|
||||
|
||||
void ARemovableStaticMeshActor::ReportGridFootprint()
|
||||
{
|
||||
// The base cell size is a property of the Move Actors component, which only exists at runtime. While
|
||||
// editing a placed actor the component is missing, so the class default is used, which is the value
|
||||
// the placement uses unless the level overrides it.
|
||||
const double BaseGridSnapSize = MoveableComponent ? MoveableComponent->GridSnapSize : UMoveActorsComponent::DefaultGridSnapSize;
|
||||
const double Pitch = UMoveActorsComponent::GetGridPitch(this, BaseGridSnapSize);
|
||||
|
||||
if (Pitch <= 0.0)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("%s: no grid pitch to report against. Set GridSnapSize on the Move Actors component."), *GetName());
|
||||
return;
|
||||
}
|
||||
|
||||
const UStaticMeshComponent* MeshComponent = GetStaticMeshComponent();
|
||||
if (!MeshComponent || !MeshComponent->GetStaticMesh())
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("%s: no static mesh assigned."), *GetName());
|
||||
return;
|
||||
}
|
||||
|
||||
const FBoxSphereBounds MeshBounds = MeshComponent->CalcBounds(MeshComponent->GetComponentTransform());
|
||||
const FVector Size = MeshBounds.BoxExtent * 2.0;
|
||||
// The same cell rule that the placement uses, so the reported margin is the margin that the object
|
||||
// will actually keep on the grid.
|
||||
const double CellsX = UMoveActorsComponent::GetGridCellCount(Size.X, Pitch);
|
||||
const double CellsY = UMoveActorsComponent::GetGridCellCount(Size.Y, Pitch);
|
||||
const double FillScale = FMath::Max((CellsX * Pitch) / FMath::Max(Size.X, 1.0), (CellsY * Pitch) / FMath::Max(Size.Y, 1.0));
|
||||
|
||||
UE_LOG(LogTemp, Display, TEXT("%s: mesh %.1f x %.1f on a %.1f grid (%d x %.1f base) -> %.0f x %.0f cells (margin %.1f x %.1f, fill scale %.2f)"),
|
||||
*GetName(), Size.X, Size.Y, Pitch, GetGridSizeMultiplier(), BaseGridSnapSize, CellsX, CellsY,
|
||||
CellsX * Pitch - Size.X, CellsY * Pitch - Size.Y, FillScale);
|
||||
}
|
||||
|
||||
void ARemovableStaticMeshActor::ReturnToInventory()
|
||||
{
|
||||
if (InventoryRef)
|
||||
|
||||
@@ -68,8 +68,19 @@ public:
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
float DistanceFromActor;
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
float GridScale;
|
||||
/** Whole number multiplier on the placement grid: the object is placed on cells that are
|
||||
* GridSnapSize * GridScale units, so 1 (the default) keeps the grid the Move Actors component
|
||||
* defines and 3 reserves 150 units blocks on a 50 units grid. Keep it a whole number: a fractional
|
||||
* multiplier puts the object on a lattice of its own that only lines up with everything around it
|
||||
* every now and then, which is exactly what makes placed objects drift. The value is clamped to at
|
||||
* least 1 where it is used, a zero sized cell would mean "no grid at all". */
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (ClampMin = "1", UIMin = "1"))
|
||||
int32 GridScale = 1;
|
||||
|
||||
/** GridScale with its lower bound applied. The property is writable from Blueprint, where ClampMin
|
||||
* does not apply, so the value is sanitised at the point of use instead of being trusted. */
|
||||
UFUNCTION(BlueprintCallable, BlueprintPure)
|
||||
FORCEINLINE int32 GetGridSizeMultiplier() const { return FMath::Max(1, GridScale); }
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite)
|
||||
UMaterialInterface* CanPlaceMaterial;
|
||||
@@ -92,6 +103,12 @@ public:
|
||||
UFUNCTION(BlueprintCallable, Category = "Removable Object")
|
||||
void UpdateOverlapBoxExtent();
|
||||
|
||||
/** Logs how the mesh of this actor relates to the placement grid: its size, the amount of cells it
|
||||
* occupies and the scale that would make it fill those cells. Meshes do not have to be modelled to
|
||||
* the grid, this only makes the margin they keep visible so that near misses stand out. */
|
||||
UFUNCTION(CallInEditor, BlueprintCallable, Category = "Removable Object")
|
||||
void ReportGridFootprint();
|
||||
|
||||
//Used for movable actors to override their box extent for checking collision
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Removable Object")
|
||||
FVector BoxExtentOverride = FVector::ZeroVector;
|
||||
|
||||
Reference in New Issue
Block a user