Code/Player/CharacterControllerUtilities.DisableSelfCollision.cs

Component for player characters that prevents a character's own ragdoll/bone collision with itself. It ensures a ModelPhysics exists, tags the GameObject and child colliders with the owner's SteamId string, and adds a PhysicsWorld collision rule to ignore collisions between shapes that share that tag.

NetworkingFile Access
using Sandbox.Physics;

namespace AtmokineticAssets;

[Title( "Atmokinetic Assets - Character Controller Utilities" )]
[Category( "Atmokinetic Assets" )]
[Icon( "badge" )]


public sealed partial class CharacterControllerUtilities : Component, Component.INetworkSpawn
{
    /// <summary>
    /// THIS IS PRIMARILY USED FOR THE MODELPHYISICS TO GENERATE WATER SIMULATIONS/RIPPLES
    /// WITHOUT COLLIDING WITH THE CHARACTER CONTROLLER'S BOX/CAPSULE COLLIDER. 
    /// IT USES THE STEAMID AS AN IDENTIFIER.
    /// </summary>

    private string _appliedTag;
    private ModelPhysics _modelPhysics;
    private int _lastTaggedBodyCount = -1;

    public void OnNetworkSpawn( Connection owner )
    {
        EnsurePhysics();
        ApplyTag( owner );
    }

    protected override void OnStart()
    {
        EnsurePhysics();

        // Fallback for non-networked scenes: tag with the local connection
        ApplyTag( Network.Owner ?? Connection.Local );
    }

    // Create a ModelPhysics from the player's SkinnedModelRenderer if one is
    // missing, so the collision bodies this component tags actually exist.
    private void EnsurePhysics()
    {
        if ( GameObject.GetComponentInChildren<ModelPhysics>( true ).IsValid() )
            return;

        var renderer = GameObject.GetComponentInChildren<SkinnedModelRenderer>( true );
        if ( !renderer.IsValid() || renderer.Model is null )
            return;

        var physics = renderer.GameObject.AddComponent<ModelPhysics>();
        physics.Renderer = renderer;
        physics.Model = renderer.Model;
        physics.MotionEnabled = false;
    }

    protected override void OnUpdate()
    {
        // ModelPhysics creates its ragdoll bone objects lazily (and rebuilds them on
        // model reload). Instead of walking every child collider each tick, watch the
        // body count - it only changes when the ragdoll is (re)built.
        if ( string.IsNullOrEmpty( _appliedTag ) )
            return;

        if ( !_modelPhysics.IsValid() )
            _modelPhysics = GameObject.GetComponentInChildren<ModelPhysics>( true );

        int bodyCount = _modelPhysics.IsValid() ? _modelPhysics.Bodies.Count : 0;
        if ( bodyCount == _lastTaggedBodyCount )
            return;

        _lastTaggedBodyCount = bodyCount;
        TagColliders( _appliedTag );
    }

    private void ApplyTag( Connection connection )
    {
        if ( connection is null )
            return;

        var tag = connection.SteamId.ToString();

        if ( _appliedTag == tag )
            return;

        // Clean up a previously applied id (e.g. after ownership transfer)
        if ( !string.IsNullOrEmpty( _appliedTag ) )
            GameObject.Tags.Remove( _appliedTag );

        // The tag itself replicates over the network (it's GameObject state), so a
        // joining client can see it already applied from the snapshot - but the
        // collision-ignore rule lives in this machine's own PhysicsWorld, which is
        // NOT networked. So only skip re-adding the tag; always (re)register the
        // rule locally, or clients would collide with their own ragdoll forever.
        if ( !GameObject.Tags.Has( tag ) )
            GameObject.Tags.Add( tag );

        IgnoreSelfCollision( tag );
        TagColliders( tag );

        _appliedTag = tag;
    }

    // Add a rule so any two shapes both carrying this exact Steam ID tag don't
    // collide. Only this player's own colliders carry it, so its ragdoll ignores
    // its own body while still colliding with other players (whose tag differs).
    private void IgnoreSelfCollision( string tag )
    {
        var rules = Scene.PhysicsWorld?.CollisionRules;
        if ( rules is null )
            return;

        rules.Pairs[new CollisionRules.Pair( tag, tag )] = CollisionRules.Result.Ignore;
    }

    // Stamp the Steam ID tag onto every collider under the player. Shapes take
    // their tags from their GameObject, and ragdoll bone objects don't inherit
    // the root's tags, so each one has to be tagged directly.
    private void TagColliders( string tag )
    {
        foreach ( var collider in GameObject.GetComponentsInChildren<Collider>( true ) )
        {
            if ( !collider.GameObject.Tags.Has( tag ) )
                collider.GameObject.Tags.Add( tag );
        }
    }

}