using System.Collections.Generic; using UnityEngine; using Lightbug.CharacterControllerPro.Core; using Lightbug.CharacterControllerPro.Implementation; using Lightbug.Utilities; using static UnityEngine.EventSystems.PointerEventData; namespace Lightbug.CharacterControllerPro.Demo { [AddComponentMenu("Character Controller Pro/Demo/Character/States/Ladder Climbing")] public class LadderClimbing : CharacterState { [Header("Offset")] [SerializeField] protected bool useIKOffsetValues = false; [Condition("useIKOffsetValues", ConditionAttribute.ConditionType.IsTrue)] [SerializeField] protected Vector3 rightFootOffset = Vector3.zero; [Condition("useIKOffsetValues", ConditionAttribute.ConditionType.IsTrue)] [SerializeField] protected Vector3 leftFootOffset = Vector3.zero; [Condition("useIKOffsetValues", ConditionAttribute.ConditionType.IsTrue)] [SerializeField] protected Vector3 rightHandOffset = Vector3.zero; [Condition("useIKOffsetValues", ConditionAttribute.ConditionType.IsTrue)] [SerializeField] protected Vector3 leftHandOffset = Vector3.zero; [Header("Activation")] [SerializeField] protected bool useInteractAction = true; [SerializeField] protected bool filterByAngle = true; [SerializeField] protected float maxAngle = 70f; [Header("Animation")] [SerializeField] protected string bottomDownParameter = "BottomDown"; [SerializeField] protected string bottomUpParameter = "BottomUp"; [SerializeField] protected string topDownParameter = "TopDown"; [SerializeField] protected string topUpParameter = "TopUp"; [SerializeField] protected string upParameter = "Up"; [SerializeField] protected string downParameter = "Down"; [Space(10f)] [SerializeField] protected string entryStateName = "Entry"; [SerializeField] protected string exitStateName = "Exit"; [SerializeField] protected string idleStateName = "Idle"; // ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── // ───────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────── protected Dictionary ladders = new Dictionary(); public enum LadderClimbState { Entering, Exiting, Idling, Climbing, } protected LadderClimbState state; protected Ladder currentLadder = null; protected Vector3 targetPosition = Vector3.zero; protected int currentClimbingAnimation = 0; protected bool forceExit = false; protected AnimatorStateInfo animatorStateInfo; protected bool isBottom = false; public override void CheckExitTransition() { if (forceExit) CharacterStateController.EnqueueTransition(); } public override bool CheckEnterTransition(CharacterState fromState) { if (!CharacterActor.IsGrounded) return false; if (useInteractAction && !CharacterActions.interact.Started) return false; for (int i = 0; i < CharacterActor.Triggers.Count; i++) { Trigger trigger = CharacterActor.Triggers[i]; Ladder ladder = ladders.GetOrRegisterValue(trigger.transform); if (ladder != null) { if (!useInteractAction && CharacterActor.WasGrounded && !trigger.firstContact) { return false; } currentLadder = ladder; // Check if the character is closer to the top of the ladder. float distanceToTop = Vector3.Distance(CharacterActor.Position, currentLadder.TopReference.position); float distanceToBottom = Vector3.Distance(CharacterActor.Position, currentLadder.BottomReference.position); isBottom = distanceToBottom < distanceToTop; if (filterByAngle) { Vector3 ladderToCharacter = CharacterActor.Position - currentLadder.transform.position; ladderToCharacter = Vector3.ProjectOnPlane(ladderToCharacter, currentLadder.transform.up); float angle = Vector3.Angle(currentLadder.FacingDirectionVector, ladderToCharacter); if (isBottom) { if (angle >= maxAngle) return true; else continue; } else { if (angle <= maxAngle) return true; else continue; } } else { return true; } } } return false; } public override void EnterBehaviour(float dt, CharacterState fromState) { CharacterActor.Velocity = Vector3.zero; CharacterActor.IsKinematic = true; CharacterActor.alwaysNotGrounded = true; currentClimbingAnimation = isBottom ? 0 : currentLadder.ClimbingAnimations; targetPosition = isBottom ? currentLadder.BottomReference.position : currentLadder.TopReference.position; CharacterActor.SetYaw(currentLadder.FacingDirectionVector); CharacterActor.Position = targetPosition; // Root motion CharacterActor.SetUpRootMotion( true, PhysicsActor.RootMotionVelocityType.SetVelocity, false ); CharacterActor.Animator.SetTrigger(isBottom ? bottomUpParameter : topDownParameter); state = LadderClimbState.Entering; } public override void ExitBehaviour(float dt, CharacterState toState) { forceExit = false; CharacterActor.IsKinematic = false; CharacterActor.alwaysNotGrounded = false; currentLadder = null; CharacterStateController.ResetIKWeights(); CharacterActor.Velocity = Vector3.zero; CharacterActor.ForceGrounded(); } protected override void Awake() { base.Awake(); #if UNITY_2023_1_OR_NEWER Ladder[] laddersArray = FindObjectsByType(FindObjectsSortMode.None); #else Ladder[] laddersArray = FindObjectsOfType(); #endif for (int i = 0; i < laddersArray.Length; i++) ladders.Add(laddersArray[i].transform, laddersArray[i]); } protected override void Start() { base.Start(); if (CharacterActor.Animator == null) { Debug.Log("The LadderClimbing state needs the character to have a reference to an Animator component. Destroying this state..."); Destroy(this); } } public override void UpdateBehaviour(float dt) { animatorStateInfo = CharacterActor.Animator.GetCurrentAnimatorStateInfo(0); switch (state) { case LadderClimbState.Entering: // The LadderClimbing state has just begun, Make sure to wait for the "Idle" animation state. // Important: Note that the animation clip from this state (Animator) is the same as the locomotion idle clip. if (animatorStateInfo.IsName(idleStateName)) state = LadderClimbState.Idling; break; case LadderClimbState.Idling: // This state is responsible for handling inputs and setting animation triggers. if (CharacterActions.interact.Started) { if (useInteractAction) forceExit = true; } else { if (CharacterActions.movement.Up) { if (currentClimbingAnimation == currentLadder.ClimbingAnimations) { CharacterActor.Animator.SetTrigger(topUpParameter); state = LadderClimbState.Exiting; } else { CharacterActor.Animator.SetTrigger(upParameter); currentClimbingAnimation++; state = LadderClimbState.Climbing; } } else if (CharacterActions.movement.Down) { if (currentClimbingAnimation == 0) { CharacterActor.Animator.SetTrigger(bottomDownParameter); state = LadderClimbState.Exiting; } else { CharacterActor.Animator.SetTrigger(downParameter); currentClimbingAnimation--; state = LadderClimbState.Climbing; } } } break; case LadderClimbState.Climbing: // Do nothing and wait for the "Idle" animation state if (animatorStateInfo.IsName(idleStateName)) state = LadderClimbState.Idling; break; case LadderClimbState.Exiting: // Do nothing and wait for the "Exit" animation state // Important: Note that the animation clip from this state (Animator) is the same as the locomotion idle clip. if (animatorStateInfo.IsName(exitStateName)) { forceExit = true; CharacterActor.ForceGrounded(); } break; } } public override void UpdateIK(int layerIndex) { if (!useIKOffsetValues) return; UpdateIKElement(AvatarIKGoal.LeftFoot, leftFootOffset); UpdateIKElement(AvatarIKGoal.RightFoot, rightFootOffset); UpdateIKElement(AvatarIKGoal.LeftHand, leftHandOffset); UpdateIKElement(AvatarIKGoal.RightHand, rightHandOffset); } void UpdateIKElement(AvatarIKGoal avatarIKGoal, Vector3 offset) { // Get the original (weight = 0) ik position. CharacterActor.Animator.SetIKPositionWeight(avatarIKGoal, 0f); Vector3 originalRightFootPosition = CharacterActor.Animator.GetIKPosition(avatarIKGoal); // Affect the original ik position with the offset. CharacterActor.Animator.SetIKPositionWeight(avatarIKGoal, 1f); CharacterActor.Animator.SetIKPosition(avatarIKGoal, originalRightFootPosition + offset); } } }