using System.Collections;
using System.Collections.Generic;
using Invector.vCamera;
using UnityEngine;
#if INVECTOR_PLAYABLESANIMATOR
using InvectorCore.Playables;
#endif
namespace Invector.vCharacterController
{
///
/// Drives the character between animated and physics-simulated poses.
///
/// Lifecycle:
/// animated -> Mecanim/Playables own the pose, every body part is kinematic.
/// ragdolled -> the animation backend is off, physics owns the pose, hips are reparented
/// under the shared object container so the root can move freely.
/// blendToAnim -> the backend is back on and blends the last
/// simulated pose into the get-up animation over .
///
/// The exit from ragdolled is gated by , which waits for the
/// chest to settle, plus the flag.
///
[vClassHeader("RAGDOLL SYSTEM", true, "ragdollIcon", true, "Every gameobject children of the character must have their tag added in the IgnoreTag List.")]
public class vRagdoll : vMonoBehaviour
{
#region public variables
[vEditorToolbar("Debug")]
public bool startRagdolled = false;
[vButton("Active Ragdoll And Keep Ragdolled", "ActivateRagdollWithDelayToGetUp", typeof(vRagdoll))]
[vButton("Active Ragdoll", "ActivateRagdoll", typeof(vRagdoll))]
[SerializeField] protected float debugTimeToStayRagdolled;
[vEditorToolbar("Settings")]
public LayerMask groundLayer = 1 << 0;
public bool keepRagdolled;
public bool invertGetUpAnim;
public bool _ignoreGetUpAnimation;
public bool removePhysicsAfterDie;
[Tooltip("SHOOTER: Keep false to use detection hit on each children collider, don't forget to change the layer to BodyPart from hips to all children. MELEE: Keep true to only hit the main Capsule Collider.")]
public bool disableColliders = false;
public AudioSource collisionSource;
public AudioClip collisionClip;
[vEditorToolbar("Settings")]
[Tooltip("State played when getting up facing upwards. Must match an Animator state and, when PlayablesAnimator drives the character, a PDM row name.")]
public string standUpFromBackState = "StandUp@FromBack";
[Tooltip("State played when getting up facing downwards. Must match an Animator state and, when PlayablesAnimator drives the character, a PDM row name.")]
public string standUpFromBellyState = "StandUp@FromBelly";
[Header("Add Tags for Weapons or Itens here:")]
public List ignoreTags = new List() { "Weapon", "Ignore Ragdoll" };
[System.NonSerialized] public AnimatorStateInfo stateInfo;
[Range(0, 2f)]
[Tooltip("The velocity of the parent rigidbody will be applied to the Ragdoll when enabled, creating a more realistic physics")]
public float horizontalMultiplier = 1f;
[Range(0, 2f)]
public float verticalMultiplier = 0.5f;
#endregion
#region tuning constants
/// How long the last simulated pose is blended into the get-up animation.
protected const float RagdollToMecanimBlendTime = 0.25f;
/// Grace period before the blend starts, while the backend picks up the get-up state.
protected const float MecanimToGetUpTransitionTime = 0.05f;
/// Delay before the character regains control, measured from the end of the ragdoll.
protected const float ResetPlayerDelay = 1.1f;
/// How long the ragdoll simulates before the settle check starts.
protected const float StabilizeGraceTime = 2f;
/// Chest speed under which the ragdoll counts as settled.
protected const float StabilizeSpeedThreshold = 0.1f;
/// Stricter settle threshold used while dead, so corpses do not twitch back to life.
protected const float DeadStabilizeSpeedThreshold = 0.0001f;
/// Minimum interval between two ragdoll impact sounds.
protected const float CollisionSoundCooldown = 0.2f;
/// Impact speed under which a collision is too soft to be worth a sound.
protected const float MinCollisionImpactForce = 1f;
/// Maps impact speed to audio volume.
protected const float CollisionVolumeScale = 0.05f;
/// Vertical span of the ground probe cast under the blended root position.
protected const float GroundProbeDistance = 1f;
/// Deferral so the hips are reparented after the disable frame has settled.
protected const float ReparentDelay = 0.01f;
/// Deferral used by so the character initializes first.
protected const float StartRagdolledDelay = 0.1f;
/// How long the camera eases from the ragdoll pivot back to the normal player pivot.
protected const float CameraOffsetReturnSmoothTime = 0.2f;
/// Reused by the ground probe so the per-frame blend does not allocate.
static readonly RaycastHit[] GroundProbeHits = new RaycastHit[8];
#endregion
#region private variables
internal vICharacter iChar;
protected Animator animator;
#if INVECTOR_PLAYABLESANIMATOR
protected PlayablesAnimator playablesAnimator;
// True only when this ragdoll is the one that turned PlayablesAnimator off, so getting up
// restores the backend the character was actually running rather than forcing it on.
private bool suspendedPlayablesBackend;
/// True when PlayablesAnimator, not the Animator Controller, is driving the character.
protected virtual bool UsingPlayablesBackend => playablesAnimator && playablesAnimator.isActiveAndEnabled;
#endif
protected Rigidbody _parentRigb;
internal Transform characterChest, characterHips;
[vEditorToolbar("Debug")]
[vReadOnly]
public bool isActive;
[vReadOnly]
public bool inStabilize;
[vReadOnly]
public RagdollState state = RagdollState.animated;
public bool ignoreGetUpAnimation { get => _ignoreGetUpAnimation; set => _ignoreGetUpAnimation = value; }
protected Vector3 localY { get; set; }
/// True while physics, and not the animation backend, owns the pose.
bool ragdolled
{
get => state != RagdollState.animated;
set
{
if (value)
{
EnterRagdolledState();
}
else
{
ExitRagdolledState();
}
}
}
//Possible states of the ragdoll
public enum RagdollState
{
animated, //Mecanim is fully in control
ragdolled, //Mecanim turned off, physics controls the ragdoll
blendToAnim //Mecanim in control, but LateUpdate() is used to partially blend in the last ragdolled pose
}
//A helper variable to store the time when we transitioned from ragdolled to blendToAnim state
float ragdollingEndTime = -100;
//Additional vectors for storing the pose the ragdoll ended up in.
Vector3 ragdolledHipPosition, ragdolledHeadPosition, ragdolledFeetPosition;
readonly List bodyParts = new List();
// Tag lookup used inside the per-bone loops; the serialized list stays the authoring surface.
readonly HashSet ignoreTagSet = new HashSet();
// used to reset parent of hips
Transform hipsParent;
// Resolved once so neither the blend nor the stabilizer queries the Animator per frame.
BodyPart hipsBodyPart, chestBodyPart;
Transform headBone, leftFootBone, rightFootBone, leftToesBone, rightToesBone;
//used to control damage frequency
bool inApplyCollisionSound;
GameObject _ragdollContainer;
Coroutine stabilizerRoutine, resetPlayerRoutine;
Vector3 ragdollCameraTargetOffset;
bool hasRagdollCameraTargetOffset;
class BodyPart
{
public readonly Transform transform;
public readonly Rigidbody rigidbody;
public readonly Collider collider;
public Vector3 storedPosition;
public Quaternion storedRotation;
public BodyPart(Transform t)
{
transform = t;
rigidbody = t.GetComponent();
collider = t.GetComponent();
}
}
#endregion
#region unity lifecycle
protected virtual void Start()
{
_parentRigb = GetComponent();
iChar = GetComponent();
if (iChar != null)
{
iChar.onActiveRagdoll.AddListener(ActivateRagdoll);
}
EnsureCollisionSource();
LoadBodyPart();
if (startRagdolled)
{
Invoke(nameof(ActivateRagdoll), StartRagdolledDelay);
}
_ragdollContainer = vObjectContainer.root.gameObject;
}
protected virtual void OnDisable()
{
inStabilize = false;
stabilizerRoutine = null;
resetPlayerRoutine = null;
ClearRagdollCameraTargetOffset();
Invoke(nameof(ChangeToHipsParent), ReparentDelay);
setKinematic(true);
setCollider(true);
}
protected virtual void LateUpdate()
{
if (animator == null)
{
return;
}
// The pose advances once per rendered frame, so the ragdoll-to-animation blend runs every
// frame too. Stepping it at 50Hz against a pose moving at the frame rate reads as stutter
// on the way up.
RagdollBehaviour();
// The body rigidbodies interpolate between physics steps. Follow the interpolated hips at
// render frequency as well; doing this only in FixedUpdate makes the root visibly step while
// the limbs are smooth.
FollowRagdollHips();
}
protected virtual void FixedUpdate()
{
if (!isActive)
{
return;
}
if (characterHips.parent != _ragdollContainer.transform)
{
characterHips.SetParent(_ragdollContainer.transform);
}
if (CanLeaveRagdoll())
{
ragdolled = false;
RestartResetPlayerRoutine();
}
}
protected virtual void FollowRagdollHips()
{
if (ragdolled && !IsAnimationBackendDriving())
{
// Physics owns the pose, so the root follows the simulated hips, not the animation.
transform.position = characterHips.position;
// vThirdPersonCamera runs before this component's LateUpdate. Applying the cached
// correction here means its next frame sees the moved root and the offset together.
ApplyRagdollCameraTargetOffset();
}
}
protected virtual void OnDestroy()
{
try
{
if (_ragdollContainer && characterHips && characterHips.parent == _ragdollContainer.transform)
{
Destroy(characterHips.gameObject);
}
}
catch (UnityException e)
{
Debug.LogWarning(e.Message, gameObject);
}
}
#endregion
#region state transitions
/// Hands the pose to physics and suspends whichever animation backend is driving.
protected virtual void EnterRagdolledState()
{
if (state != RagdollState.animated)
{
return;
}
setKinematic(false); //allow the ragdoll RigidBodies to react to the environment
setCollider(false);
CacheRagdollCameraTargetOffset();
SuspendAnimationBackend();
state = RagdollState.ragdolled;
}
///
/// Returns the pose to the animation backend. From this
/// snapshots the simulated pose and starts the get-up blend; from any other state it only
/// restores the physics and animation flags.
///
protected virtual void ExitRagdolledState()
{
characterHips.SetParent(hipsParent);
isActive = false;
ragdollingEndTime = Time.time;
if (state == RagdollState.ragdolled)
{
setKinematic(true); //disable gravity etc.
setCollider(true);
StoreRagdolledPose();
float lastlocalY = characterHips.TransformDirection(localY).y;
if (invertGetUpAnim)
{
lastlocalY = -lastlocalY;
}
// Capture the selected state before the backend wakes up. PlayablesAnimator deliberately
// keeps a queued PlayState while disabled; queuing now means its first scheduler tick after
// the ragdoll handoff must commit get-up instead of evaluating locomotion/Idle first.
string getUpState = lastlocalY > 0 ? standUpFromBackState : standUpFromBellyState;
bool shouldPlayGetUp = !ignoreGetUpAnimation && gameObject.activeInHierarchy;
bool getUpQueuedWhilePlayablesSuspended = false;
#if INVECTOR_PLAYABLESANIMATOR
if (shouldPlayGetUp && suspendedPlayablesBackend && playablesAnimator)
{
getUpQueuedWhilePlayablesSuspended = PlayGetUpAnimation(getUpState);
}
#endif
ResumeAnimationBackend();
state = RagdollState.blendToAnim;
// Animator Controller states cannot be played while their Animator is disabled. PDM states
// are already queued above; if that queue could not be created, retry after re-enabling so a
// malformed/missing PDM row is reported instead of silently falling through to Idle.
if (shouldPlayGetUp && !getUpQueuedWhilePlayablesSuspended && !PlayGetUpAnimation(getUpState))
{
Debug.LogError($"[{nameof(vRagdoll)}] Could not play get-up state '{getUpState}'. " +
"Configure the state in the Animator Controller or an enabled PDM row with the same name.", this);
}
}
else if (state == RagdollState.animated)
{
setKinematic(true); //disable gravity etc.
setCollider(true);
ResumeAnimationBackend();
}
}
/// Snapshots the pose the ragdoll settled in, so the blend has a source to leave from.
protected virtual void StoreRagdolledPose()
{
for (int i = 0; i < bodyParts.Count; i++)
{
var b = bodyParts[i];
b.storedRotation = b.transform.rotation;
b.storedPosition = b.transform.position;
}
ragdolledFeetPosition = 0.5f * (leftToesBone.position + rightToesBone.position);
ragdolledHeadPosition = headBone.position;
ragdolledHipPosition = characterHips.position;
}
/// True when the ragdoll has settled and nothing is holding the character down.
protected virtual bool CanLeaveRagdoll()
{
return ragdolled && !inStabilize && !keepRagdolled && !(iChar != null && iChar.isDead);
}
/// True while the Animator is enabled and able to author the pose.
protected virtual bool IsAnimationBackendDriving()
{
return animator != null && animator.isActiveAndEnabled;
}
protected virtual void SuspendAnimationBackend()
{
animator.enabled = false; //disable animation
#if INVECTOR_PLAYABLESANIMATOR
// Only stop the backend that was actually driving: disabling PlayablesAnimator
// in the inspector to exercise the Animator Controller must survive a ragdoll.
suspendedPlayablesBackend = UsingPlayablesBackend;
if (suspendedPlayablesBackend)
{
playablesAnimator.enabled = false;
}
#endif
}
protected virtual void ResumeAnimationBackend()
{
animator.enabled = true; //enable animation
#if INVECTOR_PLAYABLESANIMATOR
if (suspendedPlayablesBackend)
{
playablesAnimator.enabled = true;
suspendedPlayablesBackend = false;
}
#endif
}
protected virtual void ChangeToHipsParent()
{
if (characterHips.parent != hipsParent)
{
characterHips.position = transform.position;
characterHips.SetParent(hipsParent);
}
}
///
/// Captures the difference between the normal character pivot and the hips. It is applied only
/// after the root first follows the hips, avoiding a one-frame camera dip before that move.
///
protected virtual void CacheRagdollCameraTargetOffset()
{
hasRagdollCameraTargetOffset = characterHips != null;
if (hasRagdollCameraTargetOffset)
{
ragdollCameraTargetOffset = transform.position - characterHips.position;
}
}
/// Keeps the camera pivot at its pre-ragdoll height while the root follows the hips.
protected virtual void ApplyRagdollCameraTargetOffset()
{
var camera = vThirdPersonCamera.instance;
if (hasRagdollCameraTargetOffset && camera != null && characterHips != null &&
(camera.currentTarget == transform || camera.currentTarget == characterHips))
{
camera.SetTargetPositionOffset(this, ragdollCameraTargetOffset);
}
}
protected virtual void ClearRagdollCameraTargetOffset(float smoothTime = 0f)
{
var camera = vThirdPersonCamera.instance;
if (camera != null)
{
camera.ClearTargetPositionOffset(this, smoothTime);
}
hasRagdollCameraTargetOffset = false;
ragdollCameraTargetOffset = Vector3.zero;
}
/// Aborts an active ragdoll immediately, skipping the get-up blend.
public virtual void RestoreRagdoll()
{
if (!isActive)
{
return;
}
ChangeToHipsParent();
ClearRagdollCameraTargetOffset();
iChar.ResetRagdoll();
isActive = false;
state = RagdollState.animated;
inStabilize = false;
setKinematic(true); //disable gravity etc.
setCollider(true);
ResumeAnimationBackend();
iChar?.EndRagdollBlend();
}
#endregion
#region setup
public void LoadBodyPart()
{
bodyParts.Clear();
hipsBodyPart = null;
chestBodyPart = null;
if (!animator)
{
animator = GetComponent();
}
#if INVECTOR_PLAYABLESANIMATOR
if (!playablesAnimator)
{
playablesAnimator = GetComponent();
}
#endif
CacheBones();
RebuildIgnoreTagSet();
if (!characterHips)
{
return;
}
localY = characterHips.InverseTransformDirection(transform.forward);
hipsParent = characterHips.parent;
// Set all RigidBodies to kinematic so that they can be controlled by the animation backend
// and there will be no glitches when transitioning to a ragdoll.
hipsBodyPart = new BodyPart(characterHips);
bodyParts.Add(hipsBodyPart);
var components = characterHips.GetComponentsInChildren();
for (int i = 0; i < components.Length; i++)
{
var t = components[i];
// The hips are added above; re-adding them here would process them twice on every
// kinematic switch and every blend frame.
if (!t || t == transform || t == characterHips || ignoreTagSet.Contains(t.tag))
{
continue;
}
if (t.TryGetComponent(out _))
{
var bodyPart = new BodyPart(t);
bodyParts.Add(bodyPart);
if (t == characterChest)
{
chestBodyPart = bodyPart;
}
}
}
setKinematic(true);
setCollider(true);
}
/// Resolves every bone the runtime needs, so no Animator lookups happen per frame.
protected virtual void CacheBones()
{
characterChest = animator.GetBoneTransform(HumanBodyBones.Chest);
characterHips = animator.GetBoneTransform(HumanBodyBones.Hips);
headBone = animator.GetBoneTransform(HumanBodyBones.Head);
leftFootBone = animator.GetBoneTransform(HumanBodyBones.LeftFoot);
rightFootBone = animator.GetBoneTransform(HumanBodyBones.RightFoot);
leftToesBone = animator.GetBoneTransform(HumanBodyBones.LeftToes);
rightToesBone = animator.GetBoneTransform(HumanBodyBones.RightToes);
}
/// Mirrors the serialized list into a set for per-bone lookups.
protected virtual void RebuildIgnoreTagSet()
{
ignoreTagSet.Clear();
for (int i = 0; i < ignoreTags.Count; i++)
{
if (!string.IsNullOrEmpty(ignoreTags[i]))
{
ignoreTagSet.Add(ignoreTags[i]);
}
}
}
protected virtual void EnsureCollisionSource()
{
if (collisionSource)
{
return;
}
var collisionPrefab = new GameObject("ragdollAudioSource");
collisionPrefab.transform.SetParent(transform);
collisionPrefab.transform.position = transform.position;
collisionSource = collisionPrefab.AddComponent();
}
#endregion
#region activation
public virtual void ActivateRagdollWithDelayToGetUp()
{
ActivateRagdoll(null, debugTimeToStayRagdolled);
}
///
/// Active Ragdoll
///
public virtual void ActivateRagdoll()
{
ActivateRagdoll(null);
}
///
/// Active Ragdoll
///
/// Damage
/// Time to keep ragdolled active
public virtual void ActivateRagdoll(vDamage damage, float timeToStayRagdolled)
{
if (!CanActivateRagdoll(damage))
{
return;
}
ActivateRagdoll(damage);
KeepRagdolled(timeToStayRagdolled);
}
// active ragdoll - call this method to turn the ragdoll on
public virtual void ActivateRagdoll(vDamage damage)
{
if (!CanActivateRagdoll(damage))
{
return;
}
// Only this component's routines are cancelled: StopAllCoroutines would also kill
// routines owned by other behaviours on the same character.
StopRagdollRoutines();
if (damage != null && damage.senselessTime > 0)
{
KeepRagdolled(damage.senselessTime);
}
inApplyCollisionSound = true;
isActive = true;
if (transform.parent != null && !transform.parent.gameObject.isStatic)
{
transform.parent = null;
}
// turn ragdoll on
inStabilize = true;
ragdolled = true;
iChar?.EnableRagdoll();
// start to check if the ragdoll is stable
stabilizerRoutine = StartCoroutine(RagdollStabilizer(StabilizeGraceTime));
characterHips.SetParent(_ragdollContainer.transform);
ScheduleCollisionSoundReset();
}
protected virtual bool CanActivateRagdoll(vDamage damage)
{
if (isActive || state == RagdollState.blendToAnim)
{
return false;
}
return damage == null || damage.activeRagdoll;
}
protected virtual void KeepRagdolled(float time)
{
if (time > 0)
{
keepRagdolled = true;
}
CancelInvoke(nameof(ResetStayRagdolled));
Invoke(nameof(ResetStayRagdolled), time);
}
///
/// Reset keep ragdolled time
///
public virtual void ResetStayRagdolled()
{
keepRagdolled = false;
}
void StopRagdollRoutines()
{
if (stabilizerRoutine != null)
{
StopCoroutine(stabilizerRoutine);
stabilizerRoutine = null;
}
if (resetPlayerRoutine != null)
{
StopCoroutine(resetPlayerRoutine);
resetPlayerRoutine = null;
}
}
void RestartResetPlayerRoutine()
{
if (resetPlayerRoutine != null)
{
StopCoroutine(resetPlayerRoutine);
}
resetPlayerRoutine = StartCoroutine(ResetPlayer(ResetPlayerDelay));
}
#endregion
#region collision sound
// ragdoll collision sound
public virtual void OnRagdollCollisionEnter(vRagdollCollision ragdolCollision)
{
if (inApplyCollisionSound || ragdolCollision.ImpactForce <= MinCollisionImpactForce || !collisionSource)
{
return;
}
collisionSource.clip = collisionClip;
collisionSource.volume = ragdolCollision.ImpactForce * CollisionVolumeScale;
if (collisionSource.isPlaying)
{
return;
}
inApplyCollisionSound = true;
collisionSource.Play();
ScheduleCollisionSoundReset();
}
void ScheduleCollisionSoundReset()
{
CancelInvoke(nameof(ResetCollisionSound));
Invoke(nameof(ResetCollisionSound), CollisionSoundCooldown);
}
///
/// Reset the inApplyDamage variable. Set to false;
///
protected virtual void ResetCollisionSound()
{
inApplyCollisionSound = false;
}
#endregion
#region routines
// ragdoll stabilizer - wait until the ragdoll became stable based on the chest velocity.magnitude
protected virtual IEnumerator RagdollStabilizer(float delay)
{
yield return new WaitForSeconds(delay);
float settleThreshold = iChar != null && iChar.isDead ? DeadStabilizeSpeedThreshold : StabilizeSpeedThreshold;
float rdStabilize = Mathf.Infinity;
while (rdStabilize > settleThreshold)
{
// Once the backend drives the pose again the chest velocity is meaningless, so stop polling.
if (IsAnimationBackendDriving() || chestBodyPart == null || chestBodyPart.rigidbody == null)
{
break;
}
rdStabilize = chestBodyPart.rigidbody.linearVelocity.magnitude;
yield return new WaitForEndOfFrame();
}
if (iChar != null && iChar.isDead)
{
yield return new WaitForEndOfFrame();
DestroyComponents();
}
inStabilize = false;
stabilizerRoutine = null;
}
// reset player - restore control to the character
protected virtual IEnumerator ResetPlayer(float waitTime)
{
yield return new WaitForSeconds(waitTime);
resetPlayerRoutine = null;
iChar?.ResetRagdoll();
}
#endregion
#region ragdoll to animation blend
// ragdoll blend - code based on the script by Perttu Hamalainen with modifications to work with this Controller
protected virtual void RagdollBehaviour()
{
if (state != RagdollState.blendToAnim)
{
return;
}
if (iChar == null || iChar.currentHealth <= 0 || !iChar.ragdolled)
{
return;
}
if (Time.time <= ragdollingEndTime + MecanimToGetUpTransitionTime)
{
//If we are waiting for the backend to start playing the get up animations, update the
//root of the character to the best match with the ragdoll.
AlignRootToRagdolledPose();
}
//compute the ragdoll blend amount in the range 0...1
float ragdollBlendAmount = 1.0f - (Time.time - ragdollingEndTime - MecanimToGetUpTransitionTime) / RagdollToMecanimBlendTime;
ragdollBlendAmount = Mathf.Clamp01(ragdollBlendAmount);
BlendBodyPartsTowardsStoredPose(ragdollBlendAmount);
//if the ragdoll blend amount has decreased to zero, move to animated state
if (ragdollBlendAmount == 0)
{
state = RagdollState.animated;
iChar?.EndRagdollBlend();
}
}
///
/// Moves the character root to the best match with where the ragdoll came to rest: grounded on
/// the highest surface under it, and facing the way the simulated body ended up facing.
///
protected virtual void AlignRootToRagdolledPose()
{
Vector3 animatedToRagdolled = ragdolledHipPosition - characterHips.position;
Vector3 newRootPosition = transform.position + animatedToRagdolled;
//Now cast a ray from the computed position downwards and find the highest hit that does not belong to the character
int hitCount = Physics.RaycastNonAlloc(
new Ray(newRootPosition + Vector3.up, Vector3.down),
GroundProbeHits,
GroundProbeDistance,
groundLayer,
QueryTriggerInteraction.Ignore);
for (int i = 0; i < hitCount; i++)
{
var hit = GroundProbeHits[i];
if (!hit.transform.IsChildOf(transform))
{
newRootPosition.y = Mathf.Max(newRootPosition.y, hit.point.y);
}
}
transform.position = newRootPosition;
// The root now represents the grounded animation pivot again, so the ragdoll-only
// camera correction must not carry into the get-up animation.
ClearRagdollCameraTargetOffset(CameraOffsetReturnSmoothTime);
//Get body orientation in ground plane for both the ragdolled pose and the animated get up pose
Vector3 ragdolledDirection = ragdolledHeadPosition - ragdolledFeetPosition;
ragdolledDirection.y = 0;
Vector3 meanFeetPosition = 0.5f * (leftFootBone.position + rightFootBone.position);
Vector3 animatedDirection = headBone.position - meanFeetPosition;
animatedDirection.y = 0;
//Try to match the rotations. Note that we can only rotate around Y axis, as the animated character must stay upright,
//hence setting the y components of the vectors to zero.
transform.rotation *= Quaternion.FromToRotation(animatedDirection.normalized, ragdolledDirection.normalized);
}
///
/// The backend has already written the animated pose this frame; pull each bone back towards
/// the stored simulated pose by so the get-up starts where the
/// ragdoll ended. Only the hips carry position, every bone carries rotation.
///
protected virtual void BlendBodyPartsTowardsStoredPose(float blendAmount)
{
for (int i = 0; i < bodyParts.Count; i++)
{
var b = bodyParts[i];
//this guard prevents us from modifying the root of the character, only the actual body parts
if (b.transform == transform)
{
continue;
}
//position is only interpolated for the hips
if (b == hipsBodyPart)
{
b.transform.position = Vector3.Lerp(b.transform.position, b.storedPosition, blendAmount);
}
//rotation is interpolated for all body parts
b.transform.rotation = Quaternion.Slerp(b.transform.rotation, b.storedRotation, blendAmount);
}
}
#endregion
#region body part physics
///
/// Plays a get up state on whichever animation backend is driving the character.
///
protected virtual bool PlayGetUpAnimation(string stateName)
{
if (string.IsNullOrWhiteSpace(stateName))
{
return false;
}
#if INVECTOR_PLAYABLESANIMATOR
// During the ragdoll handoff the PDM is intentionally disabled, but it is still the
// animation owner that must receive the forced state. Its queue survives OnDisable and is
// consumed at the first scheduler tick after ResumeAnimationBackend.
if (suspendedPlayablesBackend || UsingPlayablesBackend)
{
return playablesAnimator && playablesAnimator.PlayState(stateName);
}
#endif
if (animator == null || !animator.isActiveAndEnabled)
{
return false;
}
animator.Play(stateName, -1, 0f);
return true;
}
// set all rigidbodies to kinematic
protected virtual void setKinematic(bool newValue)
{
Vector3 inheritedVelocity = Vector3.zero;
if (!newValue && _parentRigb)
{
var parentVelocity = _parentRigb.linearVelocity;
inheritedVelocity = new Vector3(
parentVelocity.x * horizontalMultiplier,
parentVelocity.y * verticalMultiplier,
parentVelocity.z * horizontalMultiplier);
}
for (int i = 0; i < bodyParts.Count; i++)
{
var bp = bodyParts[i];
if (bp.transform == null || bp.rigidbody == null || ignoreTagSet.Contains(bp.transform.tag))
{
continue;
}
if (bp.rigidbody.isKinematic == newValue)
{
continue;
}
bp.rigidbody.isKinematic = newValue;
// Simulated body parts step at the fixed rate while the rest of the frame renders at
// the display rate; without interpolation the limbs strobe.
bp.rigidbody.interpolation = newValue
? RigidbodyInterpolation.None
: RigidbodyInterpolation.Interpolate;
if (!newValue)
{
bp.rigidbody.linearVelocity = inheritedVelocity;
}
}
}
// set all colliders to trigger
protected virtual void setCollider(bool newValue)
{
for (int i = 0; i < bodyParts.Count; i++)
{
var bp = bodyParts[i];
if (bp.transform == null || bp.collider == null ||
bp.transform == transform || ignoreTagSet.Contains(bp.transform.tag))
{
continue;
}
if (disableColliders)
{
bp.collider.enabled = !newValue;
}
else
{
bp.collider.isTrigger = newValue;
}
}
}
// destroy the components if the character is dead
protected virtual void DestroyComponents()
{
if (!removePhysicsAfterDie)
{
return;
}
DestroyChildComponents(respectIgnoreTags: false);
DestroyChildComponents(respectIgnoreTags: true);
DestroyChildComponents(respectIgnoreTags: true);
DestroyChildComponents(respectIgnoreTags: true);
}
void DestroyChildComponents(bool respectIgnoreTags) where T : Component
{
var components = GetComponentsInChildren();
for (int i = 0; i < components.Length; i++)
{
var comp = components[i];
if (!comp || comp.transform == transform)
{
continue;
}
if (respectIgnoreTags && ignoreTagSet.Contains(comp.gameObject.tag))
{
continue;
}
Destroy(comp);
}
}
#endregion
}
}