Fixes for collision detection when placing stuff

This commit is contained in:
2026-09-12 14:33:16 +02:00
parent 6240f501ad
commit 4e4aa006f0
3 changed files with 125 additions and 16 deletions

View File

@@ -11,6 +11,7 @@
#include "../Public/EleriPlayerController.h"
#include "../Public/MyCharacter.h"
#include "Engine/OverlapResult.h"
#include "Engine/StaticMeshActor.h"
#include "LandscapeProxy.h"
// Sets default values for this component's properties
@@ -60,6 +61,7 @@ void UMoveActorsComponent::TickComponent(float DeltaTime, ELevelTick TickType, F
{
ForwardVec = PlayerController->PlayerCameraManager->GetCameraRotation().Vector();
ForwardVec.Z = 0.f;
ForwardVec.Normalize();
}
// Prefer the per-actor tuning values, falling back to the component defaults.
@@ -76,16 +78,29 @@ void UMoveActorsComponent::TickComponent(float DeltaTime, ELevelTick TickType, F
const FVector TargetScale = (MovingActor->ScaleOverride != FVector::ZeroVector) ? MovingActor->ScaleOverride : MovingActor->GetActorScale();
MovingActor->SetActorTransform(FTransform(MovingActor->GetActorRotation(), PosVector, TargetScale));
// Snap the object down onto the ground beneath it.
// 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)
// instead of only snapping down onto the landscape.
FCollisionObjectQueryParams QueryParams;
QueryParams.AddObjectTypesToQuery(ECC_WorldStatic);
const FVector TraceEnd = MovingActor->GetActorLocation() + FVector(0.f, 0.f, -GroundTraceLength);
FCollisionQueryParams TraceParams;
TraceParams.AddIgnoredActor(MovingActor);
TraceParams.AddIgnoredActor(PlayerCharacter);
const FVector TraceStart = MovingActor->GetActorLocation() + FVector(0.f, 0.f, SurfaceProbeUpOffset);
const FVector TraceEnd = MovingActor->GetActorLocation() - FVector(0.f, 0.f, GroundTraceLength);
TArray<FHitResult> OutHits;
GetWorld()->LineTraceMultiByObjectType(OutHits, MovingActor->GetActorLocation(), TraceEnd, QueryParams);
GetWorld()->LineTraceMultiByObjectType(OutHits, TraceStart, TraceEnd, QueryParams, TraceParams);
MovingActor->bPlaceable = true;
MovingActor->IntersectingActors.Empty();
// The closest valid hit is the highest surface beneath the probe: that is what the object
// should rest on, which may be the landscape or the top of another (static mesh) actor.
const FHitResult* SupportHit = nullptr;
double ClosestSupportDistSq = TNumericLimits<double>::Max();
for (const FHitResult& HitResult : OutHits)
{
AActor* HitActor = HitResult.GetActor();
@@ -93,16 +108,39 @@ void UMoveActorsComponent::TickComponent(float DeltaTime, ELevelTick TickType, F
MovingActor->IntersectingActors.Add(HitActor);
if (HitActor->IsA(ALandscapeProxy::StaticClass()))
// Only the landscape and static mesh actors are valid surfaces to rest on.
const bool bIsValidSupport = HitActor->IsA(ALandscapeProxy::StaticClass())
|| HitActor->GetClass()->IsChildOf(AStaticMeshActor::StaticClass());
if (!bIsValidSupport) continue;
const double HitDistSq = FVector::DistSquared(TraceStart, HitResult.ImpactPoint);
if (HitDistSq < ClosestSupportDistSq)
{
MovingActor->SetActorLocation(HitResult.ImpactPoint);
ClosestSupportDistSq = HitDistSq;
SupportHit = &HitResult;
}
}
// Check whether the object overlaps anything that should block placement.
FVector Center, BoxExtent;
MovingActor->GetActorBounds(false, Center, BoxExtent);
BoxExtent *= OverlapBoxExtentMultiplier;
// The bounds of the mesh are used for placement instead of the actor bounds: the actor also
// holds the detection box and the interaction widget, and its pivot is not guaranteed to sit
// at the center (or at the bottom) of the mesh.
const FBoxSphereBounds MeshBounds = GetMovingActorMeshBounds();
if (SupportHit)
{
// Rest the mesh on the surface that was found. The pivot is not necessarily at the bottom
// of the mesh, so the surface point is shifted by the mesh' bottom offset, which is zero for
// the common case of a pivot sitting at the bottom of the mesh.
const double MeshBottomOffsetZ = (MeshBounds.Origin.Z - MeshBounds.BoxExtent.Z) - MovingActor->GetActorLocation().Z;
MovingActor->SetActorLocation(SupportHit->ImpactPoint - FVector(0.0, 0.0, MeshBottomOffsetZ));
}
// Check whether the object overlaps anything that should block placement. The query box follows
// the mesh' bounds and is queried around the center of the mesh to match the detection box:
// querying around the pivot would shift the box whenever the pivot is not at the mesh' center,
// which can make the object collide with the very surface it is resting on.
const FVector Center = MeshBounds.Origin;
FVector BoxExtent = MeshBounds.BoxExtent * OverlapBoxExtentMultiplier;
if (!MovingActor->BoxExtentOverride.IsNearlyZero())
{
BoxExtent = MovingActor->BoxExtentOverride;
@@ -122,18 +160,27 @@ void UMoveActorsComponent::TickComponent(float DeltaTime, ELevelTick TickType, F
TArray<FOverlapResult> OverlapResults;
GetWorld()->OverlapMultiByObjectType(
OverlapResults,
MovingActor->GetActorLocation(),
Center,
MovingActor->GetActorQuat(),
ObjectQueryParams,
FCollisionShape::MakeBox(BoxExtent),
OverlapQueryParams
);
bool PlantableAreaFound = false;
TArray<AActor*> OverlappingActors;
MovingActor->OverlapBoxComponent->GetOverlappingActors(OverlappingActors);
for (const FOverlapResult& OverlapResult : OverlapResults)
{
AActor* OverlappingActor = OverlapResult.GetActor();
if (!OverlappingActor) continue;
if (AActor* OverlappingActor = OverlapResult.GetActor())
{
OverlappingActors.Add(OverlappingActor);
}
}
bool PlantableAreaFound = false;
for (const AActor* OverlappingActor : OverlappingActors)
{
if (!OverlappingActor || OverlappingActor == MovingActor) continue;
if (PlantableAreaActorClass == OverlappingActor->GetClass())
{
@@ -158,6 +205,34 @@ void UMoveActorsComponent::TickComponent(float DeltaTime, ELevelTick TickType, F
}
}
// An object that rests on top of another object has its box lifted out of the plantable area it
// stands in, which would make stacking inside a plantable area impossible. Look below the object
// before rejecting the placement, using the same footprint extended downwards.
if (!PlantableAreaFound && PlantableAreaActorClass && PlantableAreaProbeDepth > 0.f)
{
const float ExtraDepth = PlantableAreaProbeDepth * 0.5f;
TArray<FOverlapResult> PlantableOverlapResults;
GetWorld()->OverlapMultiByObjectType(
PlantableOverlapResults,
Center - FVector(0.f, 0.f, ExtraDepth),
MovingActor->GetActorQuat(),
ObjectQueryParams,
FCollisionShape::MakeBox(BoxExtent + FVector(0.f, 0.f, ExtraDepth)),
OverlapQueryParams
);
for (const FOverlapResult& OverlapResult : PlantableOverlapResults)
{
const AActor* OverlappingActor = OverlapResult.GetActor();
if (OverlappingActor && PlantableAreaActorClass == OverlappingActor->GetClass())
{
PlantableAreaFound = true;
break;
}
}
}
if (!PlantableAreaFound)
{
MovingActor->bPlaceable = false;
@@ -170,6 +245,23 @@ void UMoveActorsComponent::TickComponent(float DeltaTime, ELevelTick TickType, F
MovingActor->K2_TogglePlaceableEffects(MovingActor->bPlaceable);
}
FBoxSphereBounds UMoveActorsComponent::GetMovingActorMeshBounds() const
{
if (!IsValid(MovingActor))
{
return FBoxSphereBounds(FVector::ZeroVector, FVector::ZeroVector, 0.0);
}
// CalcBounds returns the world space bounds of the mesh, so the actor scale and the actor
// rotation are taken into account. Those bounds are not necessarily centered on the pivot.
if (const UStaticMeshComponent* MeshComponent = MovingActor->GetStaticMeshComponent())
{
return MeshComponent->CalcBounds(MeshComponent->GetComponentTransform());
}
return FBoxSphereBounds(MovingActor->GetActorLocation(), FVector::ZeroVector, 0.0);
}
void UMoveActorsComponent::ToggleGridMovement()
{
bGridMovementActive = !bGridMovementActive;

View File

@@ -59,6 +59,18 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement")
float GroundTraceLength = 1000.f;
/** How far above the hovered position the downward surface probe starts. A larger value
* lets the object snap on top of taller stacks of objects instead of only onto the landscape. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement")
float SurfaceProbeUpOffset = 500.f;
/** How far below the object's own overlap box a plantable area is still accepted as the area the
* object is being placed in. An object that rests on top of another object has its box lifted
* out of the plantable area's volume, so a value larger than 0 is what keeps stacked objects
* placeable. Use 0 to only accept plantable areas that the object's own box overlaps. */
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement", meta = (ClampMin = "0.0"))
float PlantableAreaProbeDepth = 1000.f;
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float OverlapBoxExtentMultiplier = 0.9f;
@@ -89,6 +101,11 @@ protected:
UPROPERTY()
bool bGridMovementActive;
/** World bounds of the mesh of the object being moved, or a zero sized box at its location when
* it has no static mesh. The actor bounds are not used for placement because they also contain
* the detection box and the interaction widget, which do not match the mesh that is placed. */
FBoxSphereBounds GetMovingActorMeshBounds() const;
public:
// Called every frame