diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp index 1701221..de57cda 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.cpp @@ -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(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 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( - 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(EdPC3->GetActivePlacementActor())) { - // Raycast to get world click position - FHitResult AddHit; - FCollisionQueryParams AddParams; - AddParams.AddIgnoredActor(this); - AddParams.AddIgnoredActor(Cast(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 Ignored; + Ignored.Add(this); + if (AActor* SplineActor = Cast(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 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; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h index 39e5bb8..438599a 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_Pawn.h @@ -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 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 OutlineMaterial; + // ---- Camera Settings ---- UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Camera|Fly") diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp index ab16572..f32b1d8 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/GameMode/PS_Editor_PlayerController.cpp @@ -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(GetPawn())) + { + EditorPawn->ClearActiveSubElementDrag(); + } + if (PlacementActor && SelectionManager) { SelectionManager->ClearSelection(); diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp index 2103ff8..acd7788 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.cpp @@ -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 GizmoBaseMatFinder( + TEXT("/PS_Editor/M_PS_Editor_Gizmo.M_PS_Editor_Gizmo")); + if (GizmoBaseMatFinder.Succeeded()) + { + GizmoBaseMaterial = GizmoBaseMatFinder.Object; + } + GizmoRoot = CreateDefaultSubobject(TEXT("GizmoRoot")); SetRootComponent(GizmoRoot); @@ -92,9 +101,8 @@ void APS_Editor_Gizmo::BeginPlay() { Super::BeginPlay(); - // Load gizmo material - UMaterialInterface* GizmoBaseMat = LoadObject( - 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* { diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h index 6c010d9..7c419c2 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Gizmo/PS_Editor_Gizmo.h @@ -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 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 GizmoBaseMaterial; + UPROPERTY() TObjectPtr Mat_X; UPROPERTY() diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_GroundSnap.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_GroundSnap.cpp new file mode 100644 index 0000000..bd7cdf8 --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_GroundSnap.cpp @@ -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 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& 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& 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()) + { + 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 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; + } +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_GroundSnap.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_GroundSnap.h new file mode 100644 index 0000000..fc3b42c --- /dev/null +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_GroundSnap.h @@ -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& 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& ExtraIgnored = {}); +} diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp index 55b69b4..01c9032 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.cpp @@ -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 UndoAction = MakeShared(); UndoAction->Description = FString::Printf(TEXT("Snap to ground %d actor(s)"), SelectedActors.Num()); + // Extra ignores shared across every actor in the batch. + TArray ExtraIgnored; + if (APawn* Pwn = PC->GetPawn()) ExtraIgnored.Add(Pwn); + if (Gizmo) ExtraIgnored.Add(Gizmo); + for (const TWeakObjectPtr& 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()) + 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 { diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h index 0163bf2..42a7d13 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Selection/PS_Editor_SelectionManager.h @@ -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 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(); }; diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp index 953e56e..8c05930 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spawn/PS_Editor_SpawnManager.cpp @@ -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(); if (SpawnComp && SpawnComp->bAutoSnapToGround && PC->GetWorld()) { - FVector Origin, Extent; - SpawnedActor->GetActorBounds(false, Origin, Extent); - float BottomOffset = Extent.Z + SpawnComp->GroundOffset; + TArray 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 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) diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spline/PS_Editor_SplineActor.cpp b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spline/PS_Editor_SplineActor.cpp index 1053205..73485cd 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spline/PS_Editor_SplineActor.cpp +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spline/PS_Editor_SplineActor.cpp @@ -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 GizmoBaseMatFinder( + TEXT("/PS_Editor/M_PS_Editor_Gizmo.M_PS_Editor_Gizmo")); + if (GizmoBaseMatFinder.Succeeded()) + { + GizmoBaseMaterial = GizmoBaseMatFinder.Object; + } + + static ConstructorHelpers::FObjectFinder SplineLineMatFinder( + TEXT("/PS_Editor/M_PS_Editor_SplineLine.M_PS_Editor_SplineLine")); + if (SplineLineMatFinder.Succeeded()) + { + SplineLineMaterial = SplineLineMatFinder.Object; + } + + static ConstructorHelpers::FObjectFinder CubeFinder( + TEXT("/Engine/BasicShapes/Cube.Cube")); + if (CubeFinder.Succeeded()) + { + CenterCubeMesh = CubeFinder.Object; + } + + static ConstructorHelpers::FObjectFinder 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( - 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( - 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(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(this); CenterCube->SetupAttachment(GetRootComponent()); CenterCube->RegisterComponent(); - UStaticMesh* CubeMesh = LoadObject(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(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); diff --git a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spline/PS_Editor_SplineActor.h b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spline/PS_Editor_SplineActor.h index a65214a..6eb5d22 100644 --- a/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spline/PS_Editor_SplineActor.h +++ b/Unreal/Plugins/PS_Editor/Source/PS_Editor/Spline/PS_Editor_SplineActor.h @@ -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 CenterCubeMat; + // ---- Hard-ref source assets (cooked into packaged builds via ConstructorHelpers) ---- + + /** Gizmo base material for spline handles / selected state (renders on top). */ + UPROPERTY() + TObjectPtr GizmoBaseMaterial; + + /** Depth-tested material for the normal spline line. */ + UPROPERTY() + TObjectPtr SplineLineMaterial; + + /** Engine basic shapes used for the center cube and point handles. */ + UPROPERTY() + TObjectPtr CenterCubeMesh; + + UPROPERTY() + TObjectPtr HandleSphereMesh; + UPROPERTY(VisibleAnywhere) TObjectPtr SpawnableComp;