using System.Collections; using UnityEngine; #if INVECTOR_PLAYABLESANIMATOR using InvectorCore.Playables; #endif namespace Invector.vCharacterController.vActions { using System.Collections.Generic; using vCharacterController; [vClassHeader("GENERIC ACTION", "Use the vTriggerGenericAction to trigger a simple animation.\nYou can use vGenericActionReceiver component to filter events by action name", iconName = "triggerIcon")] public class vGenericAction : vActionListener, vIAnimatorMoveReceiver { #region Variables [vEditorToolbar("Settings")] [Tooltip("Tag of the object you want to access")] public string actionTag = "Action"; [vEditorToolbar("Debug")] [Header("--- Debug Only ---")] [Tooltip("Check this to enter the debug mode")] public bool debugMode; [vReadOnly] protected vTriggerGenericAction _triggerAction; public virtual vTriggerGenericAction triggerAction { get => _triggerAction; set => _triggerAction = value; } #if INVECTOR_PLAYABLESANIMATOR protected PlayablesAnimator playablesAnimator; #endif [vReadOnly, SerializeField] protected bool _playingAnimation; [vReadOnly, SerializeField] protected bool actionStarted; [vReadOnly] public bool isLockTriggerEvents; [vReadOnly, SerializeField] protected List colliders = new List(); [vEditorToolbar("Events")] public vOnActionHandle OnDoAction = new vOnActionHandle(); public vOnActionHandle OnEnterTriggerAction; public vOnActionHandle OnExitTriggerAction; public vOnActionHandle OnStartAction; public vOnActionHandle OnCancelAction; public vOnActionHandle OnEndAction; public bool doingAction { get; set; } public virtual Camera mainCamera { get; set; } public virtual vThirdPersonInput tpInput { get; set; } protected virtual float _currentInputDelay { get; set; } protected virtual Vector3 _screenCenter { get; set; } protected virtual float timeInTrigger { get; set; } protected virtual float animationBehaviourDelay { get; set; } /// True after the requested animation starts. protected bool actionAnimationDetected; protected bool finishRotationMatch; protected bool finishPositionXZMatch; protected bool finishPositionYMatch; /// /// Position the match settled on when its curve reached 1, held for the rest of the action so /// root motion cannot walk the character off it during the tail. See EvaluateToTargetPosition. /// protected Vector2 latchedXZMatch; protected bool hasLatchedXZMatch; /// /// XZ the root had when the action's match began, held while the XZ curve is still ramping. /// See for why. /// protected Vector2 matchOriginXZ; protected bool hasMatchOriginXZ; protected int animationRequestFrame; protected bool playerStatePrepared; protected bool previousLockMovement; protected bool previousLockMoveInput; protected bool actionSuspendedInterpolation; protected bool actionDisabledGravity; protected bool actionDisabledCollision; protected bool previousUseGravity; protected bool previousIsKinematic; protected bool previousColliderIsTrigger; /// Reusable buffer for nearby trigger selection. private readonly List nearActionScratch = new List(); protected virtual Vector3 screenCenter { get { var center = _screenCenter; center.x = Screen.width * 0.5f; center.y = Screen.height * 0.5f; center.z = 0; return _screenCenter = center; } } internal Dictionary actions; #endregion internal class ActionStorage { internal vTriggerGenericAction action; internal bool isValid; internal ActionStorage() { } internal ActionStorage(vTriggerGenericAction action) { this.action = action; } public static implicit operator vTriggerGenericAction(ActionStorage storage) { return storage.action; } public static implicit operator ActionStorage(vTriggerGenericAction action) { return new ActionStorage(action); } } #region Setup protected override void SetUpListener() { actionEnter = true; actionStay = true; actionExit = true; actions = new Dictionary(); } protected override void Start() { base.Start(); tpInput = GetComponent(); #if INVECTOR_PLAYABLESANIMATOR playablesAnimator = GetComponent(); #endif var actionsReceivers = GetComponentsInChildren(); for (int i = 0; i < actionsReceivers.Length; i++) { OnDoAction.AddListener(actionsReceivers[i].OnReceiveAction); } SubscribeToInput(); if (!mainCamera) { mainCamera = Camera.main; } } protected virtual void SubscribeToInput() { if (tpInput == null) { return; } tpInput.onUpdate -= CheckForTriggerAction; tpInput.onUpdate += CheckForTriggerAction; } protected virtual void UnsubscribeFromInput() { if (tpInput == null) { return; } tpInput.onUpdate -= CheckForTriggerAction; } /// Updates the action from the Animator root-motion callback. public virtual void OnAnimatorMoveEvent() { #if INVECTOR_PLAYABLESANIMATOR if (UsingPlayables) { return; } #endif UpdateGenericAction(); } #if INVECTOR_PLAYABLESANIMATOR /// Updates the action from the Playables root-motion callback. protected virtual void OnPlayablesMove() { UpdateGenericAction(); } #endif protected virtual void OnDestroy() { RestorePlayerAfterAction(); UnsubscribeFromInput(); } #endregion #region Per-frame tick protected virtual void UpdateGenericAction() { if (!mainCamera) { mainCamera = Camera.main; } AnimationBehaviour(); HandleColliders(); } protected virtual void HandleColliders() { colliders.Clear(); foreach (var key in actions.Keys) { colliders.Add(key); } if (!doingAction && triggerAction && !isLockTriggerEvents) { if (timeInTrigger <= 0) { actions.Clear(); triggerAction = null; } else { timeInTrigger -= Time.deltaTime; } } } #endregion #region Animation backend abstraction protected virtual bool IsPlayingActionAnimation(string stateName, int layer) { #if INVECTOR_PLAYABLESANIMATOR if (UsingPlayables) { return playablesAnimator.IsPlayingState(stateName, out _, layer) || playablesAnimator.IsPlayingState(stateName, out _, layer, includeTransitionGate: true); } #endif var animator = tpInput.cc.animator; var stateHash = Animator.StringToHash(stateName); if (MatchesAnimatorState(animator.GetCurrentAnimatorStateInfo(layer), stateHash)) { return true; } return animator.IsInTransition(layer) && MatchesAnimatorState(animator.GetNextAnimatorStateInfo(layer), stateHash); } protected virtual float GetActionNormalizedTime(int layer) { #if INVECTOR_PLAYABLESANIMATOR if (UsingPlayables) { return playablesAnimator.GetNormalizedTime(layer); } #endif var animator = tpInput.cc.animator; if (triggerAction != null && animator.IsInTransition(layer)) { var stateHash = Animator.StringToHash(triggerAction.playAnimation); var nextState = animator.GetNextAnimatorStateInfo(layer); if (MatchesAnimatorState(nextState, stateHash)) { return nextState.normalizedTime; } } return animator.GetCurrentAnimatorStateInfo(layer).normalizedTime; } protected static bool MatchesAnimatorState(AnimatorStateInfo stateInfo, int stateHash) { return stateInfo.shortNameHash == stateHash || stateInfo.fullPathHash == stateHash; } protected virtual Vector3 GetActionRootPosition() { #if INVECTOR_PLAYABLESANIMATOR if (UsingPlayables) { return playablesAnimator.rootPosition; } #endif return tpInput.cc.animator.rootPosition; } protected virtual Quaternion GetActionRootRotation() { #if INVECTOR_PLAYABLESANIMATOR if (UsingPlayables) { return playablesAnimator.rootRotation; } #endif return tpInput.cc.animator.rootRotation; } protected virtual void PlayActionAnimation(string stateName, float crossFadeTransition, int layer) { #if INVECTOR_PLAYABLESANIMATOR if (UsingPlayables) { playablesAnimator.PlayState(stateName, layer); return; } #endif tpInput.cc.animator.CrossFadeInFixedTime(stateName, Mathf.Max(0f, crossFadeTransition), layer, 0f); } #if INVECTOR_PLAYABLESANIMATOR /// True while Playables drives the character. protected virtual bool UsingPlayables => playablesAnimator && playablesAnimator.isActiveAndEnabled; #endif protected virtual bool inActionAnimation { get { return triggerAction != null && !string.IsNullOrEmpty(triggerAction.playAnimation) && IsPlayingActionAnimation(triggerAction.playAnimation, triggerAction.animatorLayer); } } #endregion #region Trigger detection protected virtual void CheckForTriggerAction() { if ((actions.Count == 0 && !triggerAction) || isLockTriggerEvents) { return; } vTriggerGenericAction _triggerAction = GetNearAction(); if (!doingAction && triggerAction != _triggerAction) { triggerAction = _triggerAction; if (triggerAction) { if (triggerAction._collider && actions.TryGetValue(triggerAction._collider, out var storage)) { storage.isValid = true; } triggerAction.OnValidate.Invoke(gameObject); OnEnterTriggerAction.Invoke(triggerAction); } } TriggerActionInput(); } /// Selects the valid trigger nearest to the screen center. protected virtual vTriggerGenericAction GetNearAction() { if (isLockTriggerEvents || doingAction || actionStarted || _playingAnimation) { return null; } nearActionScratch.Clear(); foreach (var key in actions.Keys) { nearActionScratch.Add(key); } var center = screenCenter; float closestSqrDistance = Mathf.Infinity; vTriggerGenericAction targetAction = null; for (int i = 0; i < nearActionScratch.Count; i++) { var key = nearActionScratch[i]; if (!key) { actions.Remove(key); return null; } if (!actions.TryGetValue(key, out var storage) || !storage.action) { continue; } var action = storage.action; if (!mainCamera) { if (!targetAction) { targetAction = action; } else { InvalidateAction(action); } continue; } var screenPoint = mainCamera.WorldToScreenPoint(key.transform.position); float sqrDistance = (screenPoint - center).sqrMagnitude; if (IsCandidateValid(action, sqrDistance, closestSqrDistance)) { closestSqrDistance = sqrDistance; if (targetAction && targetAction != action) { InvalidateAction(targetAction); } targetAction = action; } else { InvalidateAction(action); } } return targetAction; } /// Checks whether a trigger can replace the current candidate. protected virtual bool IsCandidateValid(vTriggerGenericAction action, float sqrDistance, float closestSqrDistance) { if (!action.enabled || !action.gameObject.activeInHierarchy || sqrDistance >= closestSqrDistance) { return false; } return !action.activeFromForward || IsInForward(action.transform, action.forwardAngle); } /// Invalidates a trigger that is no longer selectable. protected virtual void InvalidateAction(vTriggerGenericAction action) { if (action && action._collider && actions.TryGetValue(action._collider, out var storage) && storage.isValid) { storage.isValid = false; action.OnInvalidate.Invoke(gameObject); OnExitTriggerAction.Invoke(action); } } protected virtual bool IsInForward(Transform target, float angleToCompare) { var angle = Vector3.Angle(transform.forward, target.forward); return angle <= angleToCompare; } #endregion #region Action lifecycle protected virtual void AnimationBehaviour() { if (!doingAction || !actionStarted) { return; } if (!triggerAction) { ResetTriggerSettings(); return; } var isPlaying = playingAnimation; if (!actionAnimationDetected) { animationBehaviourDelay -= Time.deltaTime; if (animationBehaviourDelay <= 0f) { Debug.LogWarning($"GenericAction: Animation '{triggerAction.playAnimation}' did not start.", gameObject); CancelAction(); } return; } if (isPlaying) { if (triggerAction.matchTarget != null) { EvaluateToTargetPosition(); if (triggerAction.useTriggerRotation) { EvaluateToTargetRotation(); } } if (ReachedAnimationExitTime()) { EndAction(); } } else if (ShouldForceEndAction()) { EndAction(); } } /// Checks whether an automatic action reached its exit time. protected virtual bool ReachedAnimationExitTime() { if (!actionStarted || triggerAction.endActionManualy) { return false; } if (IsHoldToPlayAction(triggerAction)) { return false; } return GetActionNormalizedTime(triggerAction.animatorLayer) >= triggerAction.endExitTimeAnimation; } /// Checks whether a stopped animation must end the action. protected virtual bool ShouldForceEndAction() { if (!doingAction || !actionStarted) { return false; } if (triggerAction == null) { return true; } if (!actionAnimationDetected) { return false; } return !triggerAction.endActionManualy && !IsHoldToPlayAction(triggerAction); } /// Checks whether the animation follows held input. protected static bool IsHoldToPlayAction(vTriggerGenericAction action) { return action.inputType == vTriggerGenericAction.InputType.GetButtonTimer && action.playAnimationWhileHoldingButton; } protected virtual void EvaluateToTargetPosition() { var matchTargetPosition = GetMatchTargetPosition(); AnimationCurve XZ = triggerAction.matchPositionXZCurve; AnimationCurve Y = triggerAction.matchPositionYCurve; float normalizedTime = Mathf.Clamp(GetActionNormalizedTime(triggerAction.animatorLayer), 0, 1); Vector3 rootPosition = GetActionRootPosition(); float evaluatedXZ = XZ.Evaluate(normalizedTime); var matchTargetSpace = triggerAction.matchTarget; // Hold the XZ the root started the match at, instead of following live root motion, until // the match curve has fully taken over. // // The action is cross-faded in, so for the first frames the state being left is still // blended in and still contributing root motion - a full-speed run, typically. The XZ lerp // below starts from rootPosition, and an authored curve is near 0 there, so that residual // locomotion is what almost entirely decides the position: the body keeps travelling // forward and visibly enters the geometry the action is meant to climb (measured at up to // 0.31m into the step face, worst around normalized time 0.12-0.14, before this). // // Held per axis, in the match target's local space, and only on the axes the trigger // matches: an unchecked useLocalX/useLocalZ means that axis belongs to the animation, so // freezing it removes the root motion the action is made of. Slide Under matches the // lateral axis only and travels ~5.5m along the free one; freezing both stopped it 1.3m // in, under the obstacle, where the restored capsule pushed it through the floor. if (!hasMatchOriginXZ) { matchOriginXZ = new Vector2(rootPosition.x, rootPosition.z); hasMatchOriginXZ = true; } if (evaluatedXZ < 1f) { var frozen = matchTargetSpace.InverseTransformPoint( new Vector3(matchOriginXZ.x, rootPosition.y, matchOriginXZ.y)); var live = matchTargetSpace.InverseTransformPoint(rootPosition); var held = new Vector3( triggerAction.useLocalX ? frozen.x : live.x, live.y, triggerAction.useLocalZ ? frozen.z : live.z); var heldWorld = matchTargetSpace.TransformPoint(held); rootPosition.x = heldWorld.x; rootPosition.z = heldWorld.z; } float evaluatedY = Y.Evaluate(normalizedTime); var matchTarget = triggerAction.matchTarget; var localRelativeToTarget = matchTarget.InverseTransformPoint(matchTargetPosition); if (!triggerAction.useLocalX || !triggerAction.useLocalZ) { var localRoot = matchTarget.InverseTransformPoint(rootPosition); if (!triggerAction.useLocalX) { localRelativeToTarget.x = localRoot.x; } if (!triggerAction.useLocalZ) { localRelativeToTarget.z = localRoot.z; } } matchTargetPosition = matchTarget.TransformPoint(localRelativeToTarget); // Once the XZ curve saturates, hold the matched point instead of releasing the axis back to // root motion. Held in the target's local space and resolved through TransformPoint each // frame, on only the axes the trigger controls (useLocalX/useLocalZ), so a rotated target // and an unmatched axis both keep working. if (evaluatedXZ < 1f) { rootPosition.x = Mathf.Lerp(rootPosition.x, matchTargetPosition.x, evaluatedXZ); rootPosition.z = Mathf.Lerp(rootPosition.z, matchTargetPosition.z, evaluatedXZ); finishPositionXZMatch = true; } else { if (finishPositionXZMatch) { finishPositionXZMatch = false; latchedXZMatch = new Vector2(localRelativeToTarget.x, localRelativeToTarget.z); hasLatchedXZMatch = true; } if (hasLatchedXZMatch) { var heldLocal = new Vector3( triggerAction.useLocalX ? latchedXZMatch.x : localRelativeToTarget.x, localRelativeToTarget.y, triggerAction.useLocalZ ? latchedXZMatch.y : localRelativeToTarget.z); var heldWorld = matchTarget.TransformPoint(heldLocal); rootPosition.x = heldWorld.x; rootPosition.z = heldWorld.z; } } // Y is not held the same way: its curve can saturate while the target keeps rising (e.g. a // climb), so releasing it back to the live target lets the motion finish. if (evaluatedY < 1f) { rootPosition.y = Mathf.Lerp(rootPosition.y, matchTargetPosition.y, evaluatedY); finishPositionYMatch = true; } else if (finishPositionYMatch) { finishPositionYMatch = false; rootPosition.y = matchTargetPosition.y; } transform.position = rootPosition; } /// Offsets the target so the selected bone reaches it. protected virtual Vector3 GetMatchTargetPosition() { var targetPosition = triggerAction.matchTarget.position; var bone = GetAvatarTargetBone(triggerAction.avatarTarget); if (bone == null) { return targetPosition; } return targetPosition - transform.rotation * transform.InverseTransformPoint(bone.position); } protected virtual Transform GetAvatarTargetBone(AvatarTarget avatarTarget) { switch (avatarTarget) { case AvatarTarget.LeftHand: return tpInput.animator.GetBoneTransform(HumanBodyBones.LeftHand); case AvatarTarget.RightHand: return tpInput.animator.GetBoneTransform(HumanBodyBones.RightHand); case AvatarTarget.LeftFoot: return tpInput.animator.GetBoneTransform(HumanBodyBones.LeftFoot); case AvatarTarget.RightFoot: return tpInput.animator.GetBoneTransform(HumanBodyBones.RightFoot); default: return null; } } protected virtual void EvaluateToTargetRotation() { var targetEuler = new Vector3(transform.eulerAngles.x, triggerAction.transform.eulerAngles.y, transform.eulerAngles.z); Quaternion targetRotation = Quaternion.Euler(targetEuler); Quaternion rootRotation = GetActionRootRotation(); AnimationCurve rotationCurve = triggerAction.matchRotationCurve; float normalizedTime = GetActionNormalizedTime(triggerAction.animatorLayer); float evaluatedCurve = rotationCurve.Evaluate(normalizedTime); if (evaluatedCurve < 1) { rootRotation = Quaternion.Lerp(rootRotation, targetRotation, evaluatedCurve); finishRotationMatch = true; } else if (finishRotationMatch) { finishRotationMatch = false; rootRotation = targetRotation; } transform.rotation = rootRotation; } protected virtual void EndAction() { if (!triggerAction) { return; } var trigger = triggerAction; OnEndAction.Invoke(trigger); trigger.OnEndAnimation.Invoke(); OnExitTriggerAction.Invoke(trigger); ResetTriggerSettings(); DestroyTriggerIfRequested(trigger); LogDebug("End Action: " + trigger.actionName, trigger); } /// Tracks the requested action animation. public virtual bool playingAnimation { get { if (triggerAction == null || !doingAction || !actionStarted) { return _playingAnimation = false; } if (!actionAnimationDetected && Time.frameCount <= animationRequestFrame) { return false; } var detected = inActionAnimation; if (detected) { actionAnimationDetected = true; } if (!_playingAnimation && detected) { _playingAnimation = true; triggerAction.OnStartAnimation.Invoke(); } else if (_playingAnimation && !detected) { _playingAnimation = false; } return _playingAnimation; } protected set { _playingAnimation = value; } } public virtual bool actionConditions { get { return !doingAction && !actionStarted && !_playingAnimation && !tpInput.cc.isJumping && !tpInput.cc.customAction; } } #endregion #region Collider callbacks public override void OnActionEnter(Collider other) { if (isLockTriggerEvents) { return; } if (other == null || !other.gameObject.CompareTag(actionTag) || actions.ContainsKey(other)) { return; } vTriggerGenericAction[] _triggerActions = other.GetComponents(); for (int i = 0; i < _triggerActions.Length; i++) { var _triggerAction = _triggerActions[i]; if (_triggerAction && _triggerAction.enabled) { actions.Add(other, _triggerAction); _triggerAction.OnPlayerEnter.Invoke(gameObject); LogDebug("Enter Trigger: " + _triggerAction.actionName, other.gameObject); break; } } } public override void OnActionExit(Collider other) { if (isLockTriggerEvents) { return; } if (other != null && other.gameObject.CompareTag(actionTag) && actions.ContainsKey(other) && (!doingAction || triggerAction == null || other != triggerAction._collider)) { vTriggerGenericAction action = actions[other]; actions.Remove(other); action.OnPlayerExit.Invoke(gameObject); action.OnInvalidate.Invoke(gameObject); OnExitTriggerAction.Invoke(action); LogDebug("Exit Trigger: " + action.actionName, other.gameObject); } } public override void OnActionStay(Collider other) { if (isLockTriggerEvents) { return; } if (other != null && actions.TryGetValue(other, out var storage) && storage.action) { storage.action.OnPlayerStay.Invoke(gameObject); timeInTrigger = .5f; } } #endregion #region Public control /// Ends a manually controlled action. public virtual void FinishAction() { if (triggerAction && actionStarted && triggerAction.endActionManualy) { EndAction(); } } public virtual void CancelAction() { if (!triggerAction || !actionStarted) { return; } var trigger = triggerAction; trigger.OnCancelAction.Invoke(); OnExitTriggerAction.Invoke(triggerAction); ResetTriggerSettings(); DestroyTriggerIfRequested(trigger); LogDebug("Cancel Action: " + trigger.actionName, trigger); } #endregion #region Input handling public virtual void TriggerActionInput() { if (triggerAction == null || !triggerAction.gameObject.activeInHierarchy || (triggerAction.CanDoAction == false)) { return; } switch (triggerAction.inputType) { case vTriggerGenericAction.InputType.AutoAction: if (actionConditions) { TriggerActionEvents(); TriggerAnimation(); } break; case vTriggerGenericAction.InputType.GetButtonDown: if (actionConditions && triggerAction.actionInput.GetButtonDown()) { TriggerActionEvents(); TriggerAnimation(); } break; case vTriggerGenericAction.InputType.GetDoubleButton: if (actionConditions && triggerAction.actionInput.GetDoubleButtonDown(triggerAction.doubleButtomTime)) { TriggerActionEvents(); TriggerAnimation(); } break; case vTriggerGenericAction.InputType.GetButtonTimer: HandleButtonTimerInput(); break; } } /// Handles hold-to-act input. protected virtual void HandleButtonTimerInput() { if (_currentInputDelay > 0) { _currentInputDelay -= Time.deltaTime; return; } if (!doingAction && !actionConditions) { triggerAction.UpdateButtonTimer(0f); return; } bool playWhileHolding = triggerAction.playAnimationWhileHoldingButton; var released = false; var heldTime = 0f; TriggerActionEventsInput(); if (triggerAction.actionInput.GetButtonTimer(ref heldTime, ref released, triggerAction.buttonTimer)) { triggerAction.UpdateButtonTimer(0); triggerAction.OnFinishActionInput.Invoke(); if (playWhileHolding) { ResetActionState(); EndAction(); } else { TriggerAnimation(); } } if (triggerAction && triggerAction.actionInput.inButtomTimer) { triggerAction.UpdateButtonTimer(heldTime); if (playWhileHolding) { TriggerAnimation(); } } if (released && triggerAction) { CancelButtonTimer(); } } protected virtual void CancelButtonTimer() { triggerAction.OnCancelActionInput.Invoke(); _currentInputDelay = triggerAction.inputDelay; triggerAction.UpdateButtonTimer(0); OnCancelAction.Invoke(triggerAction); ResetActionState(); ResetTriggerSettings(false); } protected virtual void TriggerActionEventsInput() { if (triggerAction && triggerAction.actionInput.GetButtonDown()) { TriggerActionEvents(); } } public virtual void TriggerActionEvents() { if (!triggerAction || doingAction) { return; } var action = triggerAction; doingAction = true; OnStartAction.Invoke(action); OnDoAction.Invoke(action); StartCoroutine(action.OnPressActionDelay(gameObject)); LogDebug("Start Action: " + action.actionName, action); } public virtual void TriggerAnimation() { if (!triggerAction || !doingAction || actionStarted) { return; } actionStarted = true; actionAnimationDetected = false; _playingAnimation = false; // The origin is captured on the first match frame of THIS action; clear any held from a // previous one, which ResetTriggerSettings only covers on the cancel/end paths. hasMatchOriginXZ = false; hasLatchedXZMatch = false; animationRequestFrame = Time.frameCount; animationBehaviourDelay = Mathf.Max(0.1f, triggerAction.crossFadeTransition + 0.1f); // A trigger used only for events (such as a collectible) has no action animation to // synchronize with. Do not interrupt locomotion for that one frame before it ends. if (string.IsNullOrEmpty(triggerAction.playAnimation)) { actionAnimationDetected = true; return; } PreparePlayerForAction(); if (triggerAction.animatorActionState != 0) { tpInput.cc.SetActionState(triggerAction.animatorActionState); } PlayActionAnimation(triggerAction.playAnimation, triggerAction.crossFadeTransition, triggerAction.animatorLayer); if (!string.IsNullOrEmpty(triggerAction.customCameraState)) { tpInput.ChangeCameraState(triggerAction.customCameraState, true); } LogDebug("Play Animation: " + triggerAction.playAnimation, triggerAction); } #endregion #region Reset & physics hand-off public virtual void ResetActionState() { if (triggerAction && triggerAction.resetAnimatorActionState) { tpInput.cc.SetActionState(0); } } public virtual void ResetTriggerSettings(bool removeTrigger = true) { RestorePlayerAfterAction(); ResetActionState(); if (triggerAction != null && !string.IsNullOrEmpty(triggerAction.customCameraState)) { tpInput.ResetCameraState(); } if (triggerAction != null && removeTrigger && triggerAction._collider) { actions.Remove(triggerAction._collider); } triggerAction = null; doingAction = false; actionStarted = false; actionAnimationDetected = false; _playingAnimation = false; animationBehaviourDelay = 0f; finishRotationMatch = false; finishPositionXZMatch = false; finishPositionYMatch = false; hasLatchedXZMatch = false; hasMatchOriginXZ = false; } /// Stops locomotion and captures the player state. protected virtual void PreparePlayerForAction() { if (playerStatePrepared || tpInput == null || tpInput.cc == null) { return; } previousLockMovement = tpInput.cc.lockMovement; previousLockMoveInput = tpInput.lockMoveInput; playerStatePrepared = true; tpInput.cc.lockMovement = true; tpInput.lockMoveInput = true; tpInput.cc.StopCharacter(); DisablePlayerGravityAndCollision(); } /// Restores the player state captured for the action. protected virtual void RestorePlayerAfterAction() { if (!playerStatePrepared && !actionSuspendedInterpolation && !actionDisabledGravity && !actionDisabledCollision) { return; } EnablePlayerGravityAndCollision(); if (!playerStatePrepared || tpInput == null || tpInput.cc == null) { return; } tpInput.cc.lockMovement = previousLockMovement; tpInput.lockMoveInput = previousLockMoveInput; playerStatePrepared = false; } public virtual void DisablePlayerGravityAndCollision() { if (!triggerAction || tpInput == null || tpInput.cc == null) { return; } if (!actionSuspendedInterpolation && (triggerAction.matchTarget != null || triggerAction.useTriggerRotation || triggerAction.disableGravity)) { tpInput.cc.SetRigidbodyInterpolation(false); actionSuspendedInterpolation = true; } if (triggerAction.disableGravity && !actionDisabledGravity) { var body = tpInput.cc._rigidbody; previousUseGravity = body.useGravity; previousIsKinematic = body.isKinematic; actionDisabledGravity = true; body.useGravity = false; if (!body.isKinematic) { body.linearVelocity = Vector3.zero; } body.isKinematic = true; } if (triggerAction.disableCollision && !actionDisabledCollision) { var capsule = tpInput.cc._capsuleCollider; previousColliderIsTrigger = capsule.isTrigger; actionDisabledCollision = true; capsule.isTrigger = true; } } public virtual void EnablePlayerGravityAndCollision() { if (tpInput == null || tpInput.cc == null) { return; } if (actionSuspendedInterpolation) { tpInput.cc.SetRigidbodyInterpolation(true); actionSuspendedInterpolation = false; } if (actionDisabledGravity) { var body = tpInput.cc._rigidbody; body.isKinematic = previousIsKinematic; body.useGravity = previousUseGravity; actionDisabledGravity = false; if (!body.isKinematic) { tpInput.cc.ResyncAfterPhysicsHandoff(); } } if (actionDisabledCollision) { tpInput.cc._capsuleCollider.isTrigger = previousColliderIsTrigger; actionDisabledCollision = false; } } protected virtual void DestroyTriggerIfRequested(vTriggerGenericAction trigger) { if (trigger && trigger.destroyAfter) { StartCoroutine(DestroyActionDelay(trigger)); } } public virtual IEnumerator DestroyActionDelay(vTriggerGenericAction triggerAction) { var _triggerAction = triggerAction; yield return new WaitForSeconds(_triggerAction.destroyDelay); if (_triggerAction != null && _triggerAction.gameObject != null) { OnExitTriggerAction.Invoke(triggerAction); Destroy(_triggerAction.gameObject); } } public virtual void SetLockTriggerEvents(bool value) { foreach (var storage in actions.Values) { if (storage.action) { storage.action.OnPlayerExit.Invoke(gameObject); storage.action.OnInvalidate.Invoke(gameObject); } } actions.Clear(); isLockTriggerEvents = value; } #endregion #region Debug /// Logs the main action lifecycle when debug mode is enabled. protected void LogDebug(string message, Object context = null) { if (!debugMode) { return; } Debug.Log($"GenericAction: {message}", context ? context : gameObject); } #endregion } }