1029 lines
38 KiB
C#
1029 lines
38 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
using Invector.vCamera;
|
|
using UnityEngine;
|
|
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
using InvectorCore.Playables;
|
|
#endif
|
|
|
|
namespace Invector.vCharacterController
|
|
{
|
|
/// <summary>
|
|
/// 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 <see cref="RagdollBehaviour"/> blends the last
|
|
/// simulated pose into the get-up animation over <see cref="RagdollToMecanimBlendTime"/>.
|
|
///
|
|
/// The exit from <c>ragdolled</c> is gated by <see cref="RagdollStabilizer"/>, which waits for the
|
|
/// chest to settle, plus the <see cref="keepRagdolled"/> flag.
|
|
/// </summary>
|
|
[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<string> ignoreTags = new List<string>() { "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
|
|
|
|
/// <summary>How long the last simulated pose is blended into the get-up animation.</summary>
|
|
protected const float RagdollToMecanimBlendTime = 0.25f;
|
|
/// <summary>Grace period before the blend starts, while the backend picks up the get-up state.</summary>
|
|
protected const float MecanimToGetUpTransitionTime = 0.05f;
|
|
/// <summary>Delay before the character regains control, measured from the end of the ragdoll.</summary>
|
|
protected const float ResetPlayerDelay = 1.1f;
|
|
/// <summary>How long the ragdoll simulates before the settle check starts.</summary>
|
|
protected const float StabilizeGraceTime = 2f;
|
|
/// <summary>Chest speed under which the ragdoll counts as settled.</summary>
|
|
protected const float StabilizeSpeedThreshold = 0.1f;
|
|
/// <summary>Stricter settle threshold used while dead, so corpses do not twitch back to life.</summary>
|
|
protected const float DeadStabilizeSpeedThreshold = 0.0001f;
|
|
/// <summary>Minimum interval between two ragdoll impact sounds.</summary>
|
|
protected const float CollisionSoundCooldown = 0.2f;
|
|
/// <summary>Impact speed under which a collision is too soft to be worth a sound.</summary>
|
|
protected const float MinCollisionImpactForce = 1f;
|
|
/// <summary>Maps impact speed to audio volume.</summary>
|
|
protected const float CollisionVolumeScale = 0.05f;
|
|
/// <summary>Vertical span of the ground probe cast under the blended root position.</summary>
|
|
protected const float GroundProbeDistance = 1f;
|
|
/// <summary>Deferral so the hips are reparented after the disable frame has settled.</summary>
|
|
protected const float ReparentDelay = 0.01f;
|
|
/// <summary>Deferral used by <see cref="startRagdolled"/> so the character initializes first.</summary>
|
|
protected const float StartRagdolledDelay = 0.1f;
|
|
/// <summary>How long the camera eases from the ragdoll pivot back to the normal player pivot.</summary>
|
|
protected const float CameraOffsetReturnSmoothTime = 0.2f;
|
|
|
|
/// <summary>Reused by the ground probe so the per-frame blend does not allocate.</summary>
|
|
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;
|
|
/// <summary>True when PlayablesAnimator, not the Animator Controller, is driving the character.</summary>
|
|
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; }
|
|
|
|
/// <summary>True while physics, and not the animation backend, owns the pose.</summary>
|
|
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<BodyPart> bodyParts = new List<BodyPart>();
|
|
// Tag lookup used inside the per-bone loops; the serialized list stays the authoring surface.
|
|
readonly HashSet<string> ignoreTagSet = new HashSet<string>();
|
|
// 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<Rigidbody>();
|
|
collider = t.GetComponent<Collider>();
|
|
}
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region unity lifecycle
|
|
|
|
protected virtual void Start()
|
|
{
|
|
_parentRigb = GetComponent<Rigidbody>();
|
|
iChar = GetComponent<vICharacter>();
|
|
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
|
|
|
|
/// <summary>Hands the pose to physics and suspends whichever animation backend is driving.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Returns the pose to the animation backend. From <see cref="RagdollState.ragdolled"/> this
|
|
/// snapshots the simulated pose and starts the get-up blend; from any other state it only
|
|
/// restores the physics and animation flags.
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>Snapshots the pose the ragdoll settled in, so the blend has a source to leave from.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>True when the ragdoll has settled and nothing is holding the character down.</summary>
|
|
protected virtual bool CanLeaveRagdoll()
|
|
{
|
|
return ragdolled && !inStabilize && !keepRagdolled && !(iChar != null && iChar.isDead);
|
|
}
|
|
|
|
/// <summary>True while the Animator is enabled and able to author the pose.</summary>
|
|
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);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
protected virtual void CacheRagdollCameraTargetOffset()
|
|
{
|
|
hasRagdollCameraTargetOffset = characterHips != null;
|
|
if (hasRagdollCameraTargetOffset)
|
|
{
|
|
ragdollCameraTargetOffset = transform.position - characterHips.position;
|
|
}
|
|
}
|
|
|
|
/// <summary>Keeps the camera pivot at its pre-ragdoll height while the root follows the hips.</summary>
|
|
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;
|
|
}
|
|
|
|
/// <summary>Aborts an active ragdoll immediately, skipping the get-up blend.</summary>
|
|
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<Animator>();
|
|
}
|
|
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
if (!playablesAnimator)
|
|
{
|
|
playablesAnimator = GetComponent<PlayablesAnimator>();
|
|
}
|
|
#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<Transform>();
|
|
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<Rigidbody>(out _))
|
|
{
|
|
var bodyPart = new BodyPart(t);
|
|
bodyParts.Add(bodyPart);
|
|
if (t == characterChest)
|
|
{
|
|
chestBodyPart = bodyPart;
|
|
}
|
|
}
|
|
}
|
|
|
|
setKinematic(true);
|
|
setCollider(true);
|
|
}
|
|
|
|
/// <summary>Resolves every bone the runtime needs, so no Animator lookups happen per frame.</summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>Mirrors the serialized <see cref="ignoreTags"/> list into a set for per-bone lookups.</summary>
|
|
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<AudioSource>();
|
|
}
|
|
|
|
#endregion
|
|
|
|
#region activation
|
|
|
|
public virtual void ActivateRagdollWithDelayToGetUp()
|
|
{
|
|
ActivateRagdoll(null, debugTimeToStayRagdolled);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Active Ragdoll
|
|
/// </summary>
|
|
public virtual void ActivateRagdoll()
|
|
{
|
|
ActivateRagdoll(null);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Active Ragdoll
|
|
/// </summary>
|
|
/// <param name="damage">Damage</param>
|
|
/// <param name="timeToStayRagdolled">Time to keep ragdolled active</param>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reset keep ragdolled time
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Reset the inApplyDamage variable. Set to false;
|
|
/// </summary>
|
|
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();
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// 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.
|
|
/// </summary>
|
|
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);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The backend has already written the animated pose this frame; pull each bone back towards
|
|
/// the stored simulated pose by <paramref name="blendAmount"/> so the get-up starts where the
|
|
/// ragdoll ended. Only the hips carry position, every bone carries rotation.
|
|
/// </summary>
|
|
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
|
|
|
|
/// <summary>
|
|
/// Plays a get up state on whichever animation backend is driving the character.
|
|
/// </summary>
|
|
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<MonoBehaviour>(respectIgnoreTags: false);
|
|
DestroyChildComponents<CharacterJoint>(respectIgnoreTags: true);
|
|
DestroyChildComponents<Rigidbody>(respectIgnoreTags: true);
|
|
DestroyChildComponents<Collider>(respectIgnoreTags: true);
|
|
}
|
|
|
|
void DestroyChildComponents<T>(bool respectIgnoreTags) where T : Component
|
|
{
|
|
var components = GetComponentsInChildren<T>();
|
|
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
|
|
}
|
|
}
|