things/player/Player.Corpse.cs

Ragdoll component and spawn helper for a Player. Clones the player's model into a non-networked corpse GameObject, sets up physics and renderers, applies impulses or explosion forces, fades out and destroys after a lifetime.

Native Interop
using Sandbox;
using Sandbox.Citizen;
using System.Collections.Generic;
using System.Linq;

public partial class Player
{
	void SpawnDeathRagdoll( Vector2 dir, float upwardAmount, float force, Vector2 velocity )
	{
		if ( !Model.IsValid() )
			return;

		PlayerDeathRagdoll.SpawnFor( this, dir, upwardAmount, force, velocity );
	}
}

public sealed class PlayerDeathRagdoll : Component
{
	const float LIFETIME = 8f;
	const float FADE_TIME = 1.5f;

	[Property] public ModelPhysics Physics { get; private set; }
	[Property] public SkinnedModelRenderer Renderer { get; private set; }

	private readonly List<ModelRenderer> _renderers = new();
	private readonly List<Color> _baseTints = new();
	private TimeSince _timeSinceSpawn;

	public static PlayerDeathRagdoll SpawnFor( Player player, Vector2 dir, float upwardAmount, float force, Vector2 velocity )
	{
		if ( !player.Model.IsValid() )
			return null;

		var corpseGo = player.Model.Clone( new CloneConfig
		{
			Name = $"{player.GameObject.Name} corpse",
			StartEnabled = true,
			Transform = player.Model.WorldTransform
		} );

		if ( corpseGo == null )
			return null;

		SetNetworkModeRecursive( corpseGo );
		corpseGo.Tags.Add( "client_death_ragdoll" );

		var corpse = corpseGo.AddComponent<PlayerDeathRagdoll>();
		corpse.Initialize( player, dir, upwardAmount, force, velocity );
		return corpse;
	}

	void Initialize( Player player, Vector2 dir, float upwardAmount, float force, Vector2 velocity )
	{
		_timeSinceSpawn = 0f;

		Renderer = GetComponent<SkinnedModelRenderer>();
		if ( !Renderer.IsValid() )
		{
			GameObject.Destroy();
			return;
		}

		var outline = GetComponent<HighlightOutline>( includeDisabled: true );
		if ( outline.IsValid() )
			outline.Destroy();

		foreach ( var collider in GetComponentsInChildren<Collider>() )
			collider.Enabled = false;

		foreach ( var renderer in GetComponentsInChildren<ModelRenderer>() )
		{
			_renderers.Add( renderer );
			_baseTints.Add( renderer.Tint );
		}

		var animationHelper = GetComponent<CitizenAnimationHelper>();
		if ( animationHelper.IsValid() )
			animationHelper.Enabled = false;

		Renderer.UseAnimGraph = false;

		Physics = GetOrAddComponent<ModelPhysics>();
		//Physics.Enabled = false;
		Physics.Model = Renderer.Model;
		Physics.Renderer = Renderer;
		Physics.CopyBonesFrom( player.ModelRenderer, teleport: true );
		Physics.Enabled = true;

		ApplyImpulse( player, dir, upwardAmount, force, velocity );
	}

	protected override void OnUpdate()
	{
		base.OnUpdate();

		if ( _timeSinceSpawn >= LIFETIME )
		{
			GameObject.Destroy();
			return;
		}

		if ( _timeSinceSpawn < LIFETIME - FADE_TIME )
			return;

		var alpha = Utils.Map( _timeSinceSpawn, LIFETIME - FADE_TIME, LIFETIME, 1f, 0f, EasingType.QuadIn );
		for ( int i = 0; i < _renderers.Count; i++ )
		{
			if ( !_renderers[i].IsValid() )
				continue;

			var tint = _baseTints[i];
			_renderers[i].Tint = tint.WithAlpha( tint.a * alpha );
		}
	}

	void ApplyImpulse( Player player, Vector2 dir, float upwardAmount, float force, Vector2 velocity )
	{
		if ( Physics == null || Physics.Bodies.Count <= 0 )
			return;

		if ( !(dir.LengthSquared > 0f) )
			dir = Utils.GetRandomVector();

		var hitHeight = player.Height * Game.Random.Float( 0f, 1f );
		var hitPos = player.WorldPosition + Vector3.Up * hitHeight;
		//var impulse = new Vector3(
		//	dir.x * Game.Random.Float( 140f, 240f ),
		//	dir.y * Game.Random.Float( 140f, 240f ),
		//	Game.Random.Float( 120f, 230f )
		//);

		float FORCE_SCALE = 1000f;
		Vector3 impulse = new Vector3( dir.x, dir.y, upwardAmount ) * force * FORCE_SCALE;

		float PLAYER_VELOCITY_SCALE = 2f;
		//impulse += new Vector3( player.TotalVelocity.x, player.TotalVelocity.y, 0f ) * PLAYER_VELOCITY_SCALE;

		impulse += (Vector3)velocity * PLAYER_VELOCITY_SCALE;

		ApplyImpulseToBodies( hitPos, impulse, 0.1f, 1.5f );
	}

	public void ApplyExplosionImpulse( Vector2 explosionPos, float radius, float force, bool inward = false )
	{
		if ( Physics == null || Physics.Bodies.Count <= 0 )
			return;

		var offset = (Vector2)WorldPosition - explosionPos;
		var distSqr = offset.LengthSquared;
		var radiusSqr = radius * radius;
		if ( distSqr >= radiusSqr )
			return;

		var dir = distSqr > 0.001f
			? offset.Normal
			: Utils.GetRandomVector();

		if ( inward )
			dir = -dir;

		var percent = Utils.Map( distSqr, 0f, radiusSqr, 1f, 0f );
		var hitPos = WorldPosition;

		const float EXPLOSION_FORCE_SCALE = 1000f;
		var upwardAmount = 0.1f + percent * Game.Random.Float( 0.8f, 1.2f );
		var impulse = new Vector3( dir.x, dir.y, upwardAmount ) * force * percent * EXPLOSION_FORCE_SCALE;

		ApplyImpulseToBodies( hitPos, impulse, 0.75f, 1.25f );
	}

	void ApplyImpulseToBodies( Vector3 hitPos, Vector3 impulse, float randomMin, float randomMax )
	{
		if ( Physics == null || Physics.Bodies.Count <= 0 )
			return;

		foreach ( var body in Physics.Bodies )
		{
			var rand = Game.Random.Float( randomMin, randomMax );

			body.Component.Sleeping = false;
			body.Component.ApplyImpulseAt( hitPos, impulse * rand );
		}
	}

	static void SetNetworkModeRecursive( GameObject gameObject )
	{
		gameObject.NetworkMode = NetworkMode.Never;

		foreach ( var child in gameObject.Children )
			SetNetworkModeRecursive( child );
	}
}