1128 lines
41 KiB
C#
1128 lines
41 KiB
C#
using System.Collections;
|
|
using System.Collections.Generic;
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
using InvectorCore.Playables;
|
|
using InvectorCore;
|
|
#endif
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
|
|
namespace Invector.vCharacterController.vActions
|
|
{
|
|
[vClassHeader("Ladder Action", iconName = "ladderIcon")]
|
|
public class vLadderAction : vActionListener, vIAnimatorMoveReceiver
|
|
{
|
|
private const string ClimbLadderTag = "ClimbLadder";
|
|
private const string SlideDownLadderTag = "LadderSlideDown";
|
|
|
|
// PDM state tags marking which hand a ladder state leaves planted when it ends.
|
|
private const string LadderFootLeftTag = "LadderFootLeft";
|
|
private const string LadderFootRightTag = "LadderFootRight";
|
|
|
|
private const string EnterLadderTopState = "EnterLadderTop";
|
|
private const string EnterLadderBottomState = "EnterLadderBottom";
|
|
private const string ExitLadderTopState = "ExitLadderTop";
|
|
private const string ExitLadderBottomState = "ExitLadderBottom";
|
|
private const string QuickExitLadderState = "QuickExitLadder";
|
|
private const string SlideDownLadderState = "Ladder_SlideDown";
|
|
private const string IdleLadderLeftState = "IdleLadder_Left";
|
|
private const string IdleLadderRightState = "IdleLadder_Right";
|
|
|
|
private const float EnterAnimationTransitionDuration = 0.25f;
|
|
private const float ExitAnimationTransitionDuration = 0.1f;
|
|
private const float SlideDownTransitionDuration = 0.2f;
|
|
private const float ClimbInputThreshold = 0.05f;
|
|
private const float TriggerForwardDistanceThreshold = 0.8f;
|
|
private const float ExitResetNormalizedTime = 0.8f;
|
|
|
|
// The quick exit drops the character into open air, so it hands the body back to physics as soon
|
|
// as its clip stops owning Y - matched to the row's root-motion Y range and LockMovement tag
|
|
// window. The triggered exits end standing on ground and keep root motion to the end of the clip,
|
|
// so they stay on the later shared gate above.
|
|
private const float QuickExitResetNormalizedTime = 0.6f;
|
|
private const float LadderInputSmoothingDuration = 0.1f;
|
|
private const float AnimatorInputDampTime = 0.1f;
|
|
private const float StaminaRecoveryDelay = 0.25f;
|
|
|
|
#region public variables
|
|
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
[vEditorToolbar("PDM", overrideChildOrder: true, order = 0)]
|
|
[Header("Playables Animator")]
|
|
|
|
[Tooltip("PDM that contains only the ladder states. The PDM active before entering the ladder is restored on exit.")]
|
|
public PlayablesDecisionMaker ladderPDM;
|
|
#endif
|
|
|
|
|
|
[vEditorToolbar("Settings", overrideChildOrder: true, order = 0)]
|
|
[Tooltip("Tag of the object you want to access")]
|
|
public string actionTag = "LadderTrigger";
|
|
[Tooltip("Speed multiplier for the climb ladder animations")]
|
|
public float climbSpeed = 1.5f;
|
|
[Tooltip("Speed multiplier for the climb ladder animations when the fastClimbInput is pressed")]
|
|
public float fastClimbSpeed = 3f;
|
|
[Tooltip("How much Stamina will be consumed when climbing faster")]
|
|
public float fastClimbStamina = 30f;
|
|
[Tooltip("Input to use the ladder going up or down")]
|
|
public GenericInput verticalInput = new GenericInput("Vertical", "LeftAnalogVertical", "Vertical");
|
|
[Tooltip("Input to enter the ladder")]
|
|
public GenericInput enterInput = new GenericInput("E", "A", "A");
|
|
[Tooltip("Input to exit the ladder")]
|
|
public GenericInput exitInput = new GenericInput("Space", "B", "B");
|
|
[Tooltip("Input to climb faster")]
|
|
public GenericInput fastClimbInput = new GenericInput("LeftShift", "LeftStickClick", "LeftStickClick");
|
|
[Tooltip("Input to climb faster")]
|
|
public GenericInput slideDownInput = new GenericInput("Q", "X", "X");
|
|
|
|
|
|
[vEditorToolbar("Events")]
|
|
public UnityEvent OnEnterLadder;
|
|
public UnityEvent OnExitLadder;
|
|
public UnityEvent OnEnterTriggerLadder;
|
|
public UnityEvent OnExitTriggerLadder;
|
|
|
|
|
|
[vEditorToolbar("Debug")]
|
|
public bool debugMode;
|
|
[vReadOnly(false)]
|
|
[SerializeField]
|
|
protected vTriggerLadderAction targetLadderAction;
|
|
[vReadOnly(false)]
|
|
[SerializeField]
|
|
protected vTriggerLadderAction currentLadderAction;
|
|
//[vReadOnly(false)]
|
|
//[SerializeField]
|
|
protected List<vTriggerLadderAction> actionTriggers = new List<vTriggerLadderAction>();
|
|
[vReadOnly(false)]
|
|
[SerializeField]
|
|
protected float _speed;
|
|
protected virtual float speed { get { return _speed; } set { _speed = value; } }
|
|
[vReadOnly(false)]
|
|
[SerializeField]
|
|
protected float currentClimbSpeed;
|
|
[vReadOnly(false)]
|
|
[SerializeField]
|
|
protected bool _isUsingLadder;
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
[ExposeCondition]
|
|
#endif
|
|
public virtual bool isUsingLadder { get { return _isUsingLadder; } set { _isUsingLadder = value; } }
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
[vReadOnly(false)]
|
|
[SerializeField]
|
|
protected float ladderInputSmooth;
|
|
|
|
/// <summary>Input used by the ladder PDM rows.</summary>
|
|
[ExposeCondition]
|
|
public virtual float LadderInputVertical { get { return ladderInputSmooth; } }
|
|
|
|
/// <summary>Clip retime factor for the UsingLadder PDM rows.</summary>
|
|
[ExposeCondition]
|
|
public virtual float LadderClimbSpeed { get { return currentClimbSpeed; } }
|
|
|
|
#endif
|
|
|
|
public enum vLadderSide { Left, Right }
|
|
|
|
[vReadOnly(false)]
|
|
[SerializeField]
|
|
protected vLadderSide ladderSide = vLadderSide.Left;
|
|
|
|
/// <summary>Which side the character is planted on; the PDM condition for the next state.</summary>
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
[ExposeCondition]
|
|
#endif
|
|
public virtual vLadderSide LadderSide { get { return ladderSide; } }
|
|
[vReadOnly(false)]
|
|
[SerializeField]
|
|
protected bool enterLadderStarted;
|
|
[vReadOnly(false)]
|
|
[SerializeField]
|
|
protected bool inEnterLadderAnimation;
|
|
[vReadOnly(false)]
|
|
[SerializeField]
|
|
protected bool inExitingLadderAnimation;
|
|
[vReadOnly(false)]
|
|
[SerializeField]
|
|
protected bool triggerEnterOnce;
|
|
|
|
/// <summary>
|
|
/// Root XZ captured on the first frame of the enter match, so the outgoing run's root motion
|
|
/// stops deciding where the body goes while the enter curve is still near zero.
|
|
/// </summary>
|
|
protected Vector2 matchOriginXZ;
|
|
|
|
protected bool hasMatchOriginXZ;
|
|
|
|
/// <summary>When the enter match started, so the hold can be bounded to the cross-fade.</summary>
|
|
protected float matchOriginTime;
|
|
[vReadOnly(false)]
|
|
[SerializeField]
|
|
protected bool triggerExitOnce;
|
|
|
|
#endregion
|
|
|
|
protected vThirdPersonInput tpInput;
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
protected PlayablesAnimator playablesAnimator;
|
|
protected PlayablesDecisionMaker previousPDM;
|
|
protected bool ladderPDMIsActive;
|
|
protected Coroutine restorePreviousPDMCoroutine;
|
|
|
|
protected virtual bool UsingPlayables => playablesAnimator && playablesAnimator.isActiveAndEnabled;
|
|
protected virtual bool CanUseLadderPDM => UsingPlayables && ladderPDM;
|
|
#endif
|
|
|
|
protected bool isAligningMidAir;
|
|
|
|
protected override void SetUpListener()
|
|
{
|
|
actionEnter = false;
|
|
actionStay = true;
|
|
actionExit = true;
|
|
}
|
|
|
|
protected override void Start()
|
|
{
|
|
base.Start();
|
|
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
playablesAnimator = GetComponent<PlayablesAnimator>();
|
|
if (playablesAnimator && !playablesAnimator.isActiveAndEnabled)
|
|
{
|
|
playablesAnimator = null;
|
|
}
|
|
#endif
|
|
|
|
tpInput = GetComponent<vThirdPersonInput>();
|
|
if (tpInput)
|
|
{
|
|
tpInput.onUpdate -= UpdateLadderBehavior;
|
|
tpInput.onUpdate += UpdateLadderBehavior;
|
|
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
if (UsingPlayables)
|
|
{
|
|
playablesAnimator.OnPlayablesStateEnter -= HandleLadderStateEnter;
|
|
playablesAnimator.OnPlayablesStateEnter += HandleLadderStateEnter;
|
|
}
|
|
#endif
|
|
}
|
|
}
|
|
|
|
// Detaches callbacks registered in Start, so a runtime-spawned character doesn't leak this action.
|
|
protected virtual void OnDestroy()
|
|
{
|
|
if (tpInput)
|
|
{
|
|
tpInput.onUpdate -= UpdateLadderBehavior;
|
|
}
|
|
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
if (playablesAnimator)
|
|
{
|
|
playablesAnimator.OnPlayablesStateEnter -= HandleLadderStateEnter;
|
|
}
|
|
#endif
|
|
}
|
|
|
|
// Routed by vAnimatorMoveSender. Skipped while Playables drives so the climb doesn't tick twice.
|
|
public virtual void OnAnimatorMoveEvent()
|
|
{
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
if (UsingPlayables)
|
|
{
|
|
return;
|
|
}
|
|
#endif
|
|
UsingLadder();
|
|
}
|
|
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
// PlayablesAnimator's counterpart to OnAnimatorMoveEvent.
|
|
protected virtual void OnPlayablesMove()
|
|
{
|
|
UsingLadder();
|
|
}
|
|
#endif
|
|
|
|
protected virtual void UpdateLadderBehavior()
|
|
{
|
|
AutoEnterLadder();
|
|
EnterLadderInput();
|
|
ExitLadderInput();
|
|
}
|
|
|
|
protected virtual void EnterLadderInput()
|
|
{
|
|
if (targetLadderAction == null || tpInput.cc.customAction || tpInput.cc.isJumping || !tpInput.cc.isGrounded || tpInput.cc.isRolling)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (enterInput.GetButtonDown() && !enterLadderStarted && !isUsingLadder && !targetLadderAction.autoAction)
|
|
{
|
|
TriggerEnterLadder();
|
|
}
|
|
}
|
|
|
|
protected virtual bool IsUsingLadder()
|
|
{
|
|
return tpInput.cc.IsAnimatorTag(ClimbLadderTag);
|
|
}
|
|
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
// Reads which hand the committed state ends on, not whatever ladderSide already held, so a
|
|
// forced idle (TriggerMidAirEnterLadder, the enter states) resyncs instead of drifting. Down
|
|
// states reuse the Up clips at negative speed, so their planted side is tagged rather than
|
|
// derived from the name.
|
|
protected virtual void HandleLadderStateEnter(in PlayablesStateContext context)
|
|
{
|
|
if (context.Layer == 0 && playablesAnimator.HasAnyTag(LadderFootLeftTag, LadderFootRightTag))
|
|
{
|
|
ladderSide = playablesAnimator.HasTag(LadderFootLeftTag) ? vLadderSide.Left : vLadderSide.Right;
|
|
}
|
|
}
|
|
#endif
|
|
|
|
// Animator-only: its climb loop is one state, so a transition means "still entering". The PDM
|
|
// alternates Left/Right every climb cycle and would report a transition almost every frame,
|
|
// blocking exit forever.
|
|
protected virtual bool IsInExitBlockingTransition()
|
|
{
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
if (UsingPlayables)
|
|
{
|
|
return false;
|
|
}
|
|
#endif
|
|
return tpInput.cc.animator.IsInTransition(0);
|
|
}
|
|
|
|
protected virtual bool IsPlayingLadderState(string stateName)
|
|
{
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
if (UsingPlayables)
|
|
{
|
|
return playablesAnimator.IsPlayingState(stateName);
|
|
}
|
|
#endif
|
|
if (tpInput.cc.baseLayerInfo.IsName(stateName))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
// During CrossFade the CURRENT state is still the one being left, so gating on it stays
|
|
// shut for the whole blend and the match starts late, covering the full angle in a few
|
|
// frames (the snap). The destination state is what's actually playing in.
|
|
var animator = tpInput.cc.animator;
|
|
return animator.IsInTransition(0) && animator.GetNextAnimatorStateInfo(0).IsName(stateName);
|
|
}
|
|
|
|
/// <summary>Returns the active base-layer playback progress for match-target curve evaluation.</summary>
|
|
protected virtual float GetLadderNormalizedTime()
|
|
{
|
|
float normalizedTime;
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
if (UsingPlayables)
|
|
{
|
|
normalizedTime = playablesAnimator.GetNormalizedTime(0);
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
// GetNextAnimatorStateInfo(0).normalizedTime is NOT the destination clip's progress
|
|
// during a blend - it reports near 1 and snaps the match to its final pose. Scale the
|
|
// transition's own normalizedTime by blend duration over clip length instead.
|
|
var animator = tpInput.cc.animator;
|
|
if (animator.IsInTransition(0))
|
|
{
|
|
var transition = animator.GetAnimatorTransitionInfo(0);
|
|
var next = animator.GetNextAnimatorStateInfo(0);
|
|
float clipLength = next.length > 0.0001f ? next.length : 1f;
|
|
normalizedTime = Mathf.Clamp01(transition.normalizedTime) * (transition.duration / clipLength);
|
|
}
|
|
else
|
|
{
|
|
normalizedTime = tpInput.cc.baseLayerInfo.normalizedTime;
|
|
}
|
|
}
|
|
|
|
return normalizedTime;
|
|
}
|
|
|
|
protected virtual void SetLadderAnimationSpeed(float value)
|
|
{
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
if (UsingPlayables)
|
|
{
|
|
return;
|
|
}
|
|
#endif
|
|
tpInput.cc.animator.speed = value;
|
|
}
|
|
|
|
// PlayablesAnimator resolves its own transitions; Animator uses the supplied blend duration.
|
|
protected virtual void PlayAnimation(string stateName, float transitionDuration)
|
|
{
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
if (UsingPlayables)
|
|
{
|
|
playablesAnimator.PlayState(stateName);
|
|
return;
|
|
}
|
|
#endif
|
|
tpInput.cc.animator.CrossFadeInFixedTime(stateName, transitionDuration);
|
|
}
|
|
|
|
protected virtual bool InEnterLadderAnimation()
|
|
{
|
|
return IsPlayingLadderState(EnterLadderTopState) || IsPlayingLadderState(EnterLadderBottomState);
|
|
}
|
|
|
|
// Separate from InEnterLadderAnimation: only a transition heading AWAY from the enter states
|
|
// should block the match, not the ENTER blend itself - otherwise the match starts late and snaps.
|
|
protected virtual bool InEnterLadderAnimationMatchTarget()
|
|
{
|
|
if (!InEnterLadderAnimation())
|
|
{
|
|
return false;
|
|
}
|
|
|
|
return !IsLeavingEnterLadderAnimation();
|
|
}
|
|
|
|
// True while the base layer is blending OUT of an enter-ladder state, as opposed to into one.
|
|
protected virtual bool IsLeavingEnterLadderAnimation()
|
|
{
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
if (UsingPlayables)
|
|
{
|
|
return false;
|
|
}
|
|
#endif
|
|
var animator = tpInput.cc.animator;
|
|
if (!animator.IsInTransition(0))
|
|
{
|
|
return false;
|
|
}
|
|
|
|
var next = animator.GetNextAnimatorStateInfo(0);
|
|
return !next.IsName(EnterLadderTopState) && !next.IsName(EnterLadderBottomState);
|
|
}
|
|
|
|
// Animator only: Playables rows don't expose Animator tags, so it stays false for that backend.
|
|
protected virtual bool InSlideDownAnimation()
|
|
{
|
|
return tpInput.cc.IsAnimatorTag(SlideDownLadderTag);
|
|
}
|
|
|
|
protected virtual bool InExitLadderAnimation()
|
|
{
|
|
return IsPlayingLadderState(ExitLadderTopState) || IsPlayingLadderState(ExitLadderBottomState) || IsPlayingLadderState(QuickExitLadderState);
|
|
}
|
|
|
|
protected virtual void ExitLadderInput()
|
|
{
|
|
if (!isUsingLadder || InEnterLadderAnimation())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (targetLadderAction == null)
|
|
{
|
|
HandleExitWithoutTrigger();
|
|
return;
|
|
}
|
|
|
|
currentLadderAction = targetLadderAction;
|
|
var exitState = targetLadderAction.exitAnimation;
|
|
if (exitState == ExitLadderBottomState)
|
|
{
|
|
TryExitAtBottom(exitState);
|
|
}
|
|
else if (exitState == ExitLadderTopState && IsUsingLadder())
|
|
{
|
|
TryExitAtTop(exitState);
|
|
}
|
|
}
|
|
|
|
private void HandleExitWithoutTrigger()
|
|
{
|
|
if (!IsUsingLadder())
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (slideDownInput.GetButtonDown() && !inExitingLadderAnimation)
|
|
{
|
|
PlayAnimation(SlideDownLadderState, SlideDownTransitionDuration);
|
|
}
|
|
|
|
if (!exitInput.GetButtonDown())
|
|
{
|
|
return;
|
|
}
|
|
|
|
LogLadderAction("Quick Exit");
|
|
SetLadderAnimationSpeed(1f);
|
|
PlayAnimation(QuickExitLadderState, ExitAnimationTransitionDuration);
|
|
}
|
|
|
|
private void TryExitAtBottom(string exitState)
|
|
{
|
|
var shouldExit = !triggerExitOnce &&
|
|
(exitInput.GetButtonDown() || speed <= -ClimbInputThreshold || InSlideDownAnimation());
|
|
if (!shouldExit)
|
|
{
|
|
return;
|
|
}
|
|
|
|
LogLadderAction("Exit Bottom");
|
|
triggerExitOnce = true;
|
|
PlayAnimation(exitState, ExitAnimationTransitionDuration);
|
|
}
|
|
|
|
private void TryExitAtTop(string exitState)
|
|
{
|
|
if (triggerExitOnce || speed < ClimbInputThreshold || IsInExitBlockingTransition())
|
|
{
|
|
return;
|
|
}
|
|
|
|
LogLadderAction("Exit Top");
|
|
triggerExitOnce = true;
|
|
PlayAnimation(exitState, ExitAnimationTransitionDuration);
|
|
}
|
|
|
|
protected virtual void AutoEnterLadder()
|
|
{
|
|
if (targetLadderAction == null || !targetLadderAction.autoAction)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (tpInput.cc.customAction || isUsingLadder || IsInExitBlockingTransition() || tpInput.cc.isRolling)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (tpInput.cc.input != Vector3.zero)
|
|
{
|
|
var activeCamera = Camera.main;
|
|
if (!activeCamera)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var inputDir = activeCamera.transform.TransformDirection(new Vector3(tpInput.cc.input.x, 0f, tpInput.cc.input.z));
|
|
inputDir.y = 0f;
|
|
var dist = Vector3.Distance(inputDir.normalized, targetLadderAction.transform.forward);
|
|
if (dist < TriggerForwardDistanceThreshold)
|
|
{
|
|
TriggerEnterLadder();
|
|
}
|
|
}
|
|
}
|
|
|
|
public virtual void TriggerMidAirEnterLadder(vTriggerLadderMiddle ladderMiddle)
|
|
{
|
|
if (tpInput.cc.isGrounded || isAligningMidAir)
|
|
{
|
|
return;
|
|
}
|
|
|
|
isAligningMidAir = true;
|
|
|
|
var targetPosition = ladderMiddle.refTarget.position;
|
|
var foundClosestPoint = false;
|
|
var closestDistance = Vector3.Distance(targetPosition, transform.position);
|
|
var stepCount = (int)(ladderMiddle._collider.size.y / ladderMiddle.stepHeight);
|
|
|
|
// Walk up the rungs and keep the closest one; stop once distance starts increasing again.
|
|
for (var i = 0; i < stepCount; i++)
|
|
{
|
|
var candidatePosition = ladderMiddle.refTarget.position + ladderMiddle.transform.up * ladderMiddle.stepHeight * i;
|
|
var candidateDistance = Vector3.Distance(candidatePosition, transform.position);
|
|
|
|
if (candidateDistance <= closestDistance)
|
|
{
|
|
targetPosition = candidatePosition;
|
|
closestDistance = candidateDistance;
|
|
foundClosestPoint = true;
|
|
}
|
|
else if (foundClosestPoint)
|
|
{
|
|
break;
|
|
}
|
|
}
|
|
|
|
tpInput.cc.DisableGravityAndCollision();
|
|
tpInput.cc.isCrouching = false;
|
|
tpInput.cc.ControlCapsuleHeight();
|
|
tpInput.UpdateCameraStates();
|
|
tpInput.cc.UpdateAnimator();
|
|
|
|
triggerEnterOnce = true;
|
|
enterLadderStarted = true;
|
|
tpInput.cc.animator.SetInteger(vAnimatorParameters.ActionState, 1); // set actionState 1 to avoid falling transitions
|
|
|
|
tpInput.cc.StopCharacter();
|
|
tpInput.SetLockAllInput(true);
|
|
tpInput.cc.ResetInputAnimatorParameters();
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
SwitchToLadderPDM();
|
|
#endif
|
|
|
|
var localPos = ladderMiddle.transform.InverseTransformPoint(transform.position);
|
|
var idleState = localPos.x >= 0f ? IdleLadderLeftState : IdleLadderRightState;
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
if (UsingPlayables)
|
|
{
|
|
// PDM has no ActionState guard against Falling, only IsUsingLadder - without this the
|
|
// idle state loses the next evaluation to Falling before AlignToMiddleLadder sets it.
|
|
isUsingLadder = true;
|
|
}
|
|
#endif
|
|
PlayAnimation(idleState, EnterAnimationTransitionDuration);
|
|
|
|
tpInput.cc.disableAnimations = true;
|
|
|
|
StartCoroutine(AlignToMiddleLadder(targetPosition, ladderMiddle.refTarget.rotation));
|
|
}
|
|
|
|
protected virtual IEnumerator AlignToMiddleLadder(Vector3 targetPosition, Quaternion targetRotation)
|
|
{
|
|
float time = 0f;
|
|
Vector3 initialPosition = transform.position;
|
|
Quaternion initialRotation = transform.rotation;
|
|
|
|
while (time <= 1f)
|
|
{
|
|
transform.position = Vector3.Lerp(initialPosition, targetPosition, time);
|
|
transform.rotation = Quaternion.Lerp(initialRotation, targetRotation, time);
|
|
|
|
time += Time.fixedDeltaTime * 8f;
|
|
|
|
yield return null;
|
|
}
|
|
|
|
|
|
// Pin the pose until the idle blend settles, so its root motion can't drag the character off
|
|
// the ladder. Uses IsInExitBlockingTransition, not IsInLadderTransition - see its summary.
|
|
do
|
|
{
|
|
transform.position = targetPosition;
|
|
transform.rotation = targetRotation;
|
|
yield return null;
|
|
}
|
|
while (IsInExitBlockingTransition());
|
|
yield return new WaitForEndOfFrame();
|
|
isUsingLadder = true;
|
|
isAligningMidAir = false;
|
|
}
|
|
|
|
protected virtual void TriggerEnterLadder()
|
|
{
|
|
if (debugMode)
|
|
{
|
|
Debug.Log("Enter Ladder", this);
|
|
}
|
|
|
|
OnExitTriggerLadder?.Invoke();
|
|
|
|
if (targetLadderAction.targetCharacterParent)
|
|
{
|
|
transform.parent = targetLadderAction.targetCharacterParent;
|
|
}
|
|
|
|
tpInput.cc.isCrouching = false;
|
|
tpInput.cc.ControlCapsuleHeight();
|
|
tpInput.UpdateCameraStates();
|
|
tpInput.cc.UpdateAnimator();
|
|
OnEnterLadder?.Invoke();
|
|
triggerEnterOnce = true;
|
|
enterLadderStarted = true;
|
|
tpInput.cc.animator.SetInteger(vAnimatorParameters.ActionState, 1); // set actionState 1 to avoid falling transitions
|
|
tpInput.SetLockAllInput(true);
|
|
tpInput.cc.ResetInputAnimatorParameters();
|
|
targetLadderAction.OnDoAction?.Invoke();
|
|
currentLadderAction = targetLadderAction;
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
SwitchToLadderPDM();
|
|
#endif
|
|
if (!string.IsNullOrEmpty(currentLadderAction.playAnimation))
|
|
{
|
|
if (debugMode)
|
|
{
|
|
LogLadderAction("Trigger Animation");
|
|
}
|
|
|
|
PlayAnimation(currentLadderAction.playAnimation, EnterAnimationTransitionDuration);
|
|
isUsingLadder = true;
|
|
tpInput.cc.disableAnimations = true;
|
|
tpInput.cc.StopCharacter();
|
|
}
|
|
}
|
|
|
|
protected virtual void UsingLadder()
|
|
{
|
|
if (!isUsingLadder)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// update the base layer to know what animations are being played
|
|
tpInput.cc.AnimatorLayerControl();
|
|
tpInput.cc.ActionsControl();
|
|
// update camera movement
|
|
tpInput.CameraInput();
|
|
|
|
// go up or down
|
|
speed = verticalInput.GetAxis();
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
if (UsingPlayables)
|
|
{
|
|
ladderInputSmooth = InEnterLadderAnimation()
|
|
? 0f
|
|
: Mathf.MoveTowards(ladderInputSmooth, speed, Time.deltaTime / LadderInputSmoothingDuration);
|
|
}
|
|
else
|
|
#endif
|
|
{
|
|
tpInput.cc.animator.SetFloat(vAnimatorParameters.InputVertical, speed, AnimatorInputDampTime, Time.deltaTime);
|
|
if (speed >= ClimbInputThreshold || speed <= -ClimbInputThreshold)
|
|
{
|
|
tpInput.cc.animator.speed = Mathf.Lerp(tpInput.cc.animator.speed, currentClimbSpeed, 2f * Time.deltaTime);
|
|
}
|
|
else
|
|
{
|
|
tpInput.cc.animator.speed = Mathf.Lerp(tpInput.cc.animator.speed, 1f, 2f * Time.deltaTime);
|
|
}
|
|
}
|
|
|
|
// increase speed by input and consume stamina
|
|
if (fastClimbInput.GetButton() && tpInput.cc.currentStamina > 0)
|
|
{
|
|
currentClimbSpeed = fastClimbSpeed;
|
|
StaminaConsumption();
|
|
}
|
|
else
|
|
{
|
|
currentClimbSpeed = climbSpeed;
|
|
}
|
|
|
|
// enter ladder behaviour
|
|
var _inEnterLadderAnimation = InEnterLadderAnimationMatchTarget();
|
|
|
|
if (_inEnterLadderAnimation)
|
|
{
|
|
inEnterLadderAnimation = true;
|
|
|
|
tpInput.cc.DisableGravityAndCollision(); // disable gravity & turn collision trigger
|
|
// disable ingame hud
|
|
if (currentLadderAction != null)
|
|
{
|
|
currentLadderAction.OnPlayerExit?.Invoke();
|
|
}
|
|
|
|
float normalizedTime = GetLadderNormalizedTime();
|
|
|
|
if (currentLadderAction.useTriggerRotation && currentLadderAction.matchTarget != null)
|
|
{
|
|
|
|
if (debugMode)
|
|
{
|
|
LogLadderAction("Rotating to target");
|
|
}
|
|
EvaluateToRotation(currentLadderAction.enterRotationCurve, currentLadderAction.matchTarget.transform.rotation, normalizedTime);
|
|
}
|
|
|
|
if (currentLadderAction.matchTarget != null)
|
|
{
|
|
if (transform.parent != currentLadderAction.targetCharacterParent)
|
|
{
|
|
transform.parent = currentLadderAction.targetCharacterParent;
|
|
}
|
|
|
|
if (debugMode)
|
|
{
|
|
LogLadderAction("Match target to enter");
|
|
}
|
|
|
|
EvaluateToPosition(currentLadderAction.enterPositionXZCurve, currentLadderAction.enterPositionYCurve, currentLadderAction.matchTarget.position, normalizedTime);
|
|
}
|
|
}
|
|
|
|
if (!_inEnterLadderAnimation && inEnterLadderAnimation)
|
|
{
|
|
enterLadderStarted = false;
|
|
inEnterLadderAnimation = false;
|
|
}
|
|
TriggerExitLadder();
|
|
|
|
}
|
|
|
|
protected virtual void TriggerExitLadder()
|
|
{
|
|
// exit ladder behaviour
|
|
inExitingLadderAnimation = InExitLadderAnimation();
|
|
|
|
if (inExitingLadderAnimation)
|
|
{
|
|
SetLadderAnimationSpeed(1f);
|
|
|
|
float normalizedTime = GetLadderNormalizedTime();
|
|
|
|
// A quick exit is taken mid-ladder, away from any trigger, so there is no
|
|
// currentLadderAction and no authored match target - the clip's own root motion is
|
|
// the whole movement. Only the trigger-driven exits match to a target.
|
|
if (currentLadderAction != null)
|
|
{
|
|
if (currentLadderAction.exitMatchTarget != null && !IsPlayingLadderState(QuickExitLadderState))
|
|
{
|
|
if (debugMode)
|
|
{
|
|
LogLadderAction("Match target to exit");
|
|
}
|
|
|
|
EvaluateToPosition(currentLadderAction.exitPositionXZCurve, currentLadderAction.exitPositionYCurve, currentLadderAction.exitMatchTarget.position, normalizedTime, true);
|
|
}
|
|
|
|
var newRot = new Vector3(0, GetLadderRootRotation().eulerAngles.y, 0);
|
|
EvaluateToRotation(currentLadderAction.exitRotationCurve, Quaternion.Euler(newRot), normalizedTime);
|
|
}
|
|
|
|
float resetTime = IsPlayingLadderState(QuickExitLadderState)
|
|
? QuickExitResetNormalizedTime
|
|
: ExitResetNormalizedTime;
|
|
|
|
if (normalizedTime >= resetTime)
|
|
{
|
|
// Hand the character back to physics exactly where the clip stops owning it: the
|
|
// LockMovement tag and the row's root-motion Y range both end at this same
|
|
// normalized time. Releasing earlier let gravity fight the clip's root motion for
|
|
// the body - the jitter - and releasing at a fixed wall-clock delay had no
|
|
// relation to where the clip actually was.
|
|
ResetPlayerSettings();
|
|
|
|
// The climb ran kinematic, so heightReached is still pinned to the top of the
|
|
// ladder. The fall that starts here is a new one, not a drop from that height -
|
|
// without this it reads as a long fall and triggers fall damage, LandHigh or the
|
|
// ragdoll.
|
|
tpInput.cc.ResetFallState();
|
|
}
|
|
}
|
|
}
|
|
|
|
protected virtual Vector3 GetLadderRootPosition()
|
|
{
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
if (UsingPlayables)
|
|
{
|
|
return playablesAnimator.rootPosition;
|
|
}
|
|
#endif
|
|
return tpInput.cc.animator.rootPosition;
|
|
}
|
|
|
|
// Equivalent of Animator.rootRotation: the pending pose after this frame's root motion, applied
|
|
// by the script's own write to transform.rotation.
|
|
protected virtual Quaternion GetLadderRootRotation()
|
|
{
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
if (UsingPlayables)
|
|
{
|
|
return playablesAnimator.rootRotation;
|
|
}
|
|
#endif
|
|
return tpInput.cc.animator.rootRotation;
|
|
}
|
|
|
|
protected virtual void EvaluateToPosition(AnimationCurve XZ, AnimationCurve Y, Vector3 targetPosition, float normalizedTime, bool isExit = false)
|
|
{
|
|
Vector3 rootPosition = GetLadderRootPosition();
|
|
|
|
float evaluatedXZ = XZ.Evaluate(normalizedTime);
|
|
float evaluatedY = Y.Evaluate(normalizedTime);
|
|
|
|
// Hold the XZ the root started at, but ONLY for the length of the cross-fade, and only when
|
|
// entering. During that window the state being left is still blended in and still feeding
|
|
// its own root motion - a full-speed run when the player sprints at the ladder - which the
|
|
// lerp below would otherwise carry straight into the wall the ladder is mounted on.
|
|
//
|
|
// The window is wall-clock, not curve-driven, because a curve reading ~0 means two
|
|
// opposite things here: early in EnterLadderBottom it means "the match has not taken over
|
|
// yet" (freeze is right), while in EnterLadderTop and in every exit it means "this stretch
|
|
// belongs to root motion" - those animations travel 2.45m down and off the ladder under
|
|
// their own steam, and freezing pins the character in place. Bounding the hold to the
|
|
// cross-fade covers the contaminated frames without ever reaching that authored motion.
|
|
//
|
|
// Both axes are held, unlike vGenericAction: vTriggerLadderAction has no
|
|
// useLocalX/useLocalZ, so the match always owns X and Z together.
|
|
if (!isExit)
|
|
{
|
|
if (!hasMatchOriginXZ)
|
|
{
|
|
matchOriginXZ = new Vector2(rootPosition.x, rootPosition.z);
|
|
matchOriginTime = Time.time;
|
|
hasMatchOriginXZ = true;
|
|
}
|
|
|
|
if (Time.time - matchOriginTime < EnterAnimationTransitionDuration)
|
|
{
|
|
rootPosition.x = matchOriginXZ.x;
|
|
rootPosition.z = matchOriginXZ.y;
|
|
}
|
|
}
|
|
|
|
if (evaluatedXZ < 1f)
|
|
{
|
|
rootPosition.x = Mathf.Lerp(rootPosition.x, targetPosition.x, evaluatedXZ);
|
|
rootPosition.z = Mathf.Lerp(rootPosition.z, targetPosition.z, evaluatedXZ);
|
|
}
|
|
|
|
if (evaluatedY < 1f)
|
|
{
|
|
rootPosition.y = Mathf.Lerp(rootPosition.y, targetPosition.y, evaluatedY);
|
|
}
|
|
|
|
|
|
transform.position = rootPosition;
|
|
}
|
|
|
|
protected virtual void EvaluateToRotation(AnimationCurve curve, Quaternion targetRotation, float normalizedTime)
|
|
{
|
|
Quaternion rootRotation = GetLadderRootRotation();
|
|
float evaluatedCurve = curve.Evaluate(normalizedTime);
|
|
if (evaluatedCurve < 1)
|
|
{
|
|
rootRotation = Quaternion.Lerp(rootRotation, targetRotation, evaluatedCurve);
|
|
}
|
|
|
|
transform.rotation = rootRotation;
|
|
}
|
|
|
|
protected virtual void StaminaConsumption()
|
|
{
|
|
if (tpInput.cc.currentStamina <= 0)
|
|
{
|
|
return;
|
|
}
|
|
|
|
tpInput.cc.ReduceStamina(fastClimbStamina, true); // call the ReduceStamina method from the player
|
|
tpInput.cc.currentStaminaRecoveryDelay = StaminaRecoveryDelay;
|
|
}
|
|
|
|
protected virtual void AddLadderTrigger(vTriggerLadderAction _ladderAction)
|
|
{
|
|
if (!_ladderAction)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (targetLadderAction != _ladderAction)
|
|
{
|
|
targetLadderAction = _ladderAction;
|
|
LogLadderAction("Trigger stay");
|
|
}
|
|
|
|
if (!actionTriggers.Contains(targetLadderAction))
|
|
{
|
|
actionTriggers.Add(targetLadderAction);
|
|
targetLadderAction.OnPlayerEnter?.Invoke();
|
|
}
|
|
}
|
|
|
|
protected virtual void RemoveLadderTrigger(vTriggerLadderAction _ladderAction)
|
|
{
|
|
if (_ladderAction == targetLadderAction)
|
|
{
|
|
targetLadderAction = null;
|
|
}
|
|
|
|
if (actionTriggers.Contains(_ladderAction))
|
|
{
|
|
actionTriggers.Remove(_ladderAction);
|
|
_ladderAction.OnPlayerExit?.Invoke();
|
|
}
|
|
}
|
|
|
|
protected virtual void CheckForTriggerAction(Collider other)
|
|
{
|
|
var _ladderAction = other.GetComponent<vTriggerLadderAction>();
|
|
if (!_ladderAction)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var dist = Vector3.Distance(transform.forward, _ladderAction.transform.forward);
|
|
|
|
if (isUsingLadder)
|
|
{
|
|
if (targetLadderAction != _ladderAction)
|
|
{
|
|
targetLadderAction = _ladderAction;
|
|
if (!actionTriggers.Contains(targetLadderAction))
|
|
{
|
|
actionTriggers.Add(targetLadderAction);
|
|
}
|
|
}
|
|
}
|
|
else if (!_ladderAction.activeFromForward || dist <= TriggerForwardDistanceThreshold)
|
|
{
|
|
AddLadderTrigger(_ladderAction);
|
|
OnEnterTriggerLadder?.Invoke();
|
|
}
|
|
else
|
|
{
|
|
RemoveLadderTrigger(_ladderAction);
|
|
}
|
|
}
|
|
|
|
public virtual void ResetPlayerSettings()
|
|
{
|
|
if (debugMode)
|
|
{
|
|
Debug.Log("Reset Player Settings", this);
|
|
}
|
|
|
|
speed = 0f;
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
ladderInputSmooth = 0f;
|
|
#endif
|
|
ladderSide = vLadderSide.Left;
|
|
targetLadderAction = null;
|
|
isUsingLadder = false;
|
|
OnExitLadder?.Invoke();
|
|
triggerExitOnce = false;
|
|
triggerEnterOnce = false;
|
|
inEnterLadderAnimation = false;
|
|
enterLadderStarted = false;
|
|
hasMatchOriginXZ = false;
|
|
tpInput.cc.animator.SetInteger(vAnimatorParameters.ActionState, 0);
|
|
tpInput.cc.EnableGravityAndCollision();
|
|
tpInput.SetLockAllInput(false);
|
|
tpInput.cc.StopCharacter();
|
|
tpInput.cc.disableAnimations = false;
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
RestorePreviousPDMAfterGroundCheck();
|
|
#endif
|
|
if (transform.parent != null)
|
|
{
|
|
transform.parent = null;
|
|
}
|
|
}
|
|
|
|
#if INVECTOR_PLAYABLESANIMATOR
|
|
/// <summary>Temporarily assigns the optional ladder-only PDM and remembers the current assignment.</summary>
|
|
protected virtual void SwitchToLadderPDM()
|
|
{
|
|
if (!CanUseLadderPDM)
|
|
{
|
|
return;
|
|
}
|
|
|
|
if (restorePreviousPDMCoroutine != null)
|
|
{
|
|
StopCoroutine(restorePreviousPDMCoroutine);
|
|
restorePreviousPDMCoroutine = null;
|
|
}
|
|
|
|
if (ladderPDMIsActive)
|
|
{
|
|
return;
|
|
}
|
|
|
|
previousPDM = playablesAnimator.ActivePDM;
|
|
playablesAnimator.SetPDM(ladderPDM);
|
|
ladderPDMIsActive = true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Delays the base-PDM handoff until the controller has run its next fixed ground check after
|
|
/// gravity and collision are restored. A quick exit in open air therefore still selects Falling.
|
|
/// </summary>
|
|
protected virtual void RestorePreviousPDMAfterGroundCheck()
|
|
{
|
|
if (!ladderPDMIsActive || restorePreviousPDMCoroutine != null)
|
|
{
|
|
return;
|
|
}
|
|
|
|
restorePreviousPDMCoroutine = StartCoroutine(RestorePreviousPDMOnNextFixedUpdate());
|
|
}
|
|
|
|
protected virtual IEnumerator RestorePreviousPDMOnNextFixedUpdate()
|
|
{
|
|
yield return new WaitForFixedUpdate();
|
|
restorePreviousPDMCoroutine = null;
|
|
RestorePreviousPDM();
|
|
}
|
|
|
|
/// <summary>Restores the PDM that was active before this ladder interaction started.</summary>
|
|
protected virtual void RestorePreviousPDM()
|
|
{
|
|
if (!ladderPDMIsActive)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// Do not overwrite a PDM selected by another system while the ladder was active.
|
|
if (UsingPlayables && playablesAnimator.ActivePDM == ladderPDM)
|
|
{
|
|
playablesAnimator.SetPDM(previousPDM);
|
|
}
|
|
|
|
previousPDM = null;
|
|
ladderPDMIsActive = false;
|
|
}
|
|
#endif
|
|
|
|
public override void OnActionStay(Collider other)
|
|
{
|
|
if (other.gameObject.CompareTag(actionTag))
|
|
{
|
|
CheckForTriggerAction(other);
|
|
}
|
|
}
|
|
|
|
public override void OnActionExit(Collider other)
|
|
{
|
|
if (other.gameObject.CompareTag(actionTag))
|
|
{
|
|
var _ladderAction = other.GetComponent<vTriggerLadderAction>();
|
|
if (!_ladderAction)
|
|
{
|
|
return;
|
|
}
|
|
RemoveLadderTrigger(_ladderAction);
|
|
if (debugMode)
|
|
{
|
|
var parentName = other.transform.parent ? other.transform.parent.name : "<no parent>";
|
|
Debug.Log("Trigger Exit: " + other.name + "_" + parentName, other);
|
|
}
|
|
|
|
OnExitTriggerLadder?.Invoke();
|
|
}
|
|
}
|
|
|
|
private void LogLadderAction(string message)
|
|
{
|
|
if (!debugMode)
|
|
{
|
|
return;
|
|
}
|
|
|
|
var ladderAction = currentLadderAction ? currentLadderAction : targetLadderAction;
|
|
var actionName = ladderAction ? ladderAction.name : "<none>";
|
|
var parentName = ladderAction && ladderAction.transform.parent
|
|
? ladderAction.transform.parent.name
|
|
: "<no parent>";
|
|
|
|
Debug.Log(message + ": " + actionName + "_" + parentName, this);
|
|
}
|
|
}
|
|
}
|