1
This commit is contained in:
@@ -0,0 +1,376 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEngine;
|
||||
using Lightbug.CharacterControllerPro.Core;
|
||||
using Lightbug.Utilities;
|
||||
using Lightbug.CharacterControllerPro.Implementation;
|
||||
|
||||
namespace Lightbug.CharacterControllerPro.Demo
|
||||
{
|
||||
|
||||
[AddComponentMenu("Character Controller Pro/Demo/Character/States/Ledge Hanging")]
|
||||
public class LedgeHanging : CharacterState
|
||||
{
|
||||
|
||||
[Header("Filter")]
|
||||
|
||||
[SerializeField]
|
||||
protected LayerMask layerMask = 0;
|
||||
|
||||
[SerializeField]
|
||||
protected bool filterByTag = false;
|
||||
|
||||
[SerializeField]
|
||||
protected string tagName = "Untagged";
|
||||
|
||||
[SerializeField]
|
||||
protected bool detectRigidbodies = false;
|
||||
|
||||
[Header("Detection")]
|
||||
|
||||
[SerializeField]
|
||||
protected bool groundedDetection = false;
|
||||
|
||||
[Tooltip("How far the hands are from the character along the forward direction.")]
|
||||
[Min(0f)]
|
||||
[SerializeField]
|
||||
protected float forwardDetectionOffset = 0.5f;
|
||||
|
||||
[Tooltip("How far the hands are from the character along the up direction.")]
|
||||
[Min(0.05f)]
|
||||
[SerializeField]
|
||||
protected float upwardsDetectionOffset = 1.8f;
|
||||
|
||||
[Min(0.05f)]
|
||||
[SerializeField]
|
||||
protected float separationBetweenHands = 1f;
|
||||
|
||||
[Tooltip("The distance used by the raycast methods.")]
|
||||
[Min(0.05f)]
|
||||
[SerializeField]
|
||||
protected float ledgeDetectionDistance = 0.05f;
|
||||
|
||||
[Header("Offset")]
|
||||
|
||||
[SerializeField]
|
||||
protected float verticalOffset = 0f;
|
||||
|
||||
[SerializeField]
|
||||
protected float forwardOffset = 0f;
|
||||
|
||||
[Header("Movement")]
|
||||
|
||||
public float ledgeJumpVelocity = 10f;
|
||||
|
||||
[SerializeField]
|
||||
protected bool autoClimbUp = true;
|
||||
|
||||
[Tooltip("If the previous state (\"fromState\") is contained in this list the autoClimbUp flag will be triggered.")]
|
||||
[SerializeField]
|
||||
protected CharacterState[] forceAutoClimbUpStates = null;
|
||||
|
||||
[Header("Animation")]
|
||||
|
||||
[SerializeField]
|
||||
protected string topUpParameter = "TopUp";
|
||||
|
||||
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
|
||||
protected const float MaxLedgeVerticalAngle = 50f;
|
||||
|
||||
|
||||
public enum LedgeHangingState
|
||||
{
|
||||
Idle,
|
||||
TopUp
|
||||
}
|
||||
|
||||
protected LedgeHangingState state;
|
||||
|
||||
|
||||
protected bool forceExit = false;
|
||||
protected bool forceAutoClimbUp = false;
|
||||
|
||||
|
||||
protected override void Awake()
|
||||
{
|
||||
base.Awake();
|
||||
|
||||
}
|
||||
|
||||
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 CheckExitTransition()
|
||||
{
|
||||
if (forceExit)
|
||||
CharacterStateController.EnqueueTransition<NormalMovement>();
|
||||
|
||||
}
|
||||
|
||||
HitInfo leftHitInfo = new HitInfo();
|
||||
HitInfo rightHitInfo = new HitInfo();
|
||||
|
||||
|
||||
public override bool CheckEnterTransition(CharacterState fromState)
|
||||
{
|
||||
if (!groundedDetection && CharacterActor.IsAscending)
|
||||
return false;
|
||||
|
||||
if (!groundedDetection && CharacterActor.IsGrounded)
|
||||
return false;
|
||||
|
||||
if (!IsValidLedge(CharacterActor.Position))
|
||||
return false;
|
||||
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
Vector3 initialPosition;
|
||||
|
||||
public override void EnterBehaviour(float dt, CharacterState fromState)
|
||||
{
|
||||
forceExit = false;
|
||||
initialPosition = CharacterActor.Position;
|
||||
CharacterActor.alwaysNotGrounded = true;
|
||||
CharacterActor.Velocity = Vector3.zero;
|
||||
CharacterActor.IsKinematic = true;
|
||||
|
||||
// Set the size as the default one (CharacterBody component)
|
||||
CharacterActor.SetSize(CharacterActor.DefaultBodySize, CharacterActor.SizeReferenceType.Top);
|
||||
|
||||
// Look towards the wall
|
||||
CharacterActor.SetYaw(Vector3.ProjectOnPlane(-CharacterActor.WallContact.normal, CharacterActor.Up));
|
||||
|
||||
Vector3 referencePosition = 0.5f * (leftHitInfo.point + rightHitInfo.point);
|
||||
Vector3 headToReference = referencePosition - CharacterActor.Top;
|
||||
Vector3 correction = Vector3.Project(headToReference, CharacterActor.Up) +
|
||||
verticalOffset * CharacterActor.Up +
|
||||
forwardOffset * CharacterActor.Forward;
|
||||
|
||||
CharacterActor.Position = CharacterActor.Position + correction;
|
||||
|
||||
state = LedgeHangingState.Idle;
|
||||
|
||||
// Determine if the character should skip the "hanging" state and go directly to the "climbing" state.
|
||||
for (int i = 0; i < forceAutoClimbUpStates.Length; i++)
|
||||
{
|
||||
CharacterState state = forceAutoClimbUpStates[i];
|
||||
if (fromState == state)
|
||||
{
|
||||
forceAutoClimbUp = true;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public override void ExitBehaviour(float dt, CharacterState toState)
|
||||
{
|
||||
CharacterActor.IsKinematic = false;
|
||||
CharacterActor.alwaysNotGrounded = false;
|
||||
forceAutoClimbUp = false;
|
||||
|
||||
if (ledgeJumpFlag)
|
||||
{
|
||||
ledgeJumpFlag = false;
|
||||
|
||||
CharacterActor.Position = initialPosition;
|
||||
CharacterActor.Velocity = CharacterActor.Up * ledgeJumpVelocity;
|
||||
}
|
||||
else
|
||||
{
|
||||
CharacterActor.Velocity = Vector3.zero;
|
||||
}
|
||||
}
|
||||
|
||||
bool CheckValidClimb()
|
||||
{
|
||||
HitInfoFilter ledgeHitInfoFilter = new HitInfoFilter(layerMask, false, true);
|
||||
bool overlap = CharacterActor.CharacterCollisions.CheckOverlap(
|
||||
(leftHitInfo.point + rightHitInfo.point) / 2f,
|
||||
CharacterActor.StepOffset,
|
||||
in ledgeHitInfoFilter
|
||||
);
|
||||
|
||||
return !overlap;
|
||||
}
|
||||
|
||||
bool ledgeJumpFlag = false;
|
||||
|
||||
public override void UpdateBehaviour(float dt)
|
||||
{
|
||||
|
||||
switch (state)
|
||||
{
|
||||
|
||||
case LedgeHangingState.Idle:
|
||||
|
||||
if (CharacterActions.jump.Started)
|
||||
{
|
||||
forceExit = true;
|
||||
ledgeJumpFlag = true;
|
||||
}
|
||||
else if (CharacterActions.movement.Up || autoClimbUp || forceAutoClimbUp)
|
||||
{
|
||||
if (CheckValidClimb())
|
||||
{
|
||||
state = LedgeHangingState.TopUp;
|
||||
|
||||
// Root motion
|
||||
CharacterActor.SetUpRootMotion(
|
||||
true,
|
||||
PhysicsActor.RootMotionVelocityType.SetVelocity,
|
||||
false
|
||||
);
|
||||
|
||||
|
||||
CharacterActor.Animator.SetTrigger(topUpParameter);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
else if (CharacterActions.movement.Down)
|
||||
{
|
||||
forceExit = true;
|
||||
}
|
||||
|
||||
break;
|
||||
|
||||
case LedgeHangingState.TopUp:
|
||||
|
||||
if (CharacterActor.Animator.GetCurrentAnimatorStateInfo(0).IsName("Exit"))
|
||||
{
|
||||
forceExit = true;
|
||||
CharacterActor.ForceGrounded();
|
||||
}
|
||||
|
||||
|
||||
break;
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
bool IsValidLedge(Vector3 characterPosition)
|
||||
{
|
||||
if (!CharacterActor.WallCollision)
|
||||
return false;
|
||||
|
||||
DetectLedge(
|
||||
characterPosition,
|
||||
out leftHitInfo,
|
||||
out rightHitInfo
|
||||
);
|
||||
|
||||
if (!leftHitInfo.hit || !rightHitInfo.hit)
|
||||
return false;
|
||||
|
||||
if (filterByTag)
|
||||
if (!leftHitInfo.transform.CompareTag(tagName) || !rightHitInfo.transform.CompareTag(tagName))
|
||||
return false;
|
||||
|
||||
Vector3 interpolatedNormal = Vector3.Normalize(leftHitInfo.normal + rightHitInfo.normal);
|
||||
float ledgeAngle = Vector3.Angle(CharacterActor.Up, interpolatedNormal);
|
||||
if (ledgeAngle > MaxLedgeVerticalAngle)
|
||||
return false;
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
|
||||
void DetectLedge(Vector3 position, out HitInfo leftHitInfo, out HitInfo rightHitInfo)
|
||||
{
|
||||
HitInfoFilter ledgeHitInfoFilter = new HitInfoFilter(layerMask, !detectRigidbodies, true);
|
||||
leftHitInfo = new HitInfo();
|
||||
rightHitInfo = new HitInfo();
|
||||
|
||||
Vector3 forwardDirection = CharacterActor.WallCollision ? -CharacterActor.WallContact.normal : CharacterActor.Forward;
|
||||
|
||||
|
||||
Vector3 sideDirection = Vector3.Cross(CharacterActor.Up, forwardDirection);
|
||||
|
||||
// Check if there is an object above
|
||||
Vector3 upDetection = position + CharacterActor.Up * (upwardsDetectionOffset);
|
||||
|
||||
CharacterActor.PhysicsComponent.Raycast(
|
||||
out HitInfo auxHitInfo,
|
||||
CharacterActor.Center,
|
||||
upDetection - CharacterActor.Center,
|
||||
in ledgeHitInfoFilter
|
||||
);
|
||||
|
||||
|
||||
if (auxHitInfo.hit)
|
||||
return;
|
||||
|
||||
Vector3 middleOrigin = upDetection + forwardDirection * (forwardDetectionOffset);
|
||||
|
||||
Vector3 leftOrigin = middleOrigin - sideDirection * (separationBetweenHands / 2f);
|
||||
Vector3 rightOrigin = middleOrigin + sideDirection * (separationBetweenHands / 2f);
|
||||
|
||||
CharacterActor.PhysicsComponent.Raycast(
|
||||
out leftHitInfo,
|
||||
leftOrigin,
|
||||
-CharacterActor.Up * ledgeDetectionDistance,
|
||||
in ledgeHitInfoFilter
|
||||
);
|
||||
|
||||
|
||||
CharacterActor.PhysicsComponent.Raycast(
|
||||
out rightHitInfo,
|
||||
rightOrigin,
|
||||
-CharacterActor.Up * ledgeDetectionDistance,
|
||||
in ledgeHitInfoFilter
|
||||
);
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
#if UNITY_EDITOR
|
||||
|
||||
CharacterBody characterBody = null;
|
||||
|
||||
void OnValidate()
|
||||
{
|
||||
characterBody = this.GetComponentInBranch<CharacterBody>();
|
||||
}
|
||||
|
||||
void OnDrawGizmos()
|
||||
{
|
||||
Vector3 forwardDirection = transform.forward;
|
||||
|
||||
if (characterBody != null)
|
||||
if (characterBody.Is2D)
|
||||
forwardDirection = transform.right;
|
||||
|
||||
Vector3 sideDirection = Vector3.Cross(transform.up, forwardDirection);
|
||||
Vector3 middleOrigin = transform.position + transform.up * (upwardsDetectionOffset) + forwardDirection * (forwardDetectionOffset);
|
||||
Vector3 leftOrigin = middleOrigin - sideDirection * (separationBetweenHands / 2f);
|
||||
Vector3 rightOrigin = middleOrigin + sideDirection * (separationBetweenHands / 2f);
|
||||
|
||||
CustomUtilities.DrawArrowGizmo(leftOrigin, leftOrigin - transform.up * ledgeDetectionDistance, Color.red, 0.15f);
|
||||
CustomUtilities.DrawArrowGizmo(rightOrigin, rightOrigin - transform.up * ledgeDetectionDistance, Color.red, 0.15f);
|
||||
}
|
||||
|
||||
#endif
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
Reference in New Issue
Block a user