Post by redsam121 on Feb 23, 2023 10:24:38 GMT
I've been working on a new AI action for the AI system so that AI characters can use ladders. My code is based on the original from Basic locomotion that has been modified for the vControlAI.
It works by using a custom off mesh link component to trigger the ladder input. It can be added to the character without modifying any of the original scripts.
So far, my prototype is working, but with a few problems. The first problem is that the character stays in place when triggering enter or exit animations, even though I apply root motion. The second is there's a delay when the character starts climbing, because the navmesh agent is slowly moving up first.
If someone can please help fix these problems, it would be much appreciated.
Code:
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
using Invector.vCharacterController.AI;
using UnityEngine.Windows;
namespace Invector.vCharacterController.vActions
{
[vClassHeader("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>();
if (controller)
{
}
}
private void Update()
{
ExitLadderInput();
TriggerExitLadder();
UsingLadder();
if (controller.navMeshAgent.enabled && isOnLadderLink && !controller.isJumping && controller.isGrounded && !isUsingLadder && targetLadderAction)
{
TriggerEnterLadder();
}
}
public bool isOnLadderLink
{
get
{
var linkData = controller.navMeshAgent.currentOffMeshLinkData.offMeshLink;
if (linkData != null)
{
if (linkData.area == UnityEngine.AI.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 && !controller.animator.GetCurrentAnimatorStateInfo(0).IsName("QuickExitLadder"))
{
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);
}
}
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.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();
}
}
}
}