Death Ragdolling

I’ve been finishing the work off on clientside ragdolls, moving more code into c#. It’s complete now so here’s a tour on how it works.

When the player is killed, this is called serverside:

public override void OnKilled()
{
	base.OnKilled();

	BecomeRagdollOnClient();

	Controller = null;
	Camera = new SpectateRagdollCamera(); // camera that follows Player.Corpse

	CollisionsEnabled = false;
	DrawingEnabled = false;
}

BecomeRagdollOnClient is an RPC, so it gets called clientside.

[Client]
void BecomeRagdollOnClient()
{
	var ragdoll = new ModelEntity();
	ragdoll.Pos = Pos;
	ragdoll.Rot = Rot;
	ragdoll.MoveUsingPhysics = true;
	ragdoll.UsePhysicsCollision = true;
	ragdoll.SetModel( "models/citizen/citizen.vmdl" );		

	ragdoll.CopyBonesFrom( this );
	ragdoll.SetRagdollVelocityFrom( this );

	Corpse = ragdoll; // spectate camera will follow this entity
}

So that’s making the actual ragdoll entity - which is a purely clientside entity. This method gets called on every client so other players see your corpse too. (I need to fix that harcoded SetModel)

CopyBoneFrom looks like this

/// <summary>
/// Copy the bones from the target entity, but at the current entity's position and rotation
/// </summary>
public static void CopyBonesFrom( this Entity self, Entity ent )
{
	CopyBonesFrom( self, ent, self.Pos, self.Rot );
}

/// <summary>
/// Copy the bones from the target entity, but at this position and rotation instead of the target entity's
/// </summary>
public static void CopyBonesFrom( this Entity self, Entity ent, Vector3 pos, Rotation rot )
{
	//
	// This could be a slow way of doing it, if we copy this to the
	// engine we can do it faster. But for now leave it like this because
	// it's a good test on bone read/write.
	//

	if ( self is ModelEntity to )
	{
		if ( ent is ModelEntity me )
		{
			var localPos = me.Pos;
			var localRot = me.Rot.Inverse;
			var bones = Math.Min( to.BoneCount, me.BoneCount );

			for ( int i = 0; i < bones; i++ )
			{
				var tx = me.GetBoneTransform( i );

				tx.Pos = (tx.Pos - localPos) * localRot * rot + pos;
				tx.Rot = rot * (localRot * tx.Rot);

				to.SetBoneTransform( i, tx );
			}
		}
	}
}

and SetRagdollVelocityFrom looks like this

/// <summary>
/// Set the velocity of the ragdoll entity by working out the bone positions of from delta seconds ago 
/// </summary>
public static void SetRagdollVelocityFrom( this Entity self, Entity fromEnt, float delta = 0.1f, float linearAmount = 1.0f, float angularAmount = 4.0f )
{
	if ( delta == 0 ) return;

	if ( self is ModelEntity to )
	{
		if ( fromEnt is ModelEntity from )
		{
			var bonesNow = from.ComputeBones( 0.0f );
			var bonesThn = from.ComputeBones( -delta );

			for ( int i = 0; i < from.BoneCount; i++ )
			{
				var body = to.GetBonePhysicsBody( i );
				if ( body == null ) continue;

				//
				// Linear velocity
				//
				if ( linearAmount > 0 )
				{
					var center = body.LocalMassCenter;
					var c0 = bonesThn[i].TransformVector( center );
					var c1 = bonesNow[i].TransformVector( center );
					var vLinearVelocity = (c1 - c0) * (linearAmount / delta);
					body.Velocity = vLinearVelocity;
				}

				//
				// Angular velocity
				//
				if ( angularAmount > 0 )
				{
					var diff = Rotation.Difference( bonesThn[i].Rot, bonesNow[i].Rot );
					body.AngularVelocity = new Vector3( diff.x, diff.y, diff.z ) * (angularAmount / delta);
				}
			}
		}
	}
}

Something important I want to note about the way I’m doing this… these two functions are defined in addon code, in a base addon with a bunch of other utility stuff in. I think this is important because it shows that there’s no magic happening here.

It’s made easier by the fact that there are utility functions to do this shit, but you can always open that function and see what it’s doing and make your own function if you want.

I hope that goes a bit of a way to explain why I’ve been doing ragdolls for a week. I’ve been expanding the API so all this stuff can happen in addon code instead of being engine magic.

35 Likes

How would you go about networking to only one client?

According to the wiki you can pass the player as the first argument for that

1 Like

So assuming that OnKilled() is on the Player, you would call BecomeRagdollOnClient(self)? An alternative would be to have the BecomeRagdollOnClient() on the Player as well and just call that then?

I’ve been expanding the API so all this stuff can happen in addon code instead of being engine magic.

This is great and I hope it becomes something of a common theme going forward. Being able to look at how the base game approaches problems without having to dig through something like the source 1 SDK for a similar function sounds lovely.

EDIT: Didn’t mean to reply to Wyvern, originally replied to his post with a link to the RPC article on the wiki but someone else beat me to it.

3 Likes

Exactly, assuming the code is actually in the player class which considering he just sets the camera without accessing anything it probably is.

1 Like

Fall guys gamemode confirmed?

Everyone sees the ragdoll get initialized. If the ragdoll is purely clientside, would that mean it would have no effect on other serverside objects it’s touching? And would that also mean that a player can’t move the ragdoll and have that movement be seen by the other players?

Considering that the ragdoll would exist for all players, and that a player is networked, they would all see the player, and therefore move the ragdoll around. However, it may end up in a different position as it isn’t networked.

Correct, it’s a clientside ragdoll. It’s not meant to be interacted with, it’s just showing that you died. It’ll likely end up in different positions for different clients.

3 Likes

I like the little zoom the camera does when the player dies.

Camera = new SpectateRagdollCamera();

I’m assuming that’s handling the smooth transition as well? On initiation or something?

1 Like
public class SpectateRagdollCamera : BaseCamera
{
	Vector3 FocusPoint;

	public override void Activated()
	{
		base.Activated();

		FocusPoint = LastPos - GetViewOffset();
		FieldOfView = 70;
	}

	public override void Update()
	{
		var player = Player.Local as BasePlayer;
		if ( player == null ) return;

		// lerp the focus point
		FocusPoint = Vector3.Lerp( FocusPoint, GetSpectatePoint(), Time.Delta * 5.0f );

		Pos = FocusPoint + GetViewOffset();
		Rot = player.EyeRot;

		FieldOfView = (Time.Delta * 3.0f).Lerp( FieldOfView, 50 );
		Viewer = null;
	}

	public virtual Vector3 GetSpectatePoint()
	{
		var player = Player.Local as BasePlayer;
		if ( player == null ) return LastPos;

		return player.Corpse.PhysicsGroup.MassCenter;
	}

	public virtual Vector3 GetViewOffset()
	{
		var player = Player.Local as BasePlayer;
		if ( player == null ) return Vector3.Zero;

		return player.EyeRot.Forward * -130 + Vector3.Up * 20;
	}
}

This code isn’t as clean as I want it to be but this is what it’s doing

6 Likes

Looking good! How are you feeling now about more pieces of S2 being moved to addon code after spending your time with the ragdolls? More comfortable?

Also out of curiosity, does that copy bone function work between two live players ( or from entity to player instead of the other way around )? Just generally wondering about the behavior around that

How easy would it be to change it to serverside ragdolls if it’s preferable for the gamemode? If I want lootable corpses or I’ll have to drag corpses to discreet places cuz stealth and shit.

3 Likes

Serverside ragdolls haven’t been implemented yet as far as we know but it’ll probably be near identical in terms of code, just with the ragdoll being created on the server instead of the client.

1 Like

I dont normally care much for cool code examples but good god this one is delicious

1 Like

Yep, exactly my concern as well. I suppose it’s still just as possible to drop like a serverside “loot box” entity to function for that, but for dragging corpses (like we see in TTT) is something that may be more difficult to implement if serverside ragdolls aren’t implemented.

2 Likes

Can you have a ragdoll with its head bone parented to something serverside and then the rest of the physics happen clientside?

1 Like

:thinking: :thinking: :thinking: :thinking: :thinking: :thinking: :thinking: :thinking:
funny but i don’t think so

1 Like

you should be able to do that entirely clientside

1 Like