Gizmo follow, cooker-friendly assets, and proper ground snapping
Gizmo usability: - Refresh the gizmo every tick while simulation is running so it glues to animated / AI-driven / timeline-driven actors instead of freezing at the pose they had when selected - Clear the pawn's active sub-element drag state on FinishPointPlacement / CancelPointPlacement — previously, after clicking Done on a spline, the gizmo jumped to the center cube but the internal ActiveSplinePointIndex stayed on the last clicked CP, so dragging the gizmo moved that CP instead of the whole actor - Expose UpdateGizmo() publicly on the SelectionManager so the PlayerController Tick can drive it Package cooking (fix gray gizmos / missing outline in packaged builds): - Convert every runtime LoadObject<> soft-string path into a UPROPERTY TObjectPtr<> filled via ConstructorHelpers::FObjectFinder at CDO construction time. Hard refs are picked up by the cooker so plugin Content assets (M_PS_Editor_Gizmo, M_PS_Editor_SelectionOutline, M_PS_Editor_SplineLine, and the engine basic shapes the spline actor uses) are reliably staged - Touches APS_Editor_Gizmo, APS_Editor_Pawn (outline post-process material), APS_Editor_SplineActor (gizmo material, spline line material, center cube mesh, handle sphere mesh) Ground placement (PS_Editor_GroundSnap): - New PS_Editor_GroundSnap utility centralising placement logic for catalogue spawns, End-key snap-to-ground, and spline CP placement - ProjectCameraRayToGround: aim down from a point in front of the camera when the camera ray misses (no more "looking at sky spawns object in mid-air"); classify the primary hit by normal to re-cast from below a ceiling / step back from a wall so the object lands on the floor underneath / at the foot of the surface rather than embedded in it - SnapActorToGround: start the downward ray from INSIDE the actor's volume (pivot + small buffer), ignoring the actor itself — avoids the "raised a bit high, pressed End, teleports to upper story" bug where starting above the actor's bbox could cross the current room's ceiling and find the upper floor as the first floor-like surface - Uses GetActorBounds(bOnlyCollidingComponents=true) with a fallback so characters snap from their capsule bounds rather than bbox including non-colliding meshes / attached weapons / child actors that used to lift pawns into the air - Multi-hit trace + normal.Z >= 0.3 filter skips walls, ceilings, and backfaces when finding the first true floor below Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -2,6 +2,8 @@
|
||||
#include "Camera/CameraComponent.h"
|
||||
#include "GameFramework/SpringArmComponent.h"
|
||||
#include "Components/PostProcessComponent.h"
|
||||
#include "Materials/MaterialInterface.h"
|
||||
#include "UObject/ConstructorHelpers.h"
|
||||
#include "EnhancedInputComponent.h"
|
||||
#include "EnhancedInputSubsystems.h"
|
||||
#include "InputAction.h"
|
||||
@@ -10,6 +12,7 @@
|
||||
#include "InputTriggers.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
#include "PS_Editor_SelectionManager.h"
|
||||
#include "PS_Editor_GroundSnap.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_Gizmo.h"
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
@@ -43,24 +46,27 @@ APS_Editor_Pawn::APS_Editor_Pawn()
|
||||
OutlinePostProcess = CreateDefaultSubobject<UPostProcessComponent>(TEXT("OutlinePostProcess"));
|
||||
OutlinePostProcess->SetupAttachment(RootScene);
|
||||
OutlinePostProcess->bUnbound = true; // Apply to entire scene
|
||||
|
||||
// Hard-ref the outline material at CDO-construction time so the cooker follows it into
|
||||
// packaged builds. Replaces the old runtime LoadObject<> soft-string path.
|
||||
static ConstructorHelpers::FObjectFinder<UMaterialInterface> OutlineMatFinder(
|
||||
TEXT("/PS_Editor/M_PS_Editor_SelectionOutline.M_PS_Editor_SelectionOutline"));
|
||||
if (OutlineMatFinder.Succeeded())
|
||||
{
|
||||
OutlineMaterial = OutlineMatFinder.Object;
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// Load outline post-process material
|
||||
UMaterialInterface* OutlineMat = LoadObject<UMaterialInterface>(
|
||||
nullptr, TEXT("/PS_Editor/M_PS_Editor_SelectionOutline.M_PS_Editor_SelectionOutline"));
|
||||
if (OutlineMat && OutlinePostProcess)
|
||||
// OutlineMaterial is resolved in the ctor via ConstructorHelpers (hard cook ref).
|
||||
if (OutlineMaterial && OutlinePostProcess)
|
||||
{
|
||||
OutlinePostProcess->Settings.WeightedBlendables.Array.Add(FWeightedBlendable(1.0f, OutlineMat));
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: Selection outline material loaded"));
|
||||
OutlinePostProcess->Settings.WeightedBlendables.Array.Add(FWeightedBlendable(1.0f, OutlineMaterial));
|
||||
}
|
||||
|
||||
// TODO: Spline occluded PP material — disabled for now, depth comparison unreliable
|
||||
// Consider translucent second-pass approach instead of post-process
|
||||
else
|
||||
else if (!OutlineMaterial)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: Outline material /PS_Editor/M_PS_Editor_SelectionOutline not found. "
|
||||
"Create a Post Process material in Plugins/PS_Editor/Content/"));
|
||||
@@ -769,22 +775,18 @@ void APS_Editor_Pawn::HandleLeftClickStarted(const FInputActionValue& Value)
|
||||
{
|
||||
if (IPS_Editor_SplineEditable* Spline = Cast<IPS_Editor_SplineEditable>(EdPC3->GetActivePlacementActor()))
|
||||
{
|
||||
// Raycast to get world click position
|
||||
FHitResult AddHit;
|
||||
FCollisionQueryParams AddParams;
|
||||
AddParams.AddIgnoredActor(this);
|
||||
AddParams.AddIgnoredActor(Cast<AActor>(Spline));
|
||||
const FVector AddEnd = RayOrigin + RayDir * 100000.0f;
|
||||
// Project the click ray onto ground. Uses the shared helper so that a click
|
||||
// pointing at the sky still lands the control point on the ground below the
|
||||
// camera's forward vector (instead of floating 1000 units ahead).
|
||||
TArray<AActor*> Ignored;
|
||||
Ignored.Add(this);
|
||||
if (AActor* SplineActor = Cast<AActor>(Spline)) Ignored.Add(SplineActor);
|
||||
|
||||
FVector ClickPos;
|
||||
if (GetWorld()->LineTraceSingleByChannel(AddHit, RayOrigin, AddEnd, ECC_Camera, AddParams))
|
||||
{
|
||||
ClickPos = AddHit.ImpactPoint + FVector(0.0f, 0.0f, 5.0f);
|
||||
}
|
||||
else
|
||||
{
|
||||
ClickPos = RayOrigin + RayDir * 1000.0f;
|
||||
}
|
||||
FVector ClickPos = PS_Editor_GroundSnap::ProjectCameraRayToGround(
|
||||
GetWorld(), RayOrigin, RayDir, Ignored);
|
||||
// Lift the CP slightly above the surface so the tube mesh renders cleanly
|
||||
// (otherwise Z-fighting with the ground).
|
||||
ClickPos.Z += 5.0f;
|
||||
|
||||
TArray<FVector> OldPoints = Spline->GetAllPointLocations();
|
||||
Spline->AddPoint(ClickPos);
|
||||
@@ -1023,6 +1025,12 @@ void APS_Editor_Pawn::HandleScaleMode(const FInputActionValue& Value)
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::ClearActiveSubElementDrag()
|
||||
{
|
||||
ActiveSplinePointIndex = -1;
|
||||
DraggedSplineActor = nullptr;
|
||||
}
|
||||
|
||||
void APS_Editor_Pawn::HandleDelete(const FInputActionValue& Value)
|
||||
{
|
||||
if (CameraMode != EPS_Editor_CameraMode::Idle) return;
|
||||
|
||||
@@ -11,6 +11,7 @@ class USpringArmComponent;
|
||||
class UPostProcessComponent;
|
||||
class UInputAction;
|
||||
class UInputMappingContext;
|
||||
class UMaterialInterface;
|
||||
|
||||
DECLARE_DELEGATE_TwoParams(FPS_Editor_OnClick, FVector2D /*ScreenPos*/, bool /*bAdditive*/);
|
||||
|
||||
@@ -68,6 +69,11 @@ public:
|
||||
/** Currently selected spline point index (-1 = none). Used by UI for Delete Point button. */
|
||||
int32 ActiveSplinePointIndex = -1;
|
||||
|
||||
/** Clear any active sub-element drag state (spline point / ChildMovable index + target actor).
|
||||
* Call this when leaving placement / edit mode so a subsequent gizmo drag on the newly
|
||||
* selected parent actor moves the whole actor instead of the last clicked point. */
|
||||
void ClearActiveSubElementDrag();
|
||||
|
||||
protected:
|
||||
virtual void BeginPlay() override;
|
||||
|
||||
@@ -85,6 +91,12 @@ protected:
|
||||
UPROPERTY(VisibleAnywhere, BlueprintReadOnly, Category = "Camera")
|
||||
TObjectPtr<UPostProcessComponent> OutlinePostProcess;
|
||||
|
||||
/** Selection outline post-process material (from /PS_Editor/M_PS_Editor_SelectionOutline).
|
||||
* Hard-ref so the cooker picks it up in packaged builds. Resolved via ConstructorHelpers
|
||||
* in the ctor. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UMaterialInterface> OutlineMaterial;
|
||||
|
||||
// ---- Camera Settings ----
|
||||
|
||||
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly")
|
||||
|
||||
@@ -186,6 +186,17 @@ void APS_Editor_PlayerController::Tick(float DeltaTime)
|
||||
{
|
||||
SpawnManager->TickEditorMode(DeltaTime);
|
||||
}
|
||||
|
||||
// Keep the gizmo glued to its selection every frame while simulating — otherwise
|
||||
// animated actors (AI walking, timeline-driven properties, ragdoll) leave the gizmo
|
||||
// behind at the pose it had when the actor was last selected / dragged. Outside of
|
||||
// simulation the selection doesn't move on its own so the normal mutation-driven
|
||||
// UpdateGizmo calls (click / drag / undo) are enough — and skipping this avoids any
|
||||
// feedback with the pawn's in-progress gizmo drag.
|
||||
if (bSimulating && SelectionManager && SelectionManager->IsAnythingSelected())
|
||||
{
|
||||
SelectionManager->UpdateGizmo();
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_PlayerController::OnPossess(APawn* InPawn)
|
||||
@@ -554,6 +565,14 @@ void APS_Editor_PlayerController::FinishPointPlacement()
|
||||
IPS_Editor_ChildMovable::Execute_ClearChildHighlight(PlacementActor);
|
||||
}
|
||||
|
||||
// Clear the pawn's sub-element drag state (last clicked spline point / child index).
|
||||
// Without this, grabbing the gizmo right after Done would move the last clicked point
|
||||
// instead of the whole actor, because the drag logic keys on ActiveSplinePointIndex.
|
||||
if (APS_Editor_Pawn* EditorPawn = Cast<APS_Editor_Pawn>(GetPawn()))
|
||||
{
|
||||
EditorPawn->ClearActiveSubElementDrag();
|
||||
}
|
||||
|
||||
if (PlacementActor && SelectionManager)
|
||||
{
|
||||
SelectionManager->ClearSelection();
|
||||
|
||||
@@ -26,6 +26,15 @@ APS_Editor_Gizmo::APS_Editor_Gizmo()
|
||||
{
|
||||
PrimaryActorTick.bCanEverTick = true;
|
||||
|
||||
// Hard-ref the gizmo base material at CDO-construction time so the cooker follows it
|
||||
// into packaged builds. Replaces the old runtime LoadObject<> soft-string path.
|
||||
static ConstructorHelpers::FObjectFinder<UMaterialInterface> GizmoBaseMatFinder(
|
||||
TEXT("/PS_Editor/M_PS_Editor_Gizmo.M_PS_Editor_Gizmo"));
|
||||
if (GizmoBaseMatFinder.Succeeded())
|
||||
{
|
||||
GizmoBaseMaterial = GizmoBaseMatFinder.Object;
|
||||
}
|
||||
|
||||
GizmoRoot = CreateDefaultSubobject<USceneComponent>(TEXT("GizmoRoot"));
|
||||
SetRootComponent(GizmoRoot);
|
||||
|
||||
@@ -92,9 +101,8 @@ void APS_Editor_Gizmo::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// Load gizmo material
|
||||
UMaterialInterface* GizmoBaseMat = LoadObject<UMaterialInterface>(
|
||||
nullptr, TEXT("/PS_Editor/M_PS_Editor_Gizmo.M_PS_Editor_Gizmo"));
|
||||
// GizmoBaseMaterial is resolved in the ctor via ConstructorHelpers (hard cook ref).
|
||||
UMaterialInterface* GizmoBaseMat = GizmoBaseMaterial.Get();
|
||||
|
||||
auto MakeColorMat = [&](const FLinearColor& Color) -> UMaterialInstanceDynamic*
|
||||
{
|
||||
|
||||
@@ -8,6 +8,7 @@
|
||||
class UStaticMeshComponent;
|
||||
class UProceduralMeshComponent;
|
||||
class UMaterialInstanceDynamic;
|
||||
class UMaterialInterface;
|
||||
|
||||
/**
|
||||
* Custom runtime transform gizmo for PS_Editor.
|
||||
@@ -109,6 +110,12 @@ private:
|
||||
TObjectPtr<UStaticMeshComponent> ScaleCube_Center;
|
||||
|
||||
// ---- Materials ----
|
||||
|
||||
/** Base gizmo material (from /PS_Editor/M_PS_Editor_Gizmo). Hard-ref so the cooker
|
||||
* picks it up in packaged builds. Resolved via ConstructorHelpers in the ctor. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UMaterialInterface> GizmoBaseMaterial;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UMaterialInstanceDynamic> Mat_X;
|
||||
UPROPERTY()
|
||||
|
||||
@@ -0,0 +1,195 @@
|
||||
#include "PS_Editor_GroundSnap.h"
|
||||
#include "PS_Editor_SpawnableComponent.h"
|
||||
#include "Engine/World.h"
|
||||
#include "GameFramework/Actor.h"
|
||||
#include "CollisionQueryParams.h"
|
||||
|
||||
namespace PS_Editor_GroundSnap
|
||||
{
|
||||
// Normal.Z threshold that classifies a hit surface as "floor-like" (roughly walkable
|
||||
// slope — cos(72°) ≈ 0.3). Below this, we treat the surface as a wall / ceiling /
|
||||
// overhang and keep searching.
|
||||
static constexpr float GFloorNormalThreshold = 0.3f;
|
||||
|
||||
/** Single downward trace that returns the first floor-like hit. Returns false if
|
||||
* nothing below qualifies (all walls, ceiling-like geometry, etc.). */
|
||||
static bool FindFloorBelow(UWorld* World, const FVector& Above, float Distance,
|
||||
const FCollisionQueryParams& Params, FVector& OutImpact)
|
||||
{
|
||||
TArray<FHitResult> Hits;
|
||||
if (!World->LineTraceMultiByChannel(Hits, Above, Above - FVector::UpVector * Distance,
|
||||
ECC_Visibility, Params))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
for (const FHitResult& H : Hits)
|
||||
{
|
||||
if (H.ImpactNormal.Z >= GFloorNormalThreshold)
|
||||
{
|
||||
OutImpact = H.ImpactPoint;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
FVector ProjectCameraRayToGround(UWorld* World,
|
||||
const FVector& Start,
|
||||
const FVector& Direction,
|
||||
const TArray<AActor*>& IgnoreActors,
|
||||
float MaxCameraDistance,
|
||||
float DefaultDistance,
|
||||
float DownTraceDistance)
|
||||
{
|
||||
if (!World)
|
||||
{
|
||||
return Start + Direction * DefaultDistance;
|
||||
}
|
||||
|
||||
FCollisionQueryParams Params(SCENE_QUERY_STAT(PS_Editor_GroundSnap), false);
|
||||
for (AActor* A : IgnoreActors)
|
||||
{
|
||||
if (A) Params.AddIgnoredActor(A);
|
||||
}
|
||||
|
||||
// 1) Primary camera-aligned trace.
|
||||
FHitResult Hit;
|
||||
if (World->LineTraceSingleByChannel(Hit, Start,
|
||||
Start + Direction.GetSafeNormal() * MaxCameraDistance,
|
||||
ECC_Visibility, Params))
|
||||
{
|
||||
const float NormalZ = Hit.ImpactNormal.Z;
|
||||
|
||||
// Ceiling / overhang: normal points DOWN. Re-cast downward from a point just
|
||||
// BELOW the ceiling impact (not on the surface itself — avoids re-hitting the
|
||||
// backface due to floating-point slop).
|
||||
if (NormalZ <= -0.3f)
|
||||
{
|
||||
const FVector StartBelowCeiling = Hit.ImpactPoint - FVector::UpVector * 2.0f;
|
||||
FVector FloorImpact;
|
||||
if (FindFloorBelow(World, StartBelowCeiling, DownTraceDistance, Params, FloorImpact))
|
||||
{
|
||||
return FloorImpact;
|
||||
}
|
||||
return Hit.ImpactPoint;
|
||||
}
|
||||
|
||||
// Wall: roughly horizontal normal. Step BACK along the wall normal (away from
|
||||
// the surface) by a small distance so the placed object doesn't end up with its
|
||||
// bbox half-buried in the wall, then cast down to find the floor at the foot
|
||||
// of the wall. Without this, clicking on a wall drops the object inside it.
|
||||
if (NormalZ < 0.3f) // not floor-like either
|
||||
{
|
||||
constexpr float WallStandoff = 30.0f;
|
||||
const FVector StartAwayFromWall = Hit.ImpactPoint + Hit.ImpactNormal * WallStandoff;
|
||||
FVector FloorImpact;
|
||||
if (FindFloorBelow(World, StartAwayFromWall, DownTraceDistance, Params, FloorImpact))
|
||||
{
|
||||
return FloorImpact;
|
||||
}
|
||||
// No floor below — at least return the stood-off point so we're not inside
|
||||
// the wall.
|
||||
return StartAwayFromWall;
|
||||
}
|
||||
|
||||
// Floor-like: use impact directly.
|
||||
return Hit.ImpactPoint;
|
||||
}
|
||||
|
||||
// 2) Camera missed. Aim DOWN from a fallback point in front of the camera so the
|
||||
// object still lands on ground instead of hovering in the sky.
|
||||
const FVector FallbackTarget = Start + Direction.GetSafeNormal() * DefaultDistance;
|
||||
FVector FloorImpact;
|
||||
if (FindFloorBelow(World, FallbackTarget, DownTraceDistance, Params, FloorImpact))
|
||||
{
|
||||
return FloorImpact;
|
||||
}
|
||||
|
||||
// 3) Nothing at all — give up and return the in-front point.
|
||||
return FallbackTarget;
|
||||
}
|
||||
|
||||
bool SnapActorToGround(AActor* Actor,
|
||||
FVector& OutLocation,
|
||||
float OverheadRaise,
|
||||
float MaxDistance,
|
||||
const TArray<AActor*>& ExtraIgnored)
|
||||
{
|
||||
if (!Actor || !Actor->GetWorld())
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
// Measure the bbox bottom relative to the pivot. Use COLLIDING components only so
|
||||
// a Character's capsule drives the calculation — not its skeletal mesh (often with
|
||||
// collision disabled) or attached weapons / child actors whose meshes may extend
|
||||
// below the capsule and artificially lift the pawn by that overflow.
|
||||
// Falls back to the full actor bounds if no colliding component is found (e.g. a
|
||||
// pure-mesh prop without collision — rare but possible).
|
||||
FVector BoundsOrigin, BoundsExtent;
|
||||
Actor->GetActorBounds(/*bOnlyCollidingComponents*/ true, BoundsOrigin, BoundsExtent);
|
||||
if (BoundsExtent.IsNearlyZero())
|
||||
{
|
||||
Actor->GetActorBounds(false, BoundsOrigin, BoundsExtent);
|
||||
}
|
||||
const float ActorZ = Actor->GetActorLocation().Z;
|
||||
const float BBoxBottomZ = BoundsOrigin.Z - BoundsExtent.Z;
|
||||
const float BBoxTopZ = BoundsOrigin.Z + BoundsExtent.Z;
|
||||
const float PivotAboveBottom = ActorZ - BBoxBottomZ;
|
||||
|
||||
float ExtraOffset = 0.0f;
|
||||
if (UPS_Editor_SpawnableComponent* SpawnComp = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>())
|
||||
{
|
||||
ExtraOffset = SpawnComp->GroundOffset;
|
||||
}
|
||||
|
||||
// Start INSIDE the actor (at its pivot + a small buffer), not above its bbox top.
|
||||
// Why: if we started above the bbox top and the actor was raised even slightly into
|
||||
// a ceiling, the ray would cross the ceiling and the FIRST floor-like hit (filtered
|
||||
// by normal.Z >= 0.3) would be the CEILING'S TOP FACE — i.e. the floor of the
|
||||
// upper story. That made "End" teleport the actor up to the next story when the
|
||||
// user only nudged it a bit too high.
|
||||
//
|
||||
// Starting at the pivot keeps the ray origin within the actor's own volume (pivot
|
||||
// is always inside or at the edge of the bbox for normal meshes / characters). The
|
||||
// actor is in the ignore list, so the trace passes straight through its own body
|
||||
// and finds the first floor-like surface UNDERNEATH the actor — i.e. the floor of
|
||||
// its current room.
|
||||
//
|
||||
// OverheadRaise is unused now (kept for API compat); the 2.0f buffer just avoids
|
||||
// a degenerate "ray starts exactly on ground surface" edge case when the pivot
|
||||
// coincides with the ground.
|
||||
(void)BBoxTopZ; (void)OverheadRaise;
|
||||
const FVector Start = Actor->GetActorLocation() + FVector(0.f, 0.f, 2.0f);
|
||||
const FVector End = Start - FVector::UpVector * MaxDistance;
|
||||
|
||||
FCollisionQueryParams Params(SCENE_QUERY_STAT(PS_Editor_SnapActorToGround), false);
|
||||
Params.AddIgnoredActor(Actor);
|
||||
for (AActor* A : ExtraIgnored)
|
||||
{
|
||||
if (A) Params.AddIgnoredActor(A);
|
||||
}
|
||||
|
||||
// Multi-hit trace + filter by normal so we only snap to floor-like surfaces.
|
||||
// Accepts slopes up to ~72 degrees (normal.Z > 0.3). Rejects walls, ceilings,
|
||||
// overhangs, and any backface we might cross on the way down.
|
||||
TArray<FHitResult> Hits;
|
||||
if (!Actor->GetWorld()->LineTraceMultiByChannel(Hits, Start, End, ECC_Visibility, Params))
|
||||
{
|
||||
return false;
|
||||
}
|
||||
|
||||
constexpr float FloorNormalThreshold = 0.3f; // cos(~72 deg)
|
||||
for (const FHitResult& H : Hits)
|
||||
{
|
||||
if (H.ImpactNormal.Z >= FloorNormalThreshold)
|
||||
{
|
||||
OutLocation = Actor->GetActorLocation();
|
||||
OutLocation.Z = H.ImpactPoint.Z + PivotAboveBottom + ExtraOffset;
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Trace hit stuff but nothing floor-like — don't move the actor.
|
||||
return false;
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,67 @@
|
||||
#pragma once
|
||||
|
||||
#include "CoreMinimal.h"
|
||||
|
||||
class AActor;
|
||||
class UWorld;
|
||||
|
||||
/**
|
||||
* Ground-placement helpers used across catalogue spawn, End-key snap, and spline point
|
||||
* placement. The goal is that every object the user drops / snaps ends up on the ground
|
||||
* by default, even when the camera is pointing at the sky.
|
||||
*/
|
||||
namespace PS_Editor_GroundSnap
|
||||
{
|
||||
/**
|
||||
* Project a camera-style ray onto ground.
|
||||
*
|
||||
* Sequence:
|
||||
* 1. Line-trace from Start along Direction for up to MaxCameraDistance.
|
||||
* - Hit a surface whose normal points UP / sideways -> return ImpactPoint.
|
||||
* - Hit a surface whose normal points DOWN (ceiling, overhang) -> re-cast straight
|
||||
* down from the impact point so the object lands on the floor underneath the
|
||||
* overhead surface instead of glued to the ceiling.
|
||||
* 2. Camera ray missed entirely (user aiming at the sky). Fall back to a point at
|
||||
* DefaultDistance in front of the camera, then cast DOWN from there for
|
||||
* DownTraceDistance. If THAT hits, return the downward impact.
|
||||
* 3. Still nothing (infinite drop) -> return the in-front fallback unmodified so the
|
||||
* caller at least gets a point in world space instead of a zero vector.
|
||||
*
|
||||
* @param IgnoreActors Actors the line trace must skip (typically the player pawn and,
|
||||
* for re-snapping an existing actor, the actor itself — otherwise
|
||||
* the trace hits its own mesh and nothing moves).
|
||||
*/
|
||||
PS_EDITOR_API FVector ProjectCameraRayToGround(UWorld* World,
|
||||
const FVector& Start,
|
||||
const FVector& Direction,
|
||||
const TArray<AActor*>& IgnoreActors = {},
|
||||
float MaxCameraDistance = 20000.0f,
|
||||
float DefaultDistance = 1500.0f,
|
||||
float DownTraceDistance = 50000.0f);
|
||||
|
||||
/**
|
||||
* Compute a world position such that Actor's bounding-box bottom lands on the ground
|
||||
* directly under its current XY position. Optionally adds a SpawnableComponent's
|
||||
* GroundOffset for props that look better floating slightly (rugs, characters, etc.).
|
||||
*
|
||||
* Properly handles pivots not at bbox center (e.g. characters with pivot at feet) by
|
||||
* measuring the pivot-to-bbox-bottom delta from GetActorBounds.
|
||||
*
|
||||
* The downward ray starts AT the actor's pivot (+ small buffer), not above its bbox.
|
||||
* This keeps the trace origin within the current room (the pivot is always inside or
|
||||
* on the edge of the actor's own volume) so the snap lands on the floor the actor is
|
||||
* currently in, even when the actor has been nudged slightly into a ceiling. A multi-
|
||||
* hit trace + a normal.Z >= 0.3 floor filter skips walls, ceilings, and backfaces
|
||||
* encountered on the way down.
|
||||
*
|
||||
* OverheadRaise is kept for API compatibility but is no longer used — starting above
|
||||
* the bbox was exactly what caused "End teleports to the upper story" bugs.
|
||||
*
|
||||
* @return true + OutLocation filled when ground was found; false when nothing is below.
|
||||
*/
|
||||
PS_EDITOR_API bool SnapActorToGround(AActor* Actor,
|
||||
FVector& OutLocation,
|
||||
float OverheadRaise = 50.0f,
|
||||
float MaxDistance = 50000.0f,
|
||||
const TArray<AActor*>& ExtraIgnored = {});
|
||||
}
|
||||
@@ -3,6 +3,7 @@
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_SpawnManager.h"
|
||||
#include "PS_Editor_GroundSnap.h"
|
||||
#include "PS_Editor_EditableComponent.h"
|
||||
#include "PS_Editor_SpawnableComponent.h"
|
||||
#include "PS_Editor_SplineActor.h"
|
||||
@@ -448,6 +449,11 @@ void UPS_Editor_SelectionManager::SnapSelectionToGround()
|
||||
TSharedPtr<FPS_Editor_TransformAction> UndoAction = MakeShared<FPS_Editor_TransformAction>();
|
||||
UndoAction->Description = FString::Printf(TEXT("Snap to ground %d actor(s)"), SelectedActors.Num());
|
||||
|
||||
// Extra ignores shared across every actor in the batch.
|
||||
TArray<AActor*> ExtraIgnored;
|
||||
if (APawn* Pwn = PC->GetPawn()) ExtraIgnored.Add(Pwn);
|
||||
if (Gizmo) ExtraIgnored.Add(Gizmo);
|
||||
|
||||
for (const TWeakObjectPtr<AActor>& Weak : SelectedActors)
|
||||
{
|
||||
AActor* Actor = Weak.Get();
|
||||
@@ -456,36 +462,11 @@ void UPS_Editor_SelectionManager::SnapSelectionToGround()
|
||||
UndoAction->Actors.Add(Actor);
|
||||
UndoAction->OldTransforms.Add(Actor->GetActorTransform());
|
||||
|
||||
FVector Origin, Extent;
|
||||
Actor->GetActorBounds(false, Origin, Extent);
|
||||
float BottomOffset = Extent.Z;
|
||||
|
||||
// Add custom ground offset from SpawnableComponent if available
|
||||
if (UPS_Editor_SpawnableComponent* SpawnComp = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>())
|
||||
FVector NewLocation;
|
||||
if (PS_Editor_GroundSnap::SnapActorToGround(Actor, NewLocation,
|
||||
/*OverheadRaise*/ 50.0f, /*MaxDistance*/ 50000.0f, ExtraIgnored))
|
||||
{
|
||||
BottomOffset += SpawnComp->GroundOffset;
|
||||
}
|
||||
|
||||
const FVector Start = Actor->GetActorLocation();
|
||||
const FVector End = Start - FVector::UpVector * 100000.0f;
|
||||
|
||||
FHitResult Hit;
|
||||
FCollisionQueryParams Params;
|
||||
Params.AddIgnoredActor(Actor);
|
||||
Params.AddIgnoredActor(PC->GetPawn());
|
||||
if (Gizmo) Params.AddIgnoredActor(Gizmo);
|
||||
|
||||
if (Actor->GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Camera, Params))
|
||||
{
|
||||
FVector NewLocation = Hit.ImpactPoint;
|
||||
NewLocation.Z += BottomOffset;
|
||||
Actor->SetActorLocation(NewLocation);
|
||||
|
||||
UE_LOG(LogTemp, Log, TEXT("PS_Editor: SnapToGround '%s' -> Hit '%s' (%s) at Z=%.1f, BottomOffset=%.1f, NewZ=%.1f"),
|
||||
*Actor->GetName(),
|
||||
*Hit.GetActor()->GetName(),
|
||||
*Hit.GetComponent()->GetName(),
|
||||
Hit.ImpactPoint.Z, BottomOffset, NewLocation.Z);
|
||||
}
|
||||
else
|
||||
{
|
||||
|
||||
@@ -106,6 +106,11 @@ public:
|
||||
|
||||
FPS_Editor_OnSelectionChanged OnSelectionChanged;
|
||||
|
||||
/** Update gizmo visibility, position, orientation, and axis constraints from the
|
||||
* current selection. Called on every selection mutation, and per-tick during
|
||||
* simulation so the gizmo tracks animated pawns. */
|
||||
void UpdateGizmo();
|
||||
|
||||
private:
|
||||
UPROPERTY()
|
||||
TWeakObjectPtr<APlayerController> OwnerPC;
|
||||
@@ -127,7 +132,4 @@ private:
|
||||
|
||||
/** Clean up any stale weak pointers in the selection array. */
|
||||
void CleanupStaleReferences();
|
||||
|
||||
/** Update gizmo visibility and position based on current selection. */
|
||||
void UpdateGizmo();
|
||||
};
|
||||
|
||||
@@ -5,6 +5,7 @@
|
||||
#include "PS_Editor_UndoManager.h"
|
||||
#include "PS_Editor_PlayerController.h"
|
||||
#include "PS_Editor_TimelineSubsystem.h"
|
||||
#include "PS_Editor_GroundSnap.h"
|
||||
#include "AIController.h"
|
||||
#include "BrainComponent.h"
|
||||
#include "GameFramework/PlayerController.h"
|
||||
@@ -159,27 +160,19 @@ AActor* UPS_Editor_SpawnManager::SpawnFromCatalogAtLocation(int32 Index, FVector
|
||||
SpawnedActors.Add(SpawnedActor);
|
||||
RegisterSpawnedActorId(PC->GetWorld(), SpawnedActor);
|
||||
|
||||
// Auto-snap to ground if configured
|
||||
// Auto-snap to ground if configured — uses the shared helper so it handles pivot
|
||||
// positions (characters with pivot at feet) + ignores the fresh actor itself.
|
||||
UPS_Editor_SpawnableComponent* SpawnComp = SpawnedActor->FindComponentByClass<UPS_Editor_SpawnableComponent>();
|
||||
if (SpawnComp && SpawnComp->bAutoSnapToGround && PC->GetWorld())
|
||||
{
|
||||
FVector Origin, Extent;
|
||||
SpawnedActor->GetActorBounds(false, Origin, Extent);
|
||||
float BottomOffset = Extent.Z + SpawnComp->GroundOffset;
|
||||
TArray<AActor*> ExtraIgnored;
|
||||
if (APawn* Pwn = PC->GetPawn()) ExtraIgnored.Add(Pwn);
|
||||
|
||||
const FVector Start = SpawnedActor->GetActorLocation();
|
||||
const FVector End = Start - FVector::UpVector * 100000.0f;
|
||||
|
||||
FHitResult Hit;
|
||||
FCollisionQueryParams SnapParams;
|
||||
SnapParams.AddIgnoredActor(SpawnedActor);
|
||||
SnapParams.AddIgnoredActor(PC->GetPawn());
|
||||
|
||||
if (PC->GetWorld()->LineTraceSingleByChannel(Hit, Start, End, ECC_Camera, SnapParams))
|
||||
FVector SnappedLoc;
|
||||
if (PS_Editor_GroundSnap::SnapActorToGround(SpawnedActor, SnappedLoc,
|
||||
/*OverheadRaise*/ 50.0f, /*MaxDistance*/ 50000.0f, ExtraIgnored))
|
||||
{
|
||||
FVector NewLoc = Hit.ImpactPoint;
|
||||
NewLoc.Z += BottomOffset;
|
||||
SpawnedActor->SetActorLocation(NewLoc);
|
||||
SpawnedActor->SetActorLocation(SnappedLoc);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -273,23 +266,14 @@ FVector UPS_Editor_SpawnManager::ComputeSpawnLocation() const
|
||||
FRotator CameraRotation;
|
||||
PC->GetPlayerViewPoint(CameraLocation, CameraRotation);
|
||||
|
||||
const FVector Direction = CameraRotation.Vector();
|
||||
const float MaxDistance = 5000.0f;
|
||||
const float DefaultDistance = 500.0f;
|
||||
const float SurfaceOffset = 5.0f;
|
||||
// Unified placement: camera ray first, and if the camera is pointing at the sky / empty
|
||||
// space, fall back to a downward trace from a point in front of the camera so the spawn
|
||||
// still lands on ground. Avoids the "aim up, object hovers in the sky" bug.
|
||||
TArray<AActor*> Ignored;
|
||||
if (APawn* Pwn = PC->GetPawn()) Ignored.Add(Pwn);
|
||||
|
||||
FHitResult Hit;
|
||||
FCollisionQueryParams Params;
|
||||
Params.AddIgnoredActor(PC->GetPawn());
|
||||
|
||||
if (PC->GetWorld()->LineTraceSingleByChannel(Hit, CameraLocation, CameraLocation + Direction * MaxDistance, ECC_Visibility, Params))
|
||||
{
|
||||
// Hit something — place at impact point, slightly offset along the surface normal
|
||||
return Hit.ImpactPoint + Hit.ImpactNormal * SurfaceOffset;
|
||||
}
|
||||
|
||||
// Nothing hit — fallback to default distance
|
||||
return CameraLocation + Direction * DefaultDistance;
|
||||
return PS_Editor_GroundSnap::ProjectCameraRayToGround(PC->GetWorld(), CameraLocation,
|
||||
CameraRotation.Vector(), Ignored);
|
||||
}
|
||||
|
||||
AActor* UPS_Editor_SpawnManager::SpawnActorDirect(UClass* ActorClass, FVector Location, FRotator Rotation, FVector Scale)
|
||||
|
||||
@@ -5,7 +5,9 @@
|
||||
#include "ProceduralMeshComponent.h"
|
||||
#include "Components/StaticMeshComponent.h"
|
||||
#include "Engine/StaticMesh.h"
|
||||
#include "Materials/MaterialInterface.h"
|
||||
#include "Materials/MaterialInstanceDynamic.h"
|
||||
#include "UObject/ConstructorHelpers.h"
|
||||
|
||||
APS_Editor_SplineActor::APS_Editor_SplineActor()
|
||||
{
|
||||
@@ -50,25 +52,46 @@ APS_Editor_SplineActor::APS_Editor_SplineActor()
|
||||
EditableComp->EditableProperties.Add({FName(TEXT("SplineColor"))});
|
||||
EditableComp->EditableProperties.Add({FName(TEXT("SplineSelectedColor"))});
|
||||
EditableComp->EditableProperties.Add({FName(TEXT("SplineTag"))});
|
||||
|
||||
// Hard-ref plugin + engine assets at CDO-construction time so the cooker follows them
|
||||
// into packaged builds. Replaces the old runtime LoadObject<> soft-string paths.
|
||||
static ConstructorHelpers::FObjectFinder<UMaterialInterface> GizmoBaseMatFinder(
|
||||
TEXT("/PS_Editor/M_PS_Editor_Gizmo.M_PS_Editor_Gizmo"));
|
||||
if (GizmoBaseMatFinder.Succeeded())
|
||||
{
|
||||
GizmoBaseMaterial = GizmoBaseMatFinder.Object;
|
||||
}
|
||||
|
||||
static ConstructorHelpers::FObjectFinder<UMaterialInterface> SplineLineMatFinder(
|
||||
TEXT("/PS_Editor/M_PS_Editor_SplineLine.M_PS_Editor_SplineLine"));
|
||||
if (SplineLineMatFinder.Succeeded())
|
||||
{
|
||||
SplineLineMaterial = SplineLineMatFinder.Object;
|
||||
}
|
||||
|
||||
static ConstructorHelpers::FObjectFinder<UStaticMesh> CubeFinder(
|
||||
TEXT("/Engine/BasicShapes/Cube.Cube"));
|
||||
if (CubeFinder.Succeeded())
|
||||
{
|
||||
CenterCubeMesh = CubeFinder.Object;
|
||||
}
|
||||
|
||||
static ConstructorHelpers::FObjectFinder<UStaticMesh> SphereFinder(
|
||||
TEXT("/Engine/BasicShapes/Sphere.Sphere"));
|
||||
if (SphereFinder.Succeeded())
|
||||
{
|
||||
HandleSphereMesh = SphereFinder.Object;
|
||||
}
|
||||
}
|
||||
|
||||
void APS_Editor_SplineActor::BeginPlay()
|
||||
{
|
||||
Super::BeginPlay();
|
||||
|
||||
// Gizmo material (no depth test, renders on top) for handles and selected state
|
||||
UMaterialInterface* GizmoMat = LoadObject<UMaterialInterface>(
|
||||
nullptr, TEXT("/PS_Editor/M_PS_Editor_Gizmo.M_PS_Editor_Gizmo"));
|
||||
|
||||
// Auto-generated opaque unlit material (depth tested) for spline line
|
||||
UMaterialInterface* SplineLineMat = LoadObject<UMaterialInterface>(
|
||||
nullptr, TEXT("/PS_Editor/M_PS_Editor_SplineLine.M_PS_Editor_SplineLine"));
|
||||
|
||||
if (!SplineLineMat)
|
||||
{
|
||||
UE_LOG(LogTemp, Warning, TEXT("PS_Editor: M_PS_Editor_SplineLine not found, trying without double name..."));
|
||||
SplineLineMat = LoadObject<UMaterialInterface>(nullptr, TEXT("/PS_Editor/M_PS_Editor_SplineLine"));
|
||||
}
|
||||
// GizmoBaseMaterial / SplineLineMaterial / meshes are resolved in the ctor via
|
||||
// ConstructorHelpers — hard cook references, available here.
|
||||
UMaterialInterface* GizmoMat = GizmoBaseMaterial.Get();
|
||||
UMaterialInterface* SplineLineMat = SplineLineMaterial.Get();
|
||||
|
||||
// Spline line: normal depth-tested material (uses SplineColor property)
|
||||
if (SplineLineMat)
|
||||
@@ -102,10 +125,9 @@ void APS_Editor_SplineActor::BeginPlay()
|
||||
CenterCube = NewObject<UStaticMeshComponent>(this);
|
||||
CenterCube->SetupAttachment(GetRootComponent());
|
||||
CenterCube->RegisterComponent();
|
||||
UStaticMesh* CubeMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Cube.Cube"));
|
||||
if (CubeMesh)
|
||||
if (CenterCubeMesh)
|
||||
{
|
||||
CenterCube->SetStaticMesh(CubeMesh);
|
||||
CenterCube->SetStaticMesh(CenterCubeMesh);
|
||||
}
|
||||
CenterCube->SetWorldScale3D(FVector(CenterCubeScale));
|
||||
CenterCube->SetCollisionEnabled(ECollisionEnabled::QueryOnly);
|
||||
@@ -310,11 +332,10 @@ UStaticMeshComponent* APS_Editor_SplineActor::CreatePointHandle(const FVector& W
|
||||
Handle->SetupAttachment(HandleRoot);
|
||||
Handle->RegisterComponent();
|
||||
|
||||
// Load sphere mesh
|
||||
UStaticMesh* SphereMesh = LoadObject<UStaticMesh>(nullptr, TEXT("/Engine/BasicShapes/Sphere.Sphere"));
|
||||
if (SphereMesh)
|
||||
// HandleSphereMesh resolved via ConstructorHelpers (hard cook ref).
|
||||
if (HandleSphereMesh)
|
||||
{
|
||||
Handle->SetStaticMesh(SphereMesh);
|
||||
Handle->SetStaticMesh(HandleSphereMesh);
|
||||
}
|
||||
|
||||
Handle->SetWorldLocation(WorldPosition);
|
||||
|
||||
@@ -10,6 +10,8 @@ class USplineComponent;
|
||||
class UProceduralMeshComponent;
|
||||
class UPS_Editor_SpawnableComponent;
|
||||
class UPS_Editor_EditableComponent;
|
||||
class UMaterialInterface;
|
||||
class UStaticMesh;
|
||||
|
||||
/**
|
||||
* Runtime spline actor for path/route editing in PS_Editor.
|
||||
@@ -166,6 +168,23 @@ private:
|
||||
UPROPERTY()
|
||||
TObjectPtr<UMaterialInstanceDynamic> CenterCubeMat;
|
||||
|
||||
// ---- Hard-ref source assets (cooked into packaged builds via ConstructorHelpers) ----
|
||||
|
||||
/** Gizmo base material for spline handles / selected state (renders on top). */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UMaterialInterface> GizmoBaseMaterial;
|
||||
|
||||
/** Depth-tested material for the normal spline line. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UMaterialInterface> SplineLineMaterial;
|
||||
|
||||
/** Engine basic shapes used for the center cube and point handles. */
|
||||
UPROPERTY()
|
||||
TObjectPtr<UStaticMesh> CenterCubeMesh;
|
||||
|
||||
UPROPERTY()
|
||||
TObjectPtr<UStaticMesh> HandleSphereMesh;
|
||||
|
||||
UPROPERTY(VisibleAnywhere)
|
||||
TObjectPtr<UPS_Editor_SpawnableComponent> SpawnableComp;
|
||||
|
||||
|
||||
Reference in New Issue
Block a user