Code/ShrimpleRagdoll.HitReactions.cs
namespace ShrimpleRagdolls;
public partial class ShrimpleRagdoll
{
protected struct ActiveHitReaction
{
public int BoneIndex;
public Transform DisplacedTransform;
public Transform OriginalTransform;
public Dictionary<int, Transform> ChildOriginalTransforms;
public Dictionary<int, Transform> TranslationOriginalTransforms;
public Dictionary<int, Vector3> TranslationOffsets;
public TimeUntil TimeUntilDone;
public float Duration;
}
protected List<ActiveHitReaction> ActiveHitReactions { get; set; } = new();
/// <summary>
/// When translation ends during a hit reaction, as a fraction of the total duration.
/// </summary>
[Property, Group( "Hit Reaction" ), Advanced, Range( 0f, 1f ), Step( 0.05f )]
public float HitReactionTranslationEnd { get; set; } = 0.7f;
/// <summary>
/// When rotation kicks in during a hit reaction, as a fraction of the total duration.
/// </summary>
[Property, Group( "Hit Reaction" ), Advanced, Range( 0f, 1f ), Step( 0.05f )]
public float HitReactionRotationStart { get; set; } = 1f / 3f;
/// <summary>
/// Multiplier for hit reaction translation displacement.
/// </summary>
[Property, Group( "Hit Reaction" ), Advanced, Range( 0f, 5f ), Step( 0.1f )]
public float HitReactionTranslationScale { get; set; } = 2f;
/// <summary>
/// Multiplier for hit reaction rotation displacement.
/// </summary>
[Property, Group( "Hit Reaction" ), Advanced, Range( 0f, 5f ), Step( 0.1f )]
public float HitReactionRotationScale { get; set; } = 0.5f;
public void ApplyHitReaction( Vector3 hitPosition, Vector3 force, float radius = 30f, float duration = 0.5f, float rotationStrength = 15f )
{
if ( !PhysicsWereCreated || Bodies == null || Bodies.Count == 0 )
return;
if ( !Renderer.IsValid() || !Renderer.SceneModel.IsValid() )
return;
ModelPhysics.Body? impactBody = null;
var closestDistance = float.MaxValue;
foreach ( var body in Bodies )
{
var bonePos = Renderer.SceneModel.GetBoneWorldTransform( body.Bone ).Position;
var distance = Vector3.DistanceBetween( hitPosition, bonePos );
if ( distance < closestDistance )
{
closestDistance = distance;
impactBody = body;
}
}
if ( impactBody == null )
return;
var targetBody = impactBody.Value;
if ( !IsRootBody( targetBody ) )
{
var parent = GetParentBody( targetBody );
while ( parent != null && !IsRootBody( parent.Value ) )
{
var parentPos = Renderer.SceneModel.GetBoneWorldTransform( parent.Value.Bone ).Position;
if ( Vector3.DistanceBetween( hitPosition, parentPos ) > radius )
break;
targetBody = parent.Value;
parent = GetParentBody( targetBody );
}
}
var boneWorldTransform = Renderer.SceneModel.GetBoneWorldTransform( targetBody.Bone );
var forceMagnitude = force.Length;
var forceDir = force.Normal;
var descendantCount = GetDescendants( targetBody ).Count();
var totalBodies = Bodies.Count;
var descendantRatio = totalBodies > 1 ? (float)descendantCount / (totalBodies - 1) : 0f;
var rotationBlend = 1f - descendantRatio;
Transform displacedWorld;
if ( IsRootBody( targetBody ) )
{
displacedWorld = boneWorldTransform.WithPosition( boneWorldTransform.Position + force );
}
else
{
var displacedPosition = boneWorldTransform.Position + force * HitReactionTranslationScale;
var displacedRotation = boneWorldTransform.Rotation;
var leverArm = (boneWorldTransform.Position - hitPosition).Normal;
var rotationAxis = Vector3.Cross( forceDir, leverArm ).Normal;
if ( rotationAxis.LengthSquared < 1e-4f )
rotationAxis = Vector3.Cross( forceDir, boneWorldTransform.Rotation.Up ).Normal;
if ( rotationAxis.LengthSquared > 1e-4f )
displacedRotation = Rotation.FromAxis( rotationAxis, rotationStrength * HitReactionRotationScale * forceMagnitude * rotationBlend ) * boneWorldTransform.Rotation;
displacedWorld = new Transform( displacedPosition, displacedRotation, boneWorldTransform.Scale );
}
var childOriginals = new Dictionary<int, Transform>();
foreach ( var descendant in GetDescendants( targetBody ) )
{
var childWorld = Renderer.SceneModel.GetBoneWorldTransform( descendant.Bone );
childOriginals[descendant.Bone] = childWorld;
}
var translationOriginals = new Dictionary<int, Transform>();
var translationOffsets = new Dictionary<int, Vector3>();
foreach ( var body in Bodies )
{
if ( body.Bone == targetBody.Bone || childOriginals.ContainsKey( body.Bone ) )
continue;
var bodyWorldTransform = Renderer.SceneModel.GetBoneWorldTransform( body.Bone );
var distance = Vector3.DistanceBetween( hitPosition, bodyWorldTransform.Position );
if ( distance > radius )
continue;
var falloff = 1f - (distance / radius);
falloff *= falloff;
translationOriginals[body.Bone] = bodyWorldTransform;
translationOffsets[body.Bone] = force * falloff;
}
ActiveHitReactions.Add( new ActiveHitReaction
{
BoneIndex = targetBody.Bone,
DisplacedTransform = Renderer.WorldTransform.ToLocal( displacedWorld ),
OriginalTransform = Renderer.WorldTransform.ToLocal( boneWorldTransform ),
ChildOriginalTransforms = childOriginals,
TranslationOriginalTransforms = translationOriginals,
TranslationOffsets = translationOffsets,
TimeUntilDone = duration,
Duration = duration,
} );
}
/// <summary>
/// Update all active hit reactions, called from OnUpdate
/// </summary>
internal void UpdateHitReactions()
{
if ( ActiveHitReactions.Count == 0 )
return;
if ( !Renderer.IsValid() || !Renderer.SceneModel.IsValid() )
return;
for ( var i = ActiveHitReactions.Count - 1; i >= 0; i-- )
{
var reaction = ActiveHitReactions[i];
if ( reaction.TimeUntilDone )
{
ActiveHitReactions.RemoveAt( i );
continue;
}
var fraction = reaction.TimeUntilDone.Fraction;
var positionFraction = HitReactionTranslationEnd > 0f ? MathF.Min( fraction / HitReactionTranslationEnd, 1f ) : 1f;
var positionBlend = MathF.Sin( positionFraction * MathF.PI );
var position = Vector3.Lerp( reaction.OriginalTransform.Position, reaction.DisplacedTransform.Position, positionBlend );
float rotationBlendAmount;
if ( fraction < HitReactionRotationStart )
{
rotationBlendAmount = 0f;
}
else
{
var rotationFraction = (fraction - HitReactionRotationStart) / (1f - HitReactionRotationStart);
rotationBlendAmount = MathF.Sin( rotationFraction * MathF.PI );
}
var rotation = Rotation.Slerp( reaction.OriginalTransform.Rotation, reaction.DisplacedTransform.Rotation, rotationBlendAmount );
var currentLocal = new Transform( position, rotation, reaction.OriginalTransform.Scale );
Renderer.SceneModel.SetBoneOverride( reaction.BoneIndex, in currentLocal );
if ( reaction.ChildOriginalTransforms != null && reaction.ChildOriginalTransforms.Count > 0 )
{
var originalWorld = Renderer.WorldTransform.ToWorld( reaction.OriginalTransform );
var currentWorld = Renderer.WorldTransform.ToWorld( currentLocal );
var deltaRotation = currentWorld.Rotation * originalWorld.Rotation.Inverse;
var pivot = originalWorld.Position;
var deltaPosition = currentWorld.Position - originalWorld.Position;
foreach ( var (childBoneIndex, childOriginalWorld) in reaction.ChildOriginalTransforms )
{
var rotatedPosition = pivot + deltaRotation * (childOriginalWorld.Position - pivot) + deltaPosition;
var rotatedRotation = deltaRotation * childOriginalWorld.Rotation;
var childDisplaced = new Transform( rotatedPosition, rotatedRotation, childOriginalWorld.Scale );
var childLocal = Renderer.WorldTransform.ToLocal( childDisplaced );
Renderer.SceneModel.SetBoneOverride( childBoneIndex, in childLocal );
}
}
if ( reaction.TranslationOriginalTransforms != null && reaction.TranslationOffsets != null )
{
foreach ( var (boneIndex, originalWorld) in reaction.TranslationOriginalTransforms )
{
if ( !reaction.TranslationOffsets.TryGetValue( boneIndex, out var offset ) )
continue;
var lerpedOffset = offset * positionBlend;
var displacedWorld = originalWorld.WithPosition( originalWorld.Position + lerpedOffset );
var local = Renderer.WorldTransform.ToLocal( displacedWorld );
Renderer.SceneModel.SetBoneOverride( boneIndex, in local );
}
}
}
}
private bool IsRootBody( ModelPhysics.Body body ) => GetParentBody( body ) == null;
private ModelPhysics.Body? GetParentBody( ModelPhysics.Body body )
{
var bone = Renderer.Model.Bones.AllBones[body.Bone];
var parentBone = bone.Parent;
while ( parentBone != null )
{
var parentBody = Bodies.FirstOrDefault( b => b.Bone == parentBone.Index );
if ( parentBody.Component.IsValid() )
return parentBody;
parentBone = parentBone.Parent;
}
return null;
}
private IEnumerable<ModelPhysics.Body> GetDescendants( ModelPhysics.Body root )
{
var rootBone = Renderer.Model.Bones.AllBones[root.Bone];
return Bodies.Where( b => b.Bone != root.Bone && IsDescendantOf( Renderer.Model.Bones.AllBones[b.Bone], rootBone ) );
}
private static bool IsDescendantOf( BoneCollection.Bone bone, BoneCollection.Bone ancestor )
{
var parent = bone.Parent;
while ( parent != null )
{
if ( parent.Index == ancestor.Index )
return true;
parent = parent.Parent;
}
return false;
}
}