Files
beyond/Assets/ThirdParty/Invector-3rdPersonController/Basic Locomotion/Scripts/Ragdoll/vRagdoll.cs
2024-12-18 15:25:43 +01:00

639 lines
23 KiB
C#

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
namespace Invector.vCharacterController
{
[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 childrens. MELEE: Keep true to only hit the main Capsule Collider.")]
public bool disableColliders = false;
public AudioSource collisionSource;
public AudioClip collisionClip;
[Header("Add Tags for Weapons or Itens here:")]
public List<string> ignoreTags = new List<string>() { "Weapon", "Ignore Ragdoll" };
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 private variables
internal vICharacter iChar;
Animator animator;
Rigidbody _parentRigb;
internal Transform characterChest, characterHips;
[System.NonSerialized]
public bool isActive;
bool inStabilize, updateBehaviour;
public bool ignoreGetUpAnimation { get => _ignoreGetUpAnimation; set => _ignoreGetUpAnimation = value; }
Vector3 localY;
bool ragdolled
{
get
{
return state != RagdollState.animated;
}
set
{
if (value == true)
{
if (state == RagdollState.animated)
{
//Transition from animated to ragdolled
setKinematic(false); //allow the ragdoll RigidBodies to react to the environment
setCollider(false);
animator.enabled = false; //disable animation
state = RagdollState.ragdolled;
}
}
else
{
characterHips.parent = hipsParent;
isActive = false;
if (state == RagdollState.ragdolled)
{
setKinematic(true); //disable gravity etc.
setCollider(true);
ragdollingEndTime = Time.time; //store the state change time
animator.enabled = true; //enable animation
state = RagdollState.blendToAnim;
//Store the ragdolled position for blending
foreach (BodyPart b in bodyParts)
{
b.storedRotation = b.transform.rotation;
b.storedPosition = b.transform.position;
}
//Remember some key positions
ragdolledFeetPosition = 0.5f * (animator.GetBoneTransform(HumanBodyBones.LeftToes).position + animator.GetBoneTransform(HumanBodyBones.RightToes).position);
ragdolledHeadPosition = animator.GetBoneTransform(HumanBodyBones.Head).position;
ragdolledHipPosition = animator.GetBoneTransform(HumanBodyBones.Hips).position;
//Initiate the get up animation
//hip hips forward vector pointing upwards, initiate the get up from back animation
if (!ignoreGetUpAnimation)
{
if (characterHips.TransformDirection(localY).y > 0)
{
animator.Play("StandUp@FromBack");
}
else
{
animator.Play("StandUp@FromBelly");
}
}
}
}
}
}
//Possible states of the ragdoll
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
}
//The current state
RagdollState state = RagdollState.animated;
//How long do we blend when transitioning from ragdolled to animated
readonly float ragdollToMecanimBlendTime = 0.5f;
readonly float mecanimToGetUpTransitionTime = 0.05f;
//A helper variable to store the time when we transitioned from ragdolled to blendToAnim state
float ragdollingEndTime = -100;
//Additional vectores for storing the pose the ragdoll ended up in.
Vector3 ragdolledHipPosition, ragdolledHeadPosition, ragdolledFeetPosition;
//Declare a list of body parts, initialized in Start()
readonly List<BodyPart> bodyParts = new List<BodyPart>();
// used to reset parent of hips
Transform hipsParent;
//used to controll damage frequency
bool inApplyCollisionSound;
private GameObject _ragdollContainer;
class BodyPart
{
public Transform transform;
public Rigidbody rigidbody;
public Collider collider;
public Vector3 storedPosition;
public Quaternion storedRotation;
public BodyPart(Transform t)
{
this.transform = t;
this.rigidbody = t.GetComponent<Rigidbody>();
this.collider = t.GetComponent<Collider>();
}
}
#endregion
void Start()
{
// store the Animator component
_parentRigb = GetComponent<Rigidbody>();
iChar = GetComponent<vICharacter>();
if (iChar != null)
{
iChar.onActiveRagdoll.AddListener(ActivateRagdoll);
}
if (!collisionSource)
{
var _collisionPrefab = new GameObject("ragdollAudioSource");
_collisionPrefab.transform.SetParent(gameObject.transform);
_collisionPrefab.transform.position = transform.position;
collisionSource = _collisionPrefab.AddComponent<AudioSource>();
}
LoadBodyPart();
CreateRagdollContainer();
if (startRagdolled)
{
Invoke("ActivateRagdoll", 0.1f);
}
}
public void LoadBodyPart()
{
bodyParts.Clear();
// find character chest and hips
if (!animator)
{
animator = GetComponent<Animator>();
}
characterChest = animator.GetBoneTransform(HumanBodyBones.Chest);
characterHips = animator.GetBoneTransform(HumanBodyBones.Hips);
localY = characterHips.InverseTransformDirection(Vector3.up);
hipsParent = characterHips.parent;
// set all RigidBodies to kinematic so that they can be controlled with Mecanim
// and there will be no glitches when transitioning to a ragdoll
// find all the transforms in the character, assuming that this script is attached to the root
if (characterHips)
{
Component[] components = characterHips.GetComponentsInChildren(typeof(Transform));
bodyParts.Add(new BodyPart(characterHips));
// for each of the transforms, create a BodyPart instance and store the transform
foreach (Component c in components)
{
if (!ignoreTags.Contains(c.tag) && c)
{
var t = c as Transform;
if (t != transform && t.GetComponent<Rigidbody>())
{
BodyPart bodyPart = new BodyPart(t);
if (bodyPart.rigidbody != null)
{
bodyPart.rigidbody.isKinematic = true;
c.tag = gameObject.tag;
}
bodyParts.Add(bodyPart);
}
}
}
setKinematic(true);
setCollider(true);
}
}
void CreateRagdollContainer()
{
if (!_ragdollContainer)
{
_ragdollContainer = new GameObject("RagdollContainer " + gameObject.name);
}
//_ragdollContainer.hideFlags = HideFlags.HideInHierarchy;
}
void LateUpdate()
{
if (animator == null)
{
return;
}
if (!updateBehaviour && animator.updateMode == AnimatorUpdateMode.Fixed)
{
return;
}
updateBehaviour = false;
RagdollBehaviour();
}
void FixedUpdate()
{
updateBehaviour = true;
if (!isActive)
{
return;
}
if (iChar.currentHealth > 0)
{
if (!_ragdollContainer)
{
CreateRagdollContainer();
}
if (characterHips.parent != _ragdollContainer.transform)
{
characterHips.SetParent(_ragdollContainer.transform);
}
if (ragdolled && !inStabilize && !keepRagdolled)
{
ragdolled = false;
StartCoroutine(ResetPlayer(1.1f));
}
else if (animator != null && !animator.isActiveAndEnabled && ragdolled || (animator == null && ragdolled))
{
transform.position = characterHips.position;
}
}
}
void OnDestroy()
{
try
{
if (_ragdollContainer && characterHips && characterHips.parent == _ragdollContainer.transform)
{
characterHips.SetParent(hipsParent);
Destroy(_ragdollContainer.gameObject);
}
}
catch (UnityException e)
{
Debug.LogWarning(e.Message, gameObject);
}
}
/// <summary>
/// Reset the inApplyDamage variable. Set to false;
/// </summary>
void ResetCollisionSound()
{
inApplyCollisionSound = false;
}
public void ActivateRagdollWithDelayToGetUp()
{
ActivateRagdoll(null, debugTimeToStayRagdolled);
}
void KeepRagdolled(float time)
{
if (time > 0)
{
keepRagdolled = true;
}
CancelInvoke("ResetStayRagdolled");
Invoke("ResetStayRagdolled", time);
}
/// <summary>
/// Reset keep ragdolled time
/// </summary>
public void ResetStayRagdolled()
{
keepRagdolled = false;
}
/// <summary>
/// Active Ragdoll
/// </summary>
public void ActivateRagdoll()
{
ActivateRagdoll(null);
}
/// <summary>
/// Active Ragdoll
/// </summary>
/// <param name="damage">Damage</param>
/// <param name="timeToStayRagdolled">Time to keep ragdolled active</param>
public void ActivateRagdoll(vDamage damage, float timeToStayRagdolled)
{
if (isActive || (damage != null && !damage.activeRagdoll))
{
return;
}
ActivateRagdoll(damage);
KeepRagdolled(timeToStayRagdolled);
}
// active ragdoll - call this method to turn the ragdoll on
public void ActivateRagdoll(vDamage damage)
{
if (isActive || (damage != null && !damage.activeRagdoll))
{
return;
}
if (!_ragdollContainer)
{
CreateRagdollContainer();
}
if (damage != null && damage.senselessTime > 0)
{
KeepRagdolled(damage.senselessTime);
}
inApplyCollisionSound = true;
isActive = true;
if (transform.parent != null && !transform.parent.gameObject.isStatic)
{
transform.parent = null;
}
var isDead = true;
// turn ragdoll on
inStabilize = true;
ragdolled = true;
if (iChar != null)
{
iChar.EnableRagdoll();
isDead = !(iChar.currentHealth > 0);
}
// start to check if the ragdoll is stable
StartCoroutine(RagdollStabilizer(2f));
if (!isDead)
{
characterHips.SetParent(_ragdollContainer.transform);// = null;
}
Invoke("ResetCollisionSound", 0.2f);
}
// ragdoll collision sound
public void OnRagdollCollisionEnter(vRagdollCollision ragdolCollision)
{
if (!inApplyCollisionSound && ragdolCollision.ImpactForce > 1)
{
if (collisionSource)
{
collisionSource.clip = collisionClip;
collisionSource.volume = ragdolCollision.ImpactForce * 0.05f;
if (!collisionSource.isPlaying)
{
inApplyCollisionSound = true;
collisionSource.Play();
Invoke("ResetCollisionSound", 0.2f);
}
}
}
}
// ragdoll stabilizer - wait until the ragdoll became stable based on the chest velocity.magnitude
IEnumerator RagdollStabilizer(float delay)
{
float rdStabilize = Mathf.Infinity;
yield return new WaitForSeconds(delay);
while (rdStabilize > (iChar != null && iChar.isDead ? 0.0001f : 0.1f))
{
if (animator != null && !animator.isActiveAndEnabled)
{
rdStabilize = characterChest.GetComponent<Rigidbody>().linearVelocity.magnitude;
}
else
{
break;
}
yield return new WaitForEndOfFrame();
}
if (iChar != null && iChar.isDead)
{
//Destroy(iChar as Component);
yield return new WaitForEndOfFrame();
DestroyComponents();
}
inStabilize = false;
}
// reset player - restore control to the character
IEnumerator ResetPlayer(float waitTime)
{
yield return new WaitForSeconds(waitTime);
//Debug.Log("Ragdoll OFF");
if (iChar != null)
{
iChar.ResetRagdoll();
}
}
// ragdoll blend - code based on the script by Perttu Hämäläinen with modifications to work with this Controller
void RagdollBehaviour()
{
var isDead = !(iChar != null && iChar.currentHealth > 0);
if (isDead)
{
return;
}
if (iChar == null || !iChar.ragdolled)
{
return;
}
//Blending from ragdoll back to animated
if (state == RagdollState.blendToAnim)
{
if (Time.time <= ragdollingEndTime + mecanimToGetUpTransitionTime)
{
//If we are waiting for Mecanim to start playing the get up animations, update the root of the mecanim
//character to the best match with the ragdoll
Vector3 animatedToRagdolled = ragdolledHipPosition - animator.GetBoneTransform(HumanBodyBones.Hips).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
RaycastHit[] hits = Physics.RaycastAll(new Ray(newRootPosition + Vector3.up, Vector3.down), 1, groundLayer, QueryTriggerInteraction.Ignore);
foreach (RaycastHit hit in hits)
{
if (!hit.transform.IsChildOf(transform))
{
newRootPosition.y = Mathf.Max(newRootPosition.y, hit.point.y);
}
}
transform.position = newRootPosition;
//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 * (animator.GetBoneTransform(HumanBodyBones.LeftFoot).position + animator.GetBoneTransform(HumanBodyBones.RightFoot).position);
Vector3 animatedDirection = animator.GetBoneTransform(HumanBodyBones.Head).position - meanFeetPosition;
animatedDirection.y = 0;
//Try to match the rotations. Note that we can only rotate around Y axis, as the animated characted must stay upright,
//hence setting the y components of the vectors to zero.
transform.rotation *= Quaternion.FromToRotation(animatedDirection.normalized, ragdolledDirection.normalized);
}
//compute the ragdoll blend amount in the range 0...1
float ragdollBlendAmount = 1.0f - (Time.time - ragdollingEndTime - mecanimToGetUpTransitionTime) / ragdollToMecanimBlendTime;
ragdollBlendAmount = Mathf.Clamp01(ragdollBlendAmount);
//In LateUpdate(), Mecanim has already updated the body pose according to the animations.
//To enable smooth transitioning from a ragdoll to animation, we lerp the position of the hips
//and slerp all the rotations towards the ones stored when ending the ragdolling
foreach (BodyPart b in bodyParts)
{
if (b.transform != transform)
{ //this if is to prevent us from modifying the root of the character, only the actual body parts
//position is only interpolated for the hips
if (b.transform == animator.GetBoneTransform(HumanBodyBones.Hips))
{
b.transform.position = Vector3.Lerp(b.transform.position, b.storedPosition, ragdollBlendAmount);
}
//rotation is interpolated for all body parts
b.transform.rotation = Quaternion.Slerp(b.transform.rotation, b.storedRotation, ragdollBlendAmount);
}
}
//if the ragdoll blend amount has decreased to zero, move to animated state
if (ragdollBlendAmount == 0)
{
state = RagdollState.animated;
return;
}
}
}
// set all rigidbodies to kinematic
void setKinematic(bool newValue)
{
foreach (var bp in bodyParts)
{
if (!ignoreTags.Contains(bp.transform.tag) && bp.rigidbody)
{
if (bp.rigidbody.isKinematic != newValue)
{
bp.rigidbody.isKinematic = newValue;
if (newValue == false)
{
var v = new Vector3(_parentRigb.linearVelocity.x * horizontalMultiplier, _parentRigb.linearVelocity.y * verticalMultiplier, _parentRigb.linearVelocity.z * horizontalMultiplier);
bp.rigidbody.linearVelocity = v;
}
}
}
}
}
// set all colliders to trigger
void setCollider(bool newValue)
{
//if (!disableColliders) return;
foreach (var bp in bodyParts)
{
if (!ignoreTags.Contains(bp.transform.tag))
{
if (!bp.transform.Equals(transform) && bp.collider)
{
if (disableColliders)
{
bp.collider.enabled = !newValue;
}
else
{
bp.collider.isTrigger = newValue;
}
}
}
}
}
// destroy the components if the character is dead
void DestroyComponents()
{
if (removePhysicsAfterDie)
{
var comps = GetComponentsInChildren<MonoBehaviour>();
for (int i = 0; i < comps.Length; i++)
{
if (comps[i].transform != transform)
{
Destroy(comps[i]);
}
}
var joints = GetComponentsInChildren<CharacterJoint>();
if (joints != null)
{
foreach (CharacterJoint comp in joints)
{
if (!ignoreTags.Contains(comp.gameObject.tag) && comp.transform != transform)
{
Destroy(comp);
}
}
}
var rigidbodies = GetComponentsInChildren<Rigidbody>();
if (rigidbodies != null)
{
foreach (Rigidbody comp in rigidbodies)
{
if (!ignoreTags.Contains(comp.gameObject.tag) && comp.transform != transform)
{
Destroy(comp);
}
}
}
var colliders = GetComponentsInChildren<Collider>();
if (colliders != null)
{
foreach (Collider comp in colliders)
{
if (!ignoreTags.Contains(comp.gameObject.tag) && comp.transform != transform)
{
Destroy(comp);
}
}
}
}
}
}
}