377 lines
12 KiB
C#
377 lines
12 KiB
C#
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
|
|
|
|
}
|
|
|
|
}
|
|
|