Grab system for ragdolls. RagdollGrab represents a spring hold on one body with force, rotation, limpness and break controls; the ShrimpleRagdoll partial manages active grabs, haul state and applies physics forces each tick.
namespace ShrimpleRagdolls;
/// <summary>
/// A hold on one of a ragdoll's bodies. Feed it a target every frame with <see cref="MoveTo(Vector3)"/> and let go with <see cref="Release"/>.<br />
/// It springs at the grabbed point capped at <see cref="MaxForce"/>, and capping a force rather than an acceleration is what makes their weight matter.
/// </summary>
public sealed class RagdollGrab
{
/// <summary>
/// The ragdoll being held
/// </summary>
public ShrimpleRagdoll Ragdoll { get; internal set; }
/// <summary>
/// Bone index of the body being held
/// </summary>
public int Bone { get; internal set; }
/// <summary>
/// The rigidbody being held
/// </summary>
public Rigidbody Body { get; internal set; }
/// <summary>
/// Where on the body it's held, in the body's local space
/// </summary>
public Vector3 LocalAnchor { get; set; }
/// <summary>
/// Where the grab wants the held point. Set this every frame from your hand
/// </summary>
public Transform Target { get; set; }
/// <summary>
/// Also hold the body's orientation, which is what stops them punching with an arm you're holding
/// </summary>
public bool ControlRotation { get; set; }
/// <summary>
/// The most force the grab can pull with, set from carryMass times gravity
/// </summary>
public float MaxForce { get; set; } = 200f * 800f;
/// <summary>
/// The most the grab can angularly accelerate the body, in radians per second squared<br />
/// Keep it modest, a saturated angular correction can't damp itself and spins
/// </summary>
public float MaxAngularAcceleration { get; set; } = 40f;
/// <summary>
/// Spring frequency of the hold in hz, higher is a tighter grip
/// </summary>
public float Frequency { get; set; } = 8f;
/// <summary>
/// Damping ratio of the hold, 1 is critically damped
/// </summary>
public float Damping { get; set; } = 1f;
/// <summary>
/// How limp the held limb goes, 1 means it stops fighting you entirely
/// </summary>
public float Limpness { get; set; } = 1f;
/// <summary>
/// How much limpness carries into each neighbouring bone, so an arm hangs off a held wrist
/// </summary>
public float ChainFalloff { get; set; } = 0.45f;
/// <summary>
/// How many joints out the limpness spreads, negative for the whole skeleton
/// </summary>
public int ChainDepth { get; set; } = 2;
/// <summary>
/// Also go limp down the chain from the held bone, not just up it
/// </summary>
public bool LimpChildren { get; set; } = true;
/// <summary>
/// The grab breaks if the held point gets this far from <see cref="Target"/>, 0 to never break<br />
/// Be generous, a hauled limb legitimately trails a long way behind the hand
/// </summary>
public float BreakDistance { get; set; }
/// <summary>
/// How hard they've fought the grab, breaks free at 1
/// </summary>
public float Struggle { get; internal set; }
/// <summary>
/// The force the grab pulled with last tick, negated, so a heavy victim can drag the grabber around
/// </summary>
public Vector3 ReactionForce { get; internal set; }
/// <summary>
/// True once the grab has ended, whether it was let go or broken out of
/// </summary>
public bool Released { get; internal set; }
/// <summary>
/// True while the grab is still holding a valid body
/// </summary>
public bool IsHolding => !Released && Body.IsValid();
/// <summary>
/// The held point in world space
/// </summary>
public Vector3 WorldAnchor => Body.IsValid() ? Body.WorldTransform.PointToWorld( LocalAnchor ) : Target.Position;
/// <summary>
/// Move the point the grab pulls toward, keeping the current target rotation
/// </summary>
public void MoveTo( Vector3 position ) => Target = Target.WithPosition( position );
/// <summary>
/// Move the transform the grab pulls toward
/// </summary>
public void MoveTo( Transform target ) => Target = target;
/// <summary>
/// Fight the grab, breaking it once the accumulated struggle reaches 1
/// </summary>
public void ApplyStruggle( float amount ) => Struggle = Math.Clamp( Struggle + amount, 0f, 1f );
/// <summary>
/// Let go
/// </summary>
public void Release() => Ragdoll?.ReleaseGrab( this );
// Which bodies this grab makes limp and by how much, walked once and reused
internal List<(int Index, float Strength)> ChainCache;
internal float CachedLimpness = float.NaN;
internal float CachedFalloff;
internal int CachedDepth;
internal bool CachedChildren;
internal bool ChainCacheIsStale => ChainCache == null
|| CachedLimpness != Limpness
|| CachedFalloff != ChainFalloff
|| CachedDepth != ChainDepth
|| CachedChildren != LimpChildren;
}
public partial class ShrimpleRagdoll
{
/// <summary>
/// How much of their anchoring to the animated position they lose while being hauled<br />
/// Without it, lifting someone means beating every body's drive at once, several times their bodyweight
/// </summary>
[Advanced, Property, Group( "Driven" ), Range( 0f, 1f ), Step( 0.05f )]
public float HaulLimpness { get; set; } = 0.95f;
/// <summary>
/// How much of their articulation they lose while being hauled, kept low so they still struggle
/// </summary>
[Advanced, Property, Group( "Driven" ), Range( 0f, 1f ), Step( 0.05f )]
public float HaulMotorLimpness { get; set; } = 0.35f;
/// <summary>
/// How fast a held ragdoll works itself free, in struggle per second at full strength. Scaled by
/// <see cref="StrengthMultiplier"/> so a stunned npc can't wriggle out. 0 to disable
/// </summary>
[Advanced, Property, Group( "Driven" )]
public float StruggleRate { get; set; }
/// <summary>
/// How much of their own bodyweight is currently being pulled by grabs, 0 to 1<br />
/// Multiply your npc's locomotion by 1 minus this and being held stops them walking off
/// </summary>
public float HaulFraction { get; private set; }
// Grip stretch, not their displacement. Force alone misses a light limb that follows your hand without
// loading the spring, and displacement can never start a haul near the root
private const float HaulStretchDistance = 16f;
private const float HaulBlendTime = 0.25f;
/// <summary>
/// Every grab currently holding this ragdoll
/// </summary>
public IReadOnlyList<RagdollGrab> Grabs => _grabs;
private readonly List<RagdollGrab> _grabs = new();
/// <summary>
/// Fires when something takes hold of this ragdoll
/// </summary>
public Action<RagdollGrab> Grabbed { get; set; }
/// <summary>
/// Fires when a grab ends. Check <see cref="RagdollGrab.Struggle"/> to tell a break from a let go
/// </summary>
public Action<RagdollGrab> GrabReleased { get; set; }
/// <summary>
/// True if anything is holding this ragdoll, synced so proxies can react too
/// </summary>
[Sync]
public bool IsBeingGrabbed { get; private set; }
/// <summary>
/// True if this bone is being held
/// </summary>
public bool IsBoneGrabbed( int boneIndex )
{
foreach ( var grab in _grabs )
{
if ( grab.Bone == boneIndex )
return true;
}
return false;
}
/// <summary>
/// Take hold of one of this ragdoll's bodies, owner only<br />
/// Take ownership first if the grabber should be allowed to grab a ragdoll it doesn't own<br />
/// <paramref name="carryMass"/> is how much mass this grip can hold up against gravity
/// </summary>
public RagdollGrab GrabBody( ModelPhysics.Body body, Vector3 worldPoint, float carryMass = 200f )
{
if ( IsProxy || !body.Component.IsValid() || !body.Component.PhysicsBody.IsValid() || !EnsureDrivenBuffers() )
return null;
var grab = new RagdollGrab
{
Ragdoll = this,
Bone = body.Bone,
Body = body.Component,
LocalAnchor = body.Component.WorldTransform.PointToLocal( worldPoint ),
Target = new Transform( worldPoint, body.Component.WorldRotation ),
MaxForce = carryMass * Scene.PhysicsWorld.Gravity.Length,
};
_grabs.Add( grab );
WakePhysics();
Grabbed?.Invoke( grab );
return grab;
}
/// <summary>
/// Take hold of a bone by name
/// </summary>
public RagdollGrab GrabBone( string boneName, Vector3 worldPoint, float carryMass = 200f )
{
var body = GetBodyByBoneName( boneName );
return body.HasValue ? GrabBody( body.Value, worldPoint, carryMass ) : null;
}
/// <summary>
/// Take hold of whichever body is closest to a world point
/// </summary>
/// <param name="worldPoint">Where you grabbed</param>
/// <param name="radius">How far the nearest body may be, 0 for no limit</param>
/// <param name="carryMass">How much mass this grip can hold up against gravity</param>
public RagdollGrab GrabNearest( Vector3 worldPoint, float radius = 24f, float carryMass = 200f )
{
var body = GetNearestBody( worldPoint, out var distance );
if ( body is not { } target || (radius > 0f && distance > radius) )
return null;
return GrabBody( target, worldPoint, carryMass );
}
/// <summary>
/// Let go of a grab, easing the limb's strength back
/// </summary>
public void ReleaseGrab( RagdollGrab grab )
{
if ( grab == null || grab.Released )
return;
grab.Released = true;
_grabs.Remove( grab );
GrabReleased?.Invoke( grab );
}
/// <summary>
/// Let go of everything holding this ragdoll
/// </summary>
public void ReleaseAllGrabs()
{
for ( var i = _grabs.Count - 1; i >= 0; i-- )
ReleaseGrab( _grabs[i] );
}
/// <summary>
/// Drop any grabs we're still holding after losing ownership. They'd never update again, so they would
/// never release either, and the limb would sit limp forever
/// </summary>
internal void ReleaseGrabsIfProxy()
{
if ( IsProxy && _grabs.Count > 0 )
ReleaseAllGrabs();
}
private void UpdateGrabs()
{
// Rebuilt from scratch every tick, so letting go recovers on its own
for ( var i = 0; i < _grabLimpTarget.Length; i++ )
_grabLimpTarget[i] = 1f;
// Cheap to assign every tick, a sync setter short circuits when the value hasn't changed
IsBeingGrabbed = _grabs.Count > 0;
if ( _grabs.Count == 0 )
{
HaulFraction = 0f;
return;
}
UpdateHaul();
var delta = Time.Delta;
var struggle = StruggleRate * StrengthMultiplier * delta;
for ( var i = _grabs.Count - 1; i >= 0; i-- )
{
var grab = _grabs[i];
if ( !grab.IsHolding )
{
ReleaseGrab( grab );
continue;
}
if ( struggle > 0f )
grab.ApplyStruggle( struggle );
var anchor = grab.WorldAnchor;
var slipped = grab.BreakDistance > 0f
&& Vector3.DistanceBetween( anchor, grab.Target.Position ) > grab.BreakDistance;
if ( grab.Struggle >= 1f || slipped )
{
ReleaseGrab( grab );
continue;
}
ApplyGrabLimpness( grab );
ApplyGrabForce( grab, anchor, delta );
}
}
/// <summary>
/// Works out how much of their weight the grabs are taking and drops their anchoring to match, so
/// lifting someone stops being a fight against every limb's drive at once
/// </summary>
private void UpdateHaul()
{
var weight = Mass * Scene.PhysicsWorld.Gravity.Length;
var pull = 0f;
var stretch = 0f;
foreach ( var grab in _grabs )
{
if ( !grab.IsHolding )
continue;
pull += grab.ReactionForce.Length;
stretch = MathF.Max( stretch, Vector3.DistanceBetween( grab.WorldAnchor, grab.Target.Position ) );
}
var byForce = weight > 0f ? pull / weight : 0f;
var target = Math.Clamp( MathF.Max( byForce, stretch / HaulStretchDistance ), 0f, 1f );
HaulFraction = HaulFraction.Approach( target, Time.Delta / HaulBlendTime );
}
/// <summary>
/// Writes a grab's limpness into the grab strength layer, taking the limpest value where grabs overlap
/// </summary>
private void ApplyGrabLimpness( RagdollGrab grab )
{
if ( grab.ChainCacheIsStale )
{
grab.ChainCache ??= new List<(int, float)>();
grab.ChainCache.Clear();
grab.ChainCache.AddRange( EnumerateChain( grab.Bone, 1f - Math.Clamp( grab.Limpness, 0f, 1f ),
grab.ChainFalloff, grab.ChainDepth, grab.LimpChildren ) );
grab.CachedLimpness = grab.Limpness;
grab.CachedFalloff = grab.ChainFalloff;
grab.CachedDepth = grab.ChainDepth;
grab.CachedChildren = grab.LimpChildren;
}
foreach ( var (index, chainStrength) in grab.ChainCache )
_grabLimpTarget[index] = MathF.Min( _grabLimpTarget[index], chainStrength );
}
private void ApplyGrabForce( RagdollGrab grab, Vector3 anchor, float delta )
{
var rigidbody = grab.Body;
var physics = rigidbody.PhysicsBody;
if ( !physics.IsValid() )
return;
rigidbody.Sleeping = false;
if ( grab.Frequency > 0f && grab.MaxForce > 0f )
{
var velocity = physics.GetVelocityAtPoint( anchor );
var acceleration = SpringAcceleration( grab.Target.Position - anchor, velocity, grab.Frequency, grab.Damping, delta );
var force = (acceleration * physics.Mass).ClampLength( grab.MaxForce );
// At the grabbed point rather than the mass centre, so the limb swings off the grip
physics.ApplyForceAt( anchor, force );
grab.ReactionForce = -force;
}
else
{
grab.ReactionForce = Vector3.Zero;
}
if ( !grab.ControlRotation || grab.MaxAngularAcceleration <= 0f )
return;
var rotationError = ToRotationVector( grab.Target.Rotation * rigidbody.WorldRotation.Inverse );
var angular = SpringAcceleration( rotationError, rigidbody.AngularVelocity, grab.Frequency, grab.Damping, delta );
rigidbody.AngularVelocity += angular.ClampLength( grab.MaxAngularAcceleration ) * delta;
}
}