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.
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 );
}
}