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/EleriPlayerController.h"
#include "../Public/MyCharacter.h" #include "../Public/MyCharacter.h"
#include "Engine/OverlapResult.h" #include "Engine/OverlapResult.h"
#include "Engine/StaticMeshActor.h"
#include "LandscapeProxy.h" #include "LandscapeProxy.h"
// Sets default values for this component's properties // 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 = PlayerController->PlayerCameraManager->GetCameraRotation().Vector();
ForwardVec.Z = 0.f; ForwardVec.Z = 0.f;
ForwardVec.Normalize();
} }
// Prefer the per-actor tuning values, falling back to the component defaults. // 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(); const FVector TargetScale = (MovingActor->ScaleOverride != FVector::ZeroVector) ? MovingActor->ScaleOverride : MovingActor->GetActorScale();
MovingActor->SetActorTransform(FTransform(MovingActor->GetActorRotation(), PosVector, TargetScale)); 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; FCollisionObjectQueryParams QueryParams;
QueryParams.AddObjectTypesToQuery(ECC_WorldStatic); 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; TArray<FHitResult> OutHits;
GetWorld()->LineTraceMultiByObjectType(OutHits, MovingActor->GetActorLocation(), TraceEnd, QueryParams); GetWorld()->LineTraceMultiByObjectType(OutHits, TraceStart, TraceEnd, QueryParams, TraceParams);
MovingActor->bPlaceable = true; MovingActor->bPlaceable = true;
MovingActor->IntersectingActors.Empty(); 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) for (const FHitResult& HitResult : OutHits)
{ {
AActor* HitActor = HitResult.GetActor(); AActor* HitActor = HitResult.GetActor();
@@ -93,16 +108,39 @@ void UMoveActorsComponent::TickComponent(float DeltaTime, ELevelTick TickType, F
MovingActor->IntersectingActors.Add(HitActor); 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. // The bounds of the mesh are used for placement instead of the actor bounds: the actor also
FVector Center, BoxExtent; // holds the detection box and the interaction widget, and its pivot is not guaranteed to sit
MovingActor->GetActorBounds(false, Center, BoxExtent); // at the center (or at the bottom) of the mesh.
BoxExtent *= OverlapBoxExtentMultiplier; 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()) if (!MovingActor->BoxExtentOverride.IsNearlyZero())
{ {
BoxExtent = MovingActor->BoxExtentOverride; BoxExtent = MovingActor->BoxExtentOverride;
@@ -122,18 +160,27 @@ void UMoveActorsComponent::TickComponent(float DeltaTime, ELevelTick TickType, F
TArray<FOverlapResult> OverlapResults; TArray<FOverlapResult> OverlapResults;
GetWorld()->OverlapMultiByObjectType( GetWorld()->OverlapMultiByObjectType(
OverlapResults, OverlapResults,
MovingActor->GetActorLocation(), Center,
MovingActor->GetActorQuat(), MovingActor->GetActorQuat(),
ObjectQueryParams, ObjectQueryParams,
FCollisionShape::MakeBox(BoxExtent), FCollisionShape::MakeBox(BoxExtent),
OverlapQueryParams OverlapQueryParams
); );
bool PlantableAreaFound = false; TArray<AActor*> OverlappingActors;
MovingActor->OverlapBoxComponent->GetOverlappingActors(OverlappingActors);
for (const FOverlapResult& OverlapResult : OverlapResults) for (const FOverlapResult& OverlapResult : OverlapResults)
{ {
AActor* OverlappingActor = OverlapResult.GetActor(); if (AActor* OverlappingActor = OverlapResult.GetActor())
if (!OverlappingActor) continue; {
OverlappingActors.Add(OverlappingActor);
}
}
bool PlantableAreaFound = false;
for (const AActor* OverlappingActor : OverlappingActors)
{
if (!OverlappingActor || OverlappingActor == MovingActor) continue;
if (PlantableAreaActorClass == OverlappingActor->GetClass()) 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) if (!PlantableAreaFound)
{ {
MovingActor->bPlaceable = false; MovingActor->bPlaceable = false;
@@ -170,6 +245,23 @@ void UMoveActorsComponent::TickComponent(float DeltaTime, ELevelTick TickType, F
MovingActor->K2_TogglePlaceableEffects(MovingActor->bPlaceable); 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() void UMoveActorsComponent::ToggleGridMovement()
{ {
bGridMovementActive = !bGridMovementActive; bGridMovementActive = !bGridMovementActive;

View File

@@ -59,6 +59,18 @@ public:
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement") UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement")
float GroundTraceLength = 1000.f; 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")) UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Placement", meta = (ClampMin = "0.0", ClampMax = "1.0"))
float OverlapBoxExtentMultiplier = 0.9f; float OverlapBoxExtentMultiplier = 0.9f;
@@ -89,6 +101,11 @@ protected:
UPROPERTY() UPROPERTY()
bool bGridMovementActive; 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: public:
// Called every frame // Called every frame