MythicalWar/Assets/Invector-3rdPersonController/Basic Locomotion/Scripts/CharacterController/Actions/vGenericAction.cs

1245 lines
42 KiB
C#

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.\n<b><size=12>You can use <color=red>vGenericActionReceiver</color> component to filter events by action name</size></b>", 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<Collider> colliders = new List<Collider>();
[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; }
/// <summary>True after the requested animation starts.</summary>
protected bool actionAnimationDetected;
protected bool finishRotationMatch;
protected bool finishPositionXZMatch;
protected bool finishPositionYMatch;
/// <summary>
/// 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.
/// </summary>
protected Vector2 latchedXZMatch;
protected bool hasLatchedXZMatch;
/// <summary>
/// XZ the root had when the action's match began, held while the XZ curve is still ramping.
/// See <see cref="EvaluateToTargetPosition"/> for why.
/// </summary>
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;
/// <summary>Reusable buffer for nearby trigger selection.</summary>
private readonly List<Collider> nearActionScratch = new List<Collider>();
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<Collider, ActionStorage> 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<Collider, ActionStorage>();
}
protected override void Start()
{
base.Start();
tpInput = GetComponent<vThirdPersonInput>();
#if INVECTOR_PLAYABLESANIMATOR
playablesAnimator = GetComponent<PlayablesAnimator>();
#endif
var actionsReceivers = GetComponentsInChildren<IActionReceiver>();
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;
}
/// <summary>Updates the action from the Animator root-motion callback.</summary>
public virtual void OnAnimatorMoveEvent()
{
#if INVECTOR_PLAYABLESANIMATOR
if (UsingPlayables)
{
return;
}
#endif
UpdateGenericAction();
}
#if INVECTOR_PLAYABLESANIMATOR
/// <summary>Updates the action from the Playables root-motion callback.</summary>
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
/// <summary>True while Playables drives the character.</summary>
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();
}
/// <summary>Selects the valid trigger nearest to the screen center.</summary>
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;
}
/// <summary>Checks whether a trigger can replace the current candidate.</summary>
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);
}
/// <summary>Invalidates a trigger that is no longer selectable.</summary>
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();
}
}
/// <summary>Checks whether an automatic action reached its exit time.</summary>
protected virtual bool ReachedAnimationExitTime()
{
if (!actionStarted || triggerAction.endActionManualy)
{
return false;
}
if (IsHoldToPlayAction(triggerAction))
{
return false;
}
return GetActionNormalizedTime(triggerAction.animatorLayer) >= triggerAction.endExitTimeAnimation;
}
/// <summary>Checks whether a stopped animation must end the action.</summary>
protected virtual bool ShouldForceEndAction()
{
if (!doingAction || !actionStarted)
{
return false;
}
if (triggerAction == null)
{
return true;
}
if (!actionAnimationDetected)
{
return false;
}
return !triggerAction.endActionManualy && !IsHoldToPlayAction(triggerAction);
}
/// <summary>Checks whether the animation follows held input.</summary>
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;
}
/// <summary>Offsets the target so the selected bone reaches it.</summary>
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);
}
/// <summary>Tracks the requested action animation.</summary>
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<vTriggerGenericAction>();
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
/// <summary>Ends a manually controlled action.</summary>
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;
}
}
/// <summary>Handles hold-to-act input.</summary>
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;
}
/// <summary>Stops locomotion and captures the player state.</summary>
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();
}
/// <summary>Restores the player state captured for the action.</summary>
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
/// <summary>Logs the main action lifecycle when debug mode is enabled.</summary>
protected void LogDebug(string message, Object context = null)
{
if (!debugMode)
{
return;
}
Debug.Log($"<b>GenericAction: </b>{message}", context ? context : gameObject);
}
#endregion
}
}