Dynamic Collision Component Error

so, I’ve been working on a dynamic collision component for my game. What I mean by that, more specifically, is that my component creates a collision mesh based on an enum, both at runtime and in-editor.

the component does work, as in, I cannot pass through the pawn to which i assign it to, and the scene proxy (for debug, editor) works as well, for all bodies! BUT, whenever I enter a console command ‘show Collision’ (same goes for turning on collision view in editor), I cannot seem to make my component render the debug collision the same that conventional collision meshes do.

This is potentially a really insignificant issue, but i want to make the component as close to native components in function as possible, which includes console commands rendering proper mesh preview

here’s my full code for the custom component:

.h

// Written by Dipper/NSJaws; For purposes of project Moolah/spec{tr}.

#pragma once


 “CoreMinimal.h”

 “Components/PrimitiveComponent.h”

 “Components/ShapeComponent.h”

 “PhysicsEngine/BodySetup.h”

 “Engine/StaticMesh.h”

 “SceneView.h”


 “PrimitiveViewRelevance.h”

 “PlugMoolah.h”

 “MoolahCollisionProxyComponent.generated.h”

/**

This is a dynamic collision component that changes shape based on the value of SelectedCollisionType enum.

The enum is refereced in “PlugMoolah.h”.

Works as a root component; Attached meshes & debugs render aproperly to their specificities.
*/
UCLASS(ClassGroup = (Custom), meta = (BlueprintSpawnableComponent))
class PLUGMOOLAH_API UMoolahCollisionProxyComponent : public UShapeComponent
{
GENERATED_BODY()

public:
UMoolahCollisionProxyComponent();

// Don't need it, BodySetup is already defined in parent class.
/*UPROPERTY(Transient)
TObjectPtr<UBodySetup> BodySetup;*/

// Don't need it, BodySetup is already defined in parent class.
/*virtual UBodySetup* GetBodySetup() override;*/
virtual UBodySetup* GetBodySetup() override
{
	UpdateBodySetup();
	return ShapeBodySetup;
}

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Moolah|Collision Proxy", meta = (EditCondition = "CurrentShape == ECollisionComponentType::Sphere", EditConditionHides))
float SphereExtent = 30.0f;

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Moolah|Collision Proxy", meta = (EditCondition = "CurrentShape == ECollisionComponentType::Capsule", EditConditionHides))
FVector2D CapsuleExtents = FVector2D(30.0f, 180.0f);

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Moolah|Collision Proxy", meta = (EditCondition = "CurrentShape == ECollisionComponentType::Box", EditConditionHides))
FVector BoxExtents = FVector(30.0f, 30.0f, 90.0f);

UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Moolah|Collision Proxy", meta = (EditCondition = "CurrentShape == ECollisionComponentType::Mesh", EditConditionHides))
TObjectPtr<UStaticMesh> CollisionStaticMesh;

UPROPERTY(/*EditAnywhere, meta = (HideInInspector)*/)
ECollisionComponentType CurrentShape;

#if WITH_EDITORONLY_DATA
/Display When Selected:
- Display collision only when selected.
- If false, will always display collision, if debugger is set to true.
- Exists as a small performance boost check./
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = “Moolah|Collision Proxy”, meta = (AllowPrivateAccess = “true”, HideInDetailsView))
bool bDisplayCollisionOnlyWhenSelected = false;

/*Collision Debugger Color:*/
UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Moolah|Collision Proxy", meta = (AllowPrivateAccess = "true", HideInDetailsView))
FColor DebugColor = FColor(255, 82, 44, 255);

#endif

void SetCollisionShape(ECollisionComponentType Type);

protected:
void RebuildCollision();

virtual bool ShouldCreatePhysicsState() const override
{
	return Super::ShouldCreatePhysicsState() && IsCollisionEnabled();
}

virtual void UpdateBodySetup() override;

virtual FPrimitiveSceneProxy* CreateSceneProxy() override;

virtual FBoxSphereBounds CalcBounds(const FTransform& LocalToWorld) const override;

virtual void OnRegister()  override;

#if WITH_EDITOR
// Editor-Only function that checks for property value changes.
virtual void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;
#endif
};

.cpp

// Written by Dipper/NSJaws; For purposes of project Moolah/spec{tr}.


 “MoolahCollisionProxyComponent.h”

 “PhysicsEngine/BodyInstance.h”

 “PhysicsEngine/BodySetup.h”

 “PhysicsEngine/AggregateGeom.h”

 “PhysicsEngine/SphylElem.h”

 “PhysicsEngine/SphereElem.h”

 “PhysicsEngine/BoxElem.h”

 “Engine/StaticMesh.h”

 “Components/PrimitiveComponent.h”

 “PlugMoolah.h”

/------------------------------------------------------------------------------------------------------------------------------------------------------/

class FMoolahCollisionSceneProxy final : public FPrimitiveSceneProxy
{
public:
FMoolahCollisionSceneProxy(const UMoolahCollisionProxyComponent* Component)
: FPrimitiveSceneProxy(Component)
, Proxy(Component)
{
// …
}

virtual SIZE_T GetTypeHash() const override
{
    static size_t Unique;
    return reinterpret_cast<size_t>(&Unique);
}

virtual void GetDynamicMeshElements(const TArray<const FSceneView*>& Views, const FSceneViewFamily& ViewFamily, uint32 VisibilityMap, FMeshElementCollector& Collector) const override
{

#if WITH_EDITOR
if (!Proxy || !Proxy->ShapeBodySetup)
return;

    if (Proxy->bDisplayCollisionOnlyWhenSelected)
    {
        if (!IsSelected())
            return;
    }

    const FTransform TM(GetLocalToWorld());
    const UBodySetup* BS = Proxy->ShapeBodySetup;

    for (int32 ViewIndex = 0; ViewIndex < Views.Num(); ViewIndex++)
    {
        if (!(VisibilityMap & (1 << ViewIndex)))
            continue;

        FPrimitiveDrawInterface* PDI = Collector.GetPDI(ViewIndex);

        const FColor Color = Proxy->DebugColor;

        // Draw Boxes:
        for (const FKBoxElem& Box : BS->AggGeom.BoxElems)
        {
            const FTransform ElemTM = Box.GetTransform() * TM;

            DrawWireBox(
                PDI,
                ElemTM.ToMatrixWithScale(),
                FBox(FVector(-Box.X * 0.5f, -Box.Y * 0.5f, -Box.Z * 0.5f),
                     FVector(Box.X * 0.5f, Box.Y * 0.5f, Box.Z * 0.5f)),
                Color,
                SDPG_World
            );
        }

        // Draw Spheres:
        for (const FKSphereElem& Sphere : BS->AggGeom.SphereElems)
        {
            const FVector Center = TM.TransformPosition(Sphere.Center);

            DrawWireSphere(
                PDI,
                Center,
                Color,
                Sphere.Radius,
                16,
                SDPG_World
            );
        }

        // Draw Capsules:
        for (const FKSphylElem& Capsule : BS->AggGeom.SphylElems)
        {
            const FVector Center = TM.TransformPosition(Capsule.Center);

            DrawWireCapsule(
                PDI,
                Center,
                TM.GetUnitAxis(EAxis::X),
                TM.GetUnitAxis(EAxis::Y),
                TM.GetUnitAxis(EAxis::Z),
                Color,
                Capsule.Radius,
                Capsule.Length * 0.5f,
                16,
                SDPG_World
            );
        }

        // Draw Convex Hulls:
        for (const FKConvexElem& Convex : BS->AggGeom.ConvexElems)
        {
            Convex.DrawElemWire(PDI, TM, 1.0f, Color);
        }
    }

#endif
}

virtual FPrimitiveViewRelevance GetViewRelevance(const FSceneView* View) const override
{
    FPrimitiveViewRelevance Result;

#if WITH_EDITOR
Result.bDrawRelevance = true;
Result.bDynamicRelevance = true;
Result.bEditorPrimitiveRelevance = true;
#endif

    return Result;
}

virtual uint32 GetMemoryFootprint() const override
{
    return sizeof(*this);
}

private:
const UMoolahCollisionProxyComponent* Proxy;
};

/------------------------------------------------------------------------------------------------------------------------------------------------------/

UMoolahCollisionProxyComponent::UMoolahCollisionProxyComponent()
{
// UPDATE: We have reparented our component, which already contains ShapeBodySetup.
//BodySetup = GetBodySetup(); //CreateDefaultSubobject(TEXT(“Collision Body Setup”));   // This works correctly.
//BodySetup = NewObject(this);                                                          // This causes crash on engine startup.

SetCollisionProfileName(UCollisionProfile::BlockAll_ProfileName);
SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
SetGenerateOverlapEvents(true);

// We cannot use GetBodySetup() anymore, because our parent class uses UpdateBodySetup() before returning it, potentially causing infinite recursion. 
// Instead, we directly assign to the parent class's BodySetup variable, which is what GetBodySetup() returns.
if (ShapeBodySetup)
{
    ShapeBodySetup->CollisionTraceFlag = CTF_UseSimpleAsComplex;
    ShapeBodySetup->bGenerateMirroredCollision = false;
    ShapeBodySetup->bDoubleSidedGeometry = true;
}

SetVisibility(true);
bHiddenInGame = true;
bRenderInMainPass = true;       // Optional, if you don't want to render.
bVisualizeComponent = true;     // Helps editor visualize by creating a sprite.

}

/------------------------------------------------------------------------------------------------------------------------------------------------------/

void UMoolahCollisionProxyComponent::SetCollisionShape(ECollisionComponentType NewType)
{
CurrentShape = NewType;

RebuildCollision();

}

/------------------------------------------------------------------------------------------------------------------------------------------------------/

void UMoolahCollisionProxyComponent::RebuildCollision()
{
// Fill AggGeom:
UpdateBodySetup();

// Refresh render state for debug draw:
RecreatePhysicsState();
MarkRenderStateDirty();

// Ensure collision properties are correct:
SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
SetCollisionProfileName(UCollisionProfile::BlockAll_ProfileName);
SetGenerateOverlapEvents(true);

}

/------------------------------------------------------------------------------------------------------------------------------------------------------/

void UMoolahCollisionProxyComponent::UpdateBodySetup()
{
// Ensure ShapeBodySetup exists:
switch (CurrentShape)
{
default:
case ECollisionComponentType::Capsule:
CreateShapeBodySetupIfNeeded();
break;
case ECollisionComponentType::Sphere:
CreateShapeBodySetupIfNeeded();
break;
case ECollisionComponentType::Box:
CreateShapeBodySetupIfNeeded();
break;
case ECollisionComponentType::Mesh:
// For mesh, create placeholder. (…will copy AggGeom below!)
CreateShapeBodySetupIfNeeded();
break;
}

// If we still don't have a valid BodySetup, log an error and exit:
if (!ShapeBodySetup)
{
    LOG_WITH_NET_CONTEXT_CONDIF(true, this, FUNCTION_NAME, EFunctionLogCategory::Error,
                                TEXT("Failed to create BodySetup for collision! Collision will not work."));
	return;
}

// Clear existing geometry:
ShapeBodySetup->AggGeom.EmptyElements();

// Fill AggGeom based on current shape:
switch (CurrentShape)
{
    default:
    case ECollisionComponentType::Capsule:
    {
        FKSphylElem Capsule;
        Capsule.Radius = CapsuleExtents.X;
        Capsule.Length = CapsuleExtents.Y;
        ShapeBodySetup->AggGeom.SphylElems.Add(Capsule);
        break;
    }

    case ECollisionComponentType::Sphere:
    {
        FKSphereElem Sphere;
        Sphere.Radius = SphereExtent;
        ShapeBodySetup->AggGeom.SphereElems.Add(Sphere);
        break;
    }

    case ECollisionComponentType::Box:
    {
        FKBoxElem Box;
        Box.X = BoxExtents.X * 2;
        Box.Y = BoxExtents.Y * 2;
        Box.Z = BoxExtents.Z * 2;
        ShapeBodySetup->AggGeom.BoxElems.Add(Box);
        break;
    }

    case ECollisionComponentType::Mesh:
    {
        if (CollisionStaticMesh && CollisionStaticMesh->GetBodySetup())
        {
            ShapeBodySetup->AggGeom = CollisionStaticMesh->GetBodySetup()->AggGeom;
        }
        else
        {
            LOG_WITH_NET_CONTEXT_CONDIF(true, this, FUNCTION_NAME, EFunctionLogCategory::Warning,
                                        TEXT("No valid 'StaticMesh' assigned for collision! Please select one, else the old collision remains."));
        }
        break;
    }
}

ShapeBodySetup->InvalidatePhysicsData();
ShapeBodySetup->CreatePhysicsMeshes();

//Super::UpdateBodySetup();
// You never really wanna call Super, since it's specifically written as a must-override function that causes a crash if not overriden.

}

/------------------------------------------------------------------------------------------------------------------------------------------------------/

FPrimitiveSceneProxy* UMoolahCollisionProxyComponent::CreateSceneProxy()
{
//Super::CreateSceneProxy();

UE_LOG(LogTemp, Warning, TEXT("CreateSceneProxy called for MoolahCollisionProxyComponent."));

#if WITH_EDITOR
return new FMoolahCollisionSceneProxy(this);
#else
return nullptr;
#endif
}

/------------------------------------------------------------------------------------------------------------------------------------------------------/

FBoxSphereBounds UMoolahCollisionProxyComponent::CalcBounds(const FTransform& LocalToWorld) const
{
//Super::CalcBoundingCylinder(LocalToWorld);

switch (CurrentShape)
{
    default:
    case ECollisionComponentType::Capsule:
    {
        FVector Extent(CapsuleExtents.X, CapsuleExtents.X, CapsuleExtents.Y);
        return FBoxSphereBounds(FBox(-Extent, Extent).TransformBy(LocalToWorld));
        break;
    }

    case ECollisionComponentType::Sphere:
    {
        FVector Extent(SphereExtent);
        return FBoxSphereBounds(FBox(-Extent, Extent).TransformBy(LocalToWorld));
        break;
    }

    case ECollisionComponentType::Box:
    {
        FVector Extent(BoxExtents.X, BoxExtents.Y, BoxExtents.Z);
        return FBoxSphereBounds(FBox(-Extent, Extent).TransformBy(LocalToWorld));
        break;
    }

    case ECollisionComponentType::Mesh:
    {
        if (CollisionStaticMesh)
        {
            const FBoxSphereBounds MeshBounds = CollisionStaticMesh->GetBounds();
            return MeshBounds.TransformBy(LocalToWorld);
        }
        break;
    }
}

return Super::CalcBounds(LocalToWorld);

}

/------------------------------------------------------------------------------------------------------------------------------------------------------/

void UMoolahCollisionProxyComponent::OnRegister()
{
Super::OnRegister();

RebuildCollision();

int32 NumShapes = 0;

if (BodyInstance.IsValidBodyInstance())
{
    UBodySetup* BodySetup = BodyInstance.GetBodySetup();
    if (BodySetup)
    {
        // Sum counts of various collision primitives
        NumShapes = BodySetup->AggGeom.GetElementCount();
    }
}

UE_LOG(LogTemp, Warning, TEXT("MoolahCollision Shape Count: %d"), NumShapes);

UE_LOG(LogTemp, Warning, TEXT("Bounds: %s"),
    *CalcBounds(GetComponentTransform()).GetBox().ToString());

UE_LOG(LogTemp, Warning,
    TEXT("MoolahCollision Bounds: Origin=%s Extent=%s SphereRadius=%f"),
    *Bounds.Origin.ToString(),
    *Bounds.BoxExtent.ToString(),
    Bounds.SphereRadius
);

UE_LOG(LogTemp, Warning, TEXT("bVisualizeComponent: %d, bRenderInMainPass: %d"), bVisualizeComponent, bRenderInMainPass);

UE_LOG(LogTemp, Warning, TEXT("IsRegistered: %d, Owner: %s"), IsRegistered(), *GetOwner()->GetName());

}

/------------------------------------------------------------------------------------------------------------------------------------------------------/

#if WITH_EDITOR
void UMoolahCollisionProxyComponent::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
Super::PostEditChangeProperty(PropertyChangedEvent);

GHOST_BOOL(bHasMadeChanges, false);
LOG_WITH_NET_CONTEXT_CONDIF(true, this, FUNCTION_NAME, EFunctionLogCategory::Warning,
                            TEXT("Attempting to change a property inside 'MoolahCollisionProxyComponent'."));

if (PropertyChangedEvent.Property)
{
    const FName PropertyName = PropertyChangedEvent.Property->GetFName();

    if (PropertyName == GET_MEMBER_NAME_CHECKED(UMoolahCollisionProxyComponent, SphereExtent) ||
        PropertyName == GET_MEMBER_NAME_CHECKED(UMoolahCollisionProxyComponent, CapsuleExtents) ||
        PropertyName == GET_MEMBER_NAME_CHECKED(UMoolahCollisionProxyComponent, BoxExtents))
    {
        RebuildCollision();

        bHasMadeChanges = true;
        LOG_WITH_NET_CONTEXT_CONDIF(true, this, FUNCTION_NAME, EFunctionLogCategory::Log,
			                        TEXT("Updated %s dimension(s)!"), *PropertyName.ToString());
    }

    if (PropertyName == GET_MEMBER_NAME_CHECKED(UMoolahCollisionProxyComponent, CollisionStaticMesh))
    {
        if (CollisionStaticMesh && CurrentShape == ECollisionComponentType::Mesh)
        {
            //SetCollisionShape(CurrentShape);  // We don't need to assign CurrentShape again, bypass with RebuildCollision().
            RebuildCollision();

            bHasMadeChanges = true;
            LOG_WITH_NET_CONTEXT_CONDIF(true, this, FUNCTION_NAME, EFunctionLogCategory::Log,
				                        TEXT("Updated static collision mesh!"));
        }

        LOG_WITH_NET_CONTEXT_CONDIF(CollisionStaticMesh && CurrentShape != ECollisionComponentType::Mesh, this, FUNCTION_NAME, EFunctionLogCategory::Error,
                                    TEXT("You attempted changing the Static Mesh without selecting the shape first; Select the Static Mesh Collision to propagate the change."));
        LOG_WITH_NET_CONTEXT_CONDIF(!CollisionStaticMesh && CurrentShape == ECollisionComponentType::Mesh, this, FUNCTION_NAME, EFunctionLogCategory::Error,
                                    TEXT("The Static Mesh you have chosen as your collision model is invalid!"));
    }
}

// Final log; Occurs if no relevant property changes were detected.	
LOG_WITH_NET_CONTEXT_CONDIF(!bHasMadeChanges, this, FUNCTION_NAME, EFunctionLogCategory::Warning,
                            TEXT("No properties were modified!"));

}
#endif

/------------------------------------------------------------------------------------------------------------------------------------------------------/

// Don’t need it, BodySetup is already defined in parent class.
//UBodySetup* UMoolahCollisionProxyComponent::GetBodySetup()
//{
//    return BodySetup;
//}

/------------------------------------------------------------------------------------------------------------------------------------------------------/

feel free to ignore custom debug macros, they don’t do anything special, though if anyone wants the code for them I’d be more thn willing to share!

images:

Hey @Banterberry how are you?

I’ve been researching about this a lot, but I need to clarify I’m not a programmer, so this solution could be not the best! But it could trigger a good solution on you anyways!

As far as I could understand, the show Collision command renders collision using the engine’s collision visualization pipeline, which queries GetViewRelevance() specifically for bDrawRelevance tied to collision debug rendering, not your custom scene proxy’s draw calls.

To solve this you could try with the following fixes:

  1. Fix GetViewRelevance() in your scene proxy. Your current implementation unconditionally sets bDrawRelevance = true. You need to hook into the engine’s collision visibility flag:
virtual FPrimitiveViewRelevance GetViewRelevance(const FSceneView* View) const override
{
    FPrimitiveViewRelevance Result;
    Result.bDrawRelevance = IsShown(View);
    Result.bDynamicRelevance = true;

#if WITH_EDITOR
    Result.bEditorPrimitiveRelevance = true;
#endif

    // This is the key flag! Hooks into "show Collision"
    Result.bSeparateTranslucency = true;
    
    return Result;
}
  1. Override GetBodySetup() at the component level. The show Collision view works by iterating all Primitive Components in the scene and calling GetBodySetup() on each of them to draw their AggGeom. Your GetBodySetup() currently calls UpdateBodySetup() every time it’s called. It should just return the cached setup:
virtual UBodySetup* GetBodySetup() override
{
    return ShapeBodySetup; // Dont call "UpdateBodySetup()" here
}
  1. Implement GetCollisionShape() override. The engine’s collision debug path also queries this:
virtual FCollisionShape GetCollisionShape(float Inflation = 0.0f) const override
{
    switch (CurrentShape)
    {
        case ECollisionComponentType::Sphere:
            return FCollisionShape::MakeSphere(SphereExtent + Inflation);
        case ECollisionComponentType::Box:
            return FCollisionShape::MakeBox(BoxExtents + FVector(Inflation));
        case ECollisionComponentType::Capsule:
            return FCollisionShape::MakeCapsule(
                CapsuleExtents.X + Inflation,
                CapsuleExtents.Y * 0.5f + Inflation);
        default:
            return FCollisionShape::MakeSphere(SphereExtent + Inflation);
    }
}
  1. Make sure collision object type is registered with the world. The show Collision renderer only draws components whose BodyInstance is actually registered. Add this to RebuildCollision():
void UMoolahCollisionProxyComponent::RebuildCollision()
{
    UpdateBodySetup();
    
    // Re-register the body instance so the world knows about updated geometry
    if (BodyInstance.IsValidBodyInstance())
    {
        BodyInstance.UpdateBodyScale(GetComponentTransform(), true);
    }
    
    RecreatePhysicsState();
    MarkRenderStateDirty();
    
    SetCollisionEnabled(ECollisionEnabled::QueryAndPhysics);
    SetCollisionProfileName(UCollisionProfile::BlockAll_ProfileName);
    SetGenerateOverlapEvents(true);
}

And with this it should work properly!

Please let me know if it works or if you need more help!