1245 lines
42 KiB
C#
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
|
|
}
|
|
}
|