Method for setting up collision capsule automatically to match character mesh

Figured I’d share a method I use to set up collision capsules automatically for a game whose characters vary in size:

Derive a character class from ACharacter, and in its header, add:


//////////////////////////////////////////////////////////////////////////
    // In-Editor Data Verification
#if WITH_EDITOR
    // Editor specific

protected:
    /** Set dependent properties */
    void PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent) override;

    /** Sets this pawn's collision capsule according to its base size and adjusts its relative location to accommodate foot-placed root. */
    void UpdateCollisionCapsuleToFitMesh();

    /** Sets this pawn's relative location from its collision capsule halfHeight. */
    void UpdateRelativeLocationToFitCapsule();

#endif //WITH_EDITOR

In the implementation:


// EDITOR PROPERTY ADJUSTMENTS ************************************************
#if WITH_EDITOR

void AMyGameCharacter::PostEditChangeProperty(FPropertyChangedEvent& PropertyChangedEvent)
{
    Super::PostEditChangeProperty(PropertyChangedEvent);
    const UProperty* PropertyThatChanged{ PropertyChangedEvent.Property };
    const FName PropertyName{ PropertyThatChanged != nullptr ? PropertyThatChanged->GetFName() : NAME_None };

    // User has assigned this pawn's mesh. Provide a viable starting capsule.
    if (PropertyName == FName(TEXT("Mesh")))
    {
        UpdateCollisionCapsuleToFitMesh();
    }

    // User has modified this pawn's capsule component. Keep the mesh relative location in sync.
    else if (PropertyName == FName(TEXT("CapsuleComponent")))
    {
        UpdateRelativeLocationToFitCapsule();
    }
}

void AMyGameCharacter::UpdateCollisionCapsuleToFitMesh()
{
    // Find this character's mesh bounds and scale.
    if (GetMesh() && GetMesh()->SkeletalMesh != nullptr)
    {
        const FBoxSphereBounds MeshBounds{ GetMesh()->SkeletalMesh->GetBounds() };
        const FVector Scale3D{ GetMesh()->RelativeScale3D };

        // set collision cylinder appropriately to mesh size
        const float newRadius{ ((MeshBounds.BoxExtent.X + MeshBounds.BoxExtent.Y) / 2.0f) * 0.5f * FMath::Max(Scale3D.X, Scale3D.Y) };
        const float BoxHeightConversionFactor{ 1.324f };        // conversion factor based on known good capsule height from UE3 prototype.
        const float newHalfHeight{ MeshBounds.BoxExtent.Z * 0.5f * BoxHeightConversionFactor * Scale3D.Z };

        // set the currently in-use cylinders
        GetCapsuleComponent()->SetCapsuleSize(newRadius, newHalfHeight);
        GetCapsuleComponent()->SetRelativeScale3D(FVector(1.0f, 1.0f, 1.0f));

        UpdateRelativeLocationToFitCapsule();
    }
    else
    {
        UE_LOG(LogMyGameCharacter, Error, TEXT("Attempted to update collision capsule on pawn %s, but mesh wasn't valid.")
            , *GetNameSafe(this));
    }
}

void AMyGameCharacter::UpdateRelativeLocationToFitCapsule()
{
    // Update the Mesh's relative location, taking into account foot-origin vs. center-origin characters.
    FVector NewRelativeLocation{ GetClass()->GetDefaultObject<AMyGameCharacter>()->GetMesh()->RelativeLocation };
    NewRelativeLocation.Z = -(GetCapsuleComponent()->GetScaledCapsuleHalfHeight());
    GetMesh()->SetRelativeLocation(NewRelativeLocation);
}

#endif    // WITH_EDITOR
// ****************************************************************************

Here’s what’s going on:
When you make a change to the mesh, PostEditChangeProperty will call UpdateCollisionCapsuleToFitMesh(), which uses the mesh bounds to determine what the new capsule radius and halfheight should be. It then calls UpdateRelativeLocationToFitCapsule() to place the capsule appropriately to keep the character on the ground. If you update the collision capsule, just the latter method is called.

I’ve found this method useful on a few projects that involved a lot of non-human and strangely-sized characters, so I figured I’d share.

Thanks for providing the code! In case your model differs a lot in x and y length, the following calculation of the radius might fit better (only for testing since big of distance between collision and mesh on the short side of the mesh).


const float newRadius{ FMath::Max(MeshBounds.BoxExtent.X, MeshBounds.BoxExtent.Y) * 0.5f * FMath::Max(Scale3D.X, Scale3D.Y) };