From 4e4aa006f0fcddf388c3da1b7a1c17fd794f66ea Mon Sep 17 00:00:00 2001 From: "adnan.mujkic" Date: Sat, 12 Sep 2026 14:33:16 +0200 Subject: [PATCH] Fixes for collision detection when placing stuff --- .../Blueprints/BP_ThirdPersonCharacter.uasset | 4 +- .../Components/MoveActorsComponent.cpp | 120 ++++++++++++++++-- .../Components/MoveActorsComponent.h | 17 +++ 3 files changed, 125 insertions(+), 16 deletions(-) diff --git a/Content/ThirdPersonBP/Blueprints/BP_ThirdPersonCharacter.uasset b/Content/ThirdPersonBP/Blueprints/BP_ThirdPersonCharacter.uasset index 52b45eed..2e6dc3fa 100644 --- a/Content/ThirdPersonBP/Blueprints/BP_ThirdPersonCharacter.uasset +++ b/Content/ThirdPersonBP/Blueprints/BP_ThirdPersonCharacter.uasset @@ -1,3 +1,3 @@ version https://git-lfs.github.com/spec/v1 -oid sha256:7bb6e9292a284f3d68d6457f2787076ec7396338921a854426a5f8d8bc7594fd -size 1292036 +oid sha256:c1de15680f7c2462104225a429497b27f20e5d32ff397300525d8f9dfe009875 +size 1291979 diff --git a/Source/ProjectEleri/Components/MoveActorsComponent.cpp b/Source/ProjectEleri/Components/MoveActorsComponent.cpp index 53a1a65e..5698148a 100644 --- a/Source/ProjectEleri/Components/MoveActorsComponent.cpp +++ b/Source/ProjectEleri/Components/MoveActorsComponent.cpp @@ -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 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::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 OverlapResults; GetWorld()->OverlapMultiByObjectType( OverlapResults, - MovingActor->GetActorLocation(), + Center, MovingActor->GetActorQuat(), ObjectQueryParams, FCollisionShape::MakeBox(BoxExtent), OverlapQueryParams ); - - bool PlantableAreaFound = false; + + TArray 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 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; diff --git a/Source/ProjectEleri/Components/MoveActorsComponent.h b/Source/ProjectEleri/Components/MoveActorsComponent.h index d206f361..786080dd 100644 --- a/Source/ProjectEleri/Components/MoveActorsComponent.h +++ b/Source/ProjectEleri/Components/MoveActorsComponent.h @@ -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