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 } }