Pivot-anchor fallback for no-collision spawns + always-snap on catalog

Ground snap:
- When an actor has no colliding components at all (e.g. a VR start
  marker, placement helper, or any BP whose collision is only enabled
  on entering editor mode), SnapActorToGround now anchors the PIVOT
  directly on the ground instead of falling back to the full-bounds
  bbox bottom. The full-bounds bottom could be far below the pivot
  (helper meshes, arrows, visual markers) and was lifting objects
  hundreds of units into the air.
- Actors with collision keep the original behavior (collision-bbox
  bottom rests on the ground).

Catalog spawn:
- Always snap to ground (drop the bAutoSnapToGround gate) — a user
  clicking in the catalog expects the object to land on the ground
  every time; the flag is kept for gameplay-side spawning via
  SceneLoader.
- SpawnActor now uses AlwaysSpawn instead of
  AdjustIfPossibleButAlwaysSpawn so UE never pushes the actor
  vertically to resolve overlaps — SnapActorToGround is the
  authoritative placement step.

CLAUDE.md workflow rule:
- Document that behavior / logic changes require explaining the plan
  first and waiting for go-ahead; only diagnostic-only additions
  (temporary UE_LOG) are fine without approval.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-04-24 12:53:01 +02:00
parent 93b9c7d927
commit b51f063d52
3 changed files with 43 additions and 12 deletions

View File

@@ -83,6 +83,17 @@ Symptom that the wrong widget was edited: logs you added don't appear in the Out
- When fixing a bug, fix the root cause, not the symptom.
- If something requires error handling or validation to work reliably, include it without asking.
## Workflow — CRITICAL
- **Do not make behavior / logic changes without first explaining the plan and waiting for the
user's go-ahead.** Explain what the change will be, why it fixes the issue, and what side
effects it could have. Only after the user approves (or asks to proceed), apply the edit.
- Diagnostic-only additions (temporary `UE_LOG`, read-only inspections) are fine without
approval because they can't regress the project.
- Refactors, algorithm changes, comportement changes, new defaults, removed features — all
require approval first, even when they seem obvious.
- When revealing new data (e.g. logs) suggests a fix, STATE the proposed fix in plain words
and wait. Don't pre-emptively write + apply the code change in the same turn.
## Build
- Engine: Unreal Engine 5.5
- Plugin location: `Unreal/Plugins/PS_Editor/`

View File

@@ -120,22 +120,33 @@ namespace PS_Editor_GroundSnap
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).
// Measure the bbox bottom relative to the pivot.
//
// Primary path — colliding components only: a Character's capsule drives the
// calculation, ignoring the skeletal mesh (often with collision disabled) or any
// attached weapons / child actors whose meshes might extend below the capsule and
// artificially lift the pawn by that overflow. Formula: pivot is placed so the
// collision-bbox bottom rests on the ground.
//
// Fallback — no colliding components found: the actor is treated as a pure visual
// marker (VR start zone, placement helper, or a BP that enables collision only
// when entering editor mode and hasn't done so yet). For these, we don't trust
// the full-bounds bbox (it may wrap helper meshes far below the pivot and push
// the object absurdly high). Instead we anchor the PIVOT directly on the ground —
// which is what the designer authored the pivot for on these markers.
FVector BoundsOrigin, BoundsExtent;
Actor->GetActorBounds(/*bOnlyCollidingComponents*/ true, BoundsOrigin, BoundsExtent);
if (BoundsExtent.IsNearlyZero())
const bool bUsedFallback = BoundsExtent.IsNearlyZero();
if (bUsedFallback)
{
// Read the full bounds too — only to know where "above the actor" is for the
// ray start position, not to anchor the snap.
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;
const float PivotAboveBottom = bUsedFallback ? 0.0f : (ActorZ - BBoxBottomZ);
float ExtraOffset = 0.0f;
if (UPS_Editor_SpawnableComponent* SpawnComp = Actor->FindComponentByClass<UPS_Editor_SpawnableComponent>())

View File

@@ -130,7 +130,13 @@ AActor* UPS_Editor_SpawnManager::SpawnFromCatalogAtLocation(int32 Index, FVector
}
FActorSpawnParameters SpawnParams;
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AdjustIfPossibleButAlwaysSpawn;
// AlwaysSpawn (instead of AdjustIfPossibleButAlwaysSpawn) so UE doesn't push the actor
// upward when its bounds overlap the ground at spawn time — our SnapActorToGround call
// below is the authoritative ground placement step, and it uses collision-only bounds
// which correctly ignore non-collidable attached meshes (weapons, child actors).
// With the old override, a character with a weapon dangling below the capsule could
// be shoved high by the auto-adjust, leaving it stuck in the air.
SpawnParams.SpawnCollisionHandlingOverride = ESpawnActorCollisionHandlingMethod::AlwaysSpawn;
AActor* SpawnedActor = PC->GetWorld()->SpawnActor<AActor>(Entry.ActorClass, WorldLocation, FRotator::ZeroRotator, SpawnParams);
if (!SpawnedActor)
@@ -160,10 +166,13 @@ AActor* UPS_Editor_SpawnManager::SpawnFromCatalogAtLocation(int32 Index, FVector
SpawnedActors.Add(SpawnedActor);
RegisterSpawnedActorId(PC->GetWorld(), SpawnedActor);
// 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.
// Catalogue spawns are always a deliberate user placement, so always snap to ground.
// Previously gated on SpawnableComponent::bAutoSnapToGround — that flag is preserved
// as the gameplay-spawn hint (for SceneLoader / runtime spawning) but here we want
// consistent placement regardless of BP configuration. If the actor genuinely shouldn't
// touch the ground, the user can move it with the gizmo after spawning.
UPS_Editor_SpawnableComponent* SpawnComp = SpawnedActor->FindComponentByClass<UPS_Editor_SpawnableComponent>();
if (SpawnComp && SpawnComp->bAutoSnapToGround && PC->GetWorld())
if (PC->GetWorld())
{
TArray<AActor*> ExtraIgnored;
if (APawn* Pwn = PC->GetPawn()) ExtraIgnored.Add(Pwn);