Post by redsam121 on Feb 26, 2023 6:42:41 GMT
Working Climb Ladder System for FSM AI
I have developed a working Climb system for the fsm behavior that does not require any alterations to the behaviour. Very useful for AI waypoints, move to positions and following companions. It uses Off Mesh Link in order to navigate the characters, as well as an auto input to use the ladder.
It took a while to get working, but I managed to get solve all the errors I've been encountering. It's not 100% perfect (with positioning), but will work any ladder.
How to install:
- Create a new script titled "vAILadderAction"
- Copy paste script from below.
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using Invector.vCharacterController.AI;
using UnityEngine.AI;
namespace Invector.vCharacterController.vActions
{
[vClassHeader("AI Ladder Action", iconName = "ladderIcon")]
public class vAILadderAction : vActionListener
{
#region public variables
[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;
[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 currentClimbSpeed;
[vReadOnly(false)]
[SerializeField]
public bool isUsingLadder;
[vReadOnly(false)]
[SerializeField]
protected bool enterLadderStarted;
[vReadOnly(false)]
[SerializeField]
protected bool inEnterLadderAnimation;
[vReadOnly(false)]
[SerializeField]
protected bool inExitingLadderAnimation;
[vReadOnly(false)]
[SerializeField]
protected bool triggerEnterOnce;
[vReadOnly(false)]
[SerializeField]
protected bool triggerExitOnce;
#endregion
protected vControlAI controller;
private bool _inEnterLadderAnimation;
float speed;
[vReadOnly(false)]
[SerializeField]
public vTriggerLadderAction enteredLadder;
protected override void Start()
{
base.Start();
controller = GetComponent<vControlAI>();
}
private void LateUpdate()
{
HandleNavMesh();
ExitLadderInput();
UsingLadder();
TriggerExitLadder();
if (controller.navMeshAgent.enabled && isOnLadderLink && !controller.isJumping && controller.isGrounded && !isUsingLadder && targetLadderAction)
{
TriggerEnterLadder();
}
}
private void HandleNavMesh()
{
controller.onladder = isUsingLadder || inEnterLadderAnimation || inExitingLadderAnimation;
controller.navMeshAgent.enabled = !isUsingLadder && !inEnterLadderAnimation && !inExitingLadderAnimation;
}
public bool isOnLadderLink
{
get
{
var linkData = controller.navMeshAgent.currentOffMeshLinkData.offMeshLink;
if (linkData != null)
{
if (linkData.area == NavMesh.GetAreaFromName("Ladder"))
{
return true;
}
}
return false;
}
}
public virtual void TriggerEnterLadder()
{
if (debugMode)
{
Debug.Log("Enter Ladder");
}
OnExitTriggerLadder.Invoke();
if (targetLadderAction.targetCharacterParent)
{
transform.parent = targetLadderAction.targetCharacterParent;
}
controller.isCrouching = false;
OnEnterLadder.Invoke();
triggerEnterOnce = true;
enterLadderStarted = true;
controller.animator.SetInteger(vAnimatorParameters.ActionState, 1); // set actionState 1 to avoid falling transitions
targetLadderAction.OnDoAction.Invoke();
currentLadderAction = targetLadderAction;
if (!string.IsNullOrEmpty(currentLadderAction.playAnimation))
{
if (debugMode)
{
Debug.Log("TriggerAnimation " + currentLadderAction.name + "_" + currentLadderAction.transform.parent.gameObject.name);
}
controller.animator.CrossFadeInFixedTime(currentLadderAction.playAnimation, 0.25f); // trigger the action animation clip
isUsingLadder = true;
controller.DisableAIController();
controller.enabled = true;
enteredLadder = currentLadderAction;
}
}
protected virtual void ExitLadderInput()
{
if (!isUsingLadder)
{
return;
}
if (controller.animator.GetCurrentAnimatorStateInfo(0).IsName("EnterLadderTop") || controller.animator.GetCurrentAnimatorStateInfo(0).IsName("EnterLadderBottom"))
{
return;
}
if (targetLadderAction == null)
return;
currentLadderAction = targetLadderAction;
var animationClip = targetLadderAction.exitAnimation;
if (animationClip == "ExitLadderBottom")
{
// exit ladder when reach the bottom by pressing the cancelInput or pressing down at
if (!triggerExitOnce && speed <= -0.05f && targetLadderAction != enteredLadder)
{
if (debugMode)
{
Debug.Log("Exit Bottom..." + currentLadderAction.name + "_" + currentLadderAction.transform.parent.gameObject.name);
}
triggerExitOnce = true;
controller.animator.CrossFadeInFixedTime(targetLadderAction.exitAnimation, 0.1f); // trigger the animation clip
}
}
else if (animationClip == "ExitLadderTop" && controller.IsAnimatorTag("ClimbLadder")) // exit the ladder from the top
{
if (!triggerExitOnce && !controller.animator.IsInTransition(0) && targetLadderAction != enteredLadder) // trigger the exit animation by pressing up
{
if (debugMode)
{
Debug.Log("Exit Top..." + currentLadderAction.name + "_" + currentLadderAction.transform.parent.gameObject.name);
}
triggerExitOnce = true;
controller.animator.CrossFadeInFixedTime(targetLadderAction.exitAnimation, 0.1f); // trigger the animation clip
}
}
}
protected virtual void UsingLadder()
{
if (!isUsingLadder)
{
return;
}
speed = climbSpeed;
if (targetLadderAction.playAnimation == "EnterLadderTop")
{
speed = -climbSpeed;
}
controller.animator.SetFloat(vAnimatorParameters.InputVertical, speed, 0.1f, Time.deltaTime);
//controller.animator.speed = Mathf.Lerp(controller.animator.speed, climbSpeed, 2f * Time.deltaTime);
// enter ladder behaviour
_inEnterLadderAnimation = controller.animator.GetCurrentAnimatorStateInfo(0).IsName("EnterLadderTop")
|| controller.animator.GetCurrentAnimatorStateInfo(0).IsName("EnterLadderBottom") && !controller.animator.IsInTransition(0);
if (_inEnterLadderAnimation)
{
this.inEnterLadderAnimation = true;
DisableGravityAndCollision(); // disable gravity & turn collision trigger
if (currentLadderAction != null)
{
currentLadderAction.OnPlayerExit.Invoke();
}
if (currentLadderAction.useTriggerRotation)
{
if (debugMode)
{
Debug.Log("Rotating to target..." + currentLadderAction.name + "_" + currentLadderAction.transform.parent.gameObject.name);
}
EvaluateToRotation(currentLadderAction.enterRotationCurve, currentLadderAction.matchTarget.transform.rotation, controller.animator.GetCurrentAnimatorStateInfo(0).normalizedTime);
}
if (currentLadderAction.matchTarget != null)
{
if (transform.parent != currentLadderAction.targetCharacterParent)
{
transform.parent = currentLadderAction.targetCharacterParent;
}
if (debugMode)
{
Debug.Log("Match Target to Enter..." + currentLadderAction.name + "_" + currentLadderAction.transform.parent.gameObject.name);
}
EvaluateToPosition(currentLadderAction.enterPositionXZCurve, currentLadderAction.enterPositionYCurve, currentLadderAction.matchTarget.position, controller.animator.GetCurrentAnimatorStateInfo(0).normalizedTime);
}
}
if (!_inEnterLadderAnimation && inEnterLadderAnimation)
{
enterLadderStarted = false;
inEnterLadderAnimation = false;
}
//TriggerExitLadder();
}
protected virtual void TriggerExitLadder()
{
// exit ladder behaviour
inExitingLadderAnimation = controller.animator.GetCurrentAnimatorStateInfo(0).IsName("ExitLadderTop") || controller.animator.GetCurrentAnimatorStateInfo(0).IsName("ExitLadderBottom");
if (inExitingLadderAnimation)
{
controller.animator.speed = 1;
if (currentLadderAction.exitMatchTarget != null)
{
if (debugMode)
{
Debug.Log("Match Target to exit..." + currentLadderAction.name + "_" + currentLadderAction.transform.parent.gameObject.name);
}
EvaluateToPosition(currentLadderAction.exitPositionXZCurve, currentLadderAction.exitPositionYCurve, currentLadderAction.exitMatchTarget.position, controller.animator.GetCurrentAnimatorStateInfo(0).normalizedTime);
}
var newRot = new Vector3(0, controller.animator.rootRotation.eulerAngles.y, 0);
EvaluateToRotation(currentLadderAction.exitRotationCurve, Quaternion.Euler(newRot), controller.animator.GetCurrentAnimatorStateInfo(0).normalizedTime);
if (controller.animator.GetCurrentAnimatorStateInfo(0).normalizedTime >= 0.8f)
{
// after playing the animation we reset some values
ResetPlayerSettings();
}
}
}
protected virtual void EvaluateToPosition(AnimationCurve XZ, AnimationCurve Y, Vector3 targetPosition, float normalizedTime)
{
Vector3 rootPosition = controller.animator.rootPosition;
float evaluatedXZ = XZ.Evaluate(normalizedTime);
float evaluatedY = Y.Evaluate(normalizedTime);
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 = controller.animator.rootRotation;
float evaluatedCurve = curve.Evaluate(normalizedTime);
if (evaluatedCurve < 1)
{
rootRotation = Quaternion.Lerp(rootRotation, targetRotation, evaluatedCurve);
}
transform.rotation = rootRotation;
}
protected virtual void AddLadderTrigger(vTriggerLadderAction _ladderAction)
{
if (targetLadderAction != _ladderAction)
{
targetLadderAction = _ladderAction;
if (debugMode)
{
Debug.Log("TriggerStay " + targetLadderAction.name + "_" + targetLadderAction.transform.parent.gameObject.name);
}
}
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)
{
// assign the component - it will be null when he exit the trigger area
var _ladderAction = other.GetComponent<vTriggerLadderAction>();
if (!_ladderAction)
{
return;
}
// check the maxAngle too see if the character can do the action
var dist = Vector3.Distance(transform.forward, _ladderAction.transform.forward);
if (isUsingLadder && _ladderAction != null)
{
if (targetLadderAction != _ladderAction)
{
targetLadderAction = _ladderAction;
if (!actionTriggers.Contains(targetLadderAction))
{
actionTriggers.Add(targetLadderAction);
}
}
}
else if ((_ladderAction.activeFromForward == false || dist <= 0.8f) && !isUsingLadder)
{
AddLadderTrigger(_ladderAction);
OnEnterTriggerLadder.Invoke();
}
else
{
RemoveLadderTrigger(_ladderAction);
}
}
void OnDrawGizmosSelected()
{
// Draw a yellow sphere at the transform's position
Gizmos.color = Color.yellow;
Gizmos.DrawWireSphere(GetComponent<Animator>().rootPosition, 0.1f);
}
public virtual void ResetPlayerSettings()
{
if (debugMode)
{
Debug.Log("Reset Player Settings");
}
targetLadderAction = null;
isUsingLadder = false;
OnExitLadder.Invoke();
triggerExitOnce = false;
triggerEnterOnce = false;
inEnterLadderAnimation = false;
enterLadderStarted = false;
controller.navMeshAgent.enabled = true;
controller.animator.SetInteger(vAnimatorParameters.ActionState, 0);
EnableGravityAndCollision();
controller.Stop();
if (transform.parent != null)
{
transform.parent = null;
}
}
public virtual void DisableGravityAndCollision()
{
controller.animator.SetFloat("InputHorizontal", 0f);
controller.animator.SetFloat("InputVertical", 0f);
controller.animator.SetFloat("VerticalVelocity", 0f);
//Disable gravity and collision
controller._rigidbody.useGravity = false;
controller._rigidbody.isKinematic = true;
controller._capsuleCollider.isTrigger = true;
//Reset rigidbody velocity
controller._rigidbody.velocity = Vector3.zero;
}
public virtual void EnableGravityAndCollision()
{
// Enable collision and gravity
controller._capsuleCollider.isTrigger = false;
controller._rigidbody.useGravity = true;
controller._rigidbody.isKinematic = false;
controller.enabled = true;
}
public void OnTriggerStay(Collider other)
{
if (other.gameObject.CompareTag(actionTag) && !enterLadderStarted)
{
CheckForTriggerAction(other);
}
}
private void OnTriggerExit(Collider other)
{
if (other.gameObject.CompareTag(actionTag) && !isUsingLadder)
{
var _ladderAction = other.GetComponent<vTriggerLadderAction>();
if (!_ladderAction)
{
return;
}
RemoveLadderTrigger(_ladderAction);
if (debugMode)
{
Debug.Log("TriggerExit " + other.name + "_" + other.transform.parent.gameObject.name);
}
OnExitTriggerLadder.Invoke();
}
}
}
}
- At line 98, change to:
public bool lockMovement, lockRotation, stopMove, isJumping, doingCustomAction, onladder;
- At line 629, add:
if(!onladder)
AnimatorLocomotion();
- At line 912, change to:
if (customAction && !onladder || input.magnitude < 0.1f )
How to use in-game:
- Create a new Navigation area called "Ladder".
- Simply add an offMesh link near the ladder with the new navigation area.
You are free to use this code however you please.