This commit is contained in:
CortexCore
2023-10-04 16:50:27 +08:00
parent 947e52e748
commit 5cd094ed9a
263 changed files with 144068 additions and 66 deletions

View File

@@ -0,0 +1,199 @@
using System.Collections.Generic;
using UnityEngine;
using Lightbug.Utilities;
using Lightbug.CharacterControllerPro.Implementation;
using Lightbug.CharacterControllerPro.Core;
namespace Lightbug.CharacterControllerPro.Demo
{
public enum DashMode
{
FacingDirection,
InputDirection
}
[AddComponentMenu("Character Controller Pro/Demo/Character/States/Dash")]
public class Dash : CharacterState
{
[Min(0f)]
[SerializeField]
protected float initialVelocity = 12f;
[Min(0f)]
[SerializeField]
protected float duration = 0.4f;
[SerializeField]
protected AnimationCurve movementCurve = AnimationCurve.Linear(0, 1, 1, 0);
[Min(0f)]
[SerializeField]
protected int availableNotGroundedDashes = 1;
[SerializeField]
protected bool ignoreSpeedMultipliers = false;
[SerializeField]
protected bool forceNotGrounded = true;
[Tooltip("Should the dash stop when we hit an obstacle (wall collision)?")]
[SerializeField]
protected bool cancelOnContact = true;
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
protected MaterialController materialController = null;
protected int airDashesLeft;
protected float dashCursor = 0;
protected Vector3 dashDirection = Vector2.right;
protected bool isDone = true;
protected float currentSpeedMultiplier = 1f;
#region Events
/// <summary>
/// This event is called when the dash state is entered.
///
/// The direction of the dash is passed as an argument.
/// </summary>
public event System.Action<Vector3> OnDashStart;
/// <summary>
/// This event is called when the dash action has ended.
///
/// The direction of the dash is passed as an argument.
/// </summary>
public event System.Action<Vector3> OnDashEnd;
#endregion
void OnEnable() => CharacterActor.OnGroundedStateEnter += OnGroundedStateEnter;
void OnDisable() => CharacterActor.OnGroundedStateEnter -= OnGroundedStateEnter;
public override string GetInfo()
{
return "This state is entirely based on particular movement, the \"dash\". This movement is normally a fast impulse along " +
"the forward direction. In this case the movement can be defined by using an animation curve (time vs velocity)";
}
void OnGroundedStateEnter(Vector3 localVelocity) => airDashesLeft = availableNotGroundedDashes;
bool EvaluateCancelOnContact() => CharacterActor.WallContacts.Count != 0;
protected override void Awake()
{
base.Awake();
materialController = this.GetComponentInBranch<CharacterActor, MaterialController>();
airDashesLeft = availableNotGroundedDashes;
}
public override bool CheckEnterTransition(CharacterState fromState)
{
if (!CharacterActor.IsGrounded && airDashesLeft <= 0)
return false;
return true;
}
public override void CheckExitTransition()
{
if (isDone)
{
if (OnDashEnd != null)
OnDashEnd(dashDirection);
CharacterStateController.EnqueueTransition<NormalMovement>();
}
}
public override void EnterBehaviour(float dt, CharacterState fromState)
{
if (forceNotGrounded)
CharacterActor.alwaysNotGrounded = true;
CharacterActor.UseRootMotion = false;
if (CharacterActor.IsGrounded)
{
if (!ignoreSpeedMultipliers)
{
currentSpeedMultiplier = materialController != null ? materialController.CurrentSurface.speedMultiplier * materialController.CurrentVolume.speedMultiplier : 1f;
}
}
else
{
if (!ignoreSpeedMultipliers)
{
currentSpeedMultiplier = materialController != null ? materialController.CurrentVolume.speedMultiplier : 1f;
}
airDashesLeft--;
}
//Set the dash direction
dashDirection = CharacterActor.Forward;
ResetDash();
//Execute the event
OnDashStart?.Invoke(dashDirection);
}
public override void ExitBehaviour(float dt, CharacterState toState)
{
if (forceNotGrounded)
CharacterActor.alwaysNotGrounded = false;
}
public override void UpdateBehaviour(float dt)
{
Vector3 dashVelocity = initialVelocity * currentSpeedMultiplier * movementCurve.Evaluate(dashCursor) * dashDirection;
CharacterActor.Velocity = dashVelocity;
float animationDt = dt / duration;
dashCursor += animationDt;
if (dashCursor >= 1)
{
isDone = true;
dashCursor = 0;
}
}
public override void PostUpdateBehaviour(float dt)
{
if (cancelOnContact)
isDone |= EvaluateCancelOnContact();
}
public virtual void ResetDash()
{
CharacterActor.Velocity = Vector3.zero;
isDone = false;
dashCursor = 0;
}
}
}

View File

@@ -0,0 +1,33 @@
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.CharacterControllerPro.Implementation;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
public class InputBasedBehaviour : StateMachineBehaviour
{
CharacterBrain brain;
[SerializeField]
string trigger = "";
[SerializeField]
Vector2 movementValue = Vector2.zero;
public override void OnStateEnter(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
if (brain == null)
brain = animator.GetComponentInBranch<CharacterActor, CharacterBrain>();
}
public override void OnStateMove(Animator animator, AnimatorStateInfo stateInfo, int layerIndex)
{
if (brain.CharacterActions.movement.value == movementValue)
{
animator.SetTrigger(trigger);
}
}
}
}

View File

@@ -0,0 +1,82 @@
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Implementation;
namespace Lightbug.CharacterControllerPro.Demo
{
[AddComponentMenu("Character Controller Pro/Demo/Character/States/Jet Pack")]
public class JetPack : CharacterState
{
[Header("Planar movement")]
[SerializeField]
protected float targetPlanarSpeed = 5f;
[Header("Planar movement")]
[SerializeField]
protected float targetVerticalSpeed = 5f;
[SerializeField]
protected float duration = 1f;
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
protected Vector3 smoothDampVelocity = Vector3.zero;
public override string GetInfo()
{
return "This state allows the character to imitate a \"JetPack\" type of movement. Basically the character can ascend towards the up direction, " +
"but also move in the local XZ plane.";
}
public override void EnterBehaviour(float dt, CharacterState fromState)
{
CharacterActor.alwaysNotGrounded = true;
CharacterActor.UseRootMotion = false;
smoothDampVelocity = CharacterActor.VerticalVelocity;
}
public override void ExitBehaviour(float dt, CharacterState toState)
{
CharacterActor.alwaysNotGrounded = false;
}
public override void UpdateBehaviour(float dt)
{
// Vertical movement
CharacterActor.VerticalVelocity = Vector3.SmoothDamp(CharacterActor.VerticalVelocity, targetVerticalSpeed * CharacterActor.Up, ref smoothDampVelocity, duration);
// Planar movement
CharacterActor.PlanarVelocity = Vector3.Lerp(CharacterActor.PlanarVelocity, targetPlanarSpeed * CharacterStateController.InputMovementReference, 7f * dt);
// Looking direction
CharacterActor.SetYaw(CharacterActor.PlanarVelocity);
}
public override void CheckExitTransition()
{
if (CharacterActor.Triggers.Count != 0)
{
if (CharacterActions.interact.Started)
CharacterStateController.EnqueueTransition<LadderClimbing>();
}
else if (!CharacterActions.jetPack.value)
{
CharacterStateController.EnqueueTransition<NormalMovement>();
}
}
}
}

View File

@@ -0,0 +1,338 @@
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<Transform, Ladder> ladders = new Dictionary<Transform, Ladder>();
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<NormalMovement>();
}
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<Ladder>(FindObjectsSortMode.None);
#else
Ladder[] laddersArray = FindObjectsOfType<Ladder>();
#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);
}
}
}

View File

@@ -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
}
}

View File

@@ -0,0 +1,817 @@
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.Utilities;
using Lightbug.CharacterControllerPro.Implementation;
namespace Lightbug.CharacterControllerPro.Demo
{
[AddComponentMenu("Character Controller Pro/Demo/Character/States/Normal Movement")]
public class NormalMovement : CharacterState
{
[Space(10)]
public PlanarMovementParameters planarMovementParameters = new PlanarMovementParameters();
public VerticalMovementParameters verticalMovementParameters = new VerticalMovementParameters();
public CrouchParameters crouchParameters = new CrouchParameters();
public LookingDirectionParameters lookingDirectionParameters = new LookingDirectionParameters();
[Header("Animation")]
[SerializeField]
protected string groundedParameter = "Grounded";
[SerializeField]
protected string stableParameter = "Stable";
[SerializeField]
protected string verticalSpeedParameter = "VerticalSpeed";
[SerializeField]
protected string planarSpeedParameter = "PlanarSpeed";
[SerializeField]
protected string horizontalAxisParameter = "HorizontalAxis";
[SerializeField]
protected string verticalAxisParameter = "VerticalAxis";
[SerializeField]
protected string heightParameter = "Height";
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
#region Events
/// <summary>
/// Event triggered when the character jumps.
/// </summary>
public event System.Action OnJumpPerformed;
/// <summary>
/// Event triggered when the character jumps from the ground.
/// </summary>
public event System.Action<bool> OnGroundedJumpPerformed;
/// <summary>
/// Event triggered when the character jumps while.
/// </summary>
public event System.Action<int> OnNotGroundedJumpPerformed;
#endregion
protected MaterialController materialController = null;
protected int notGroundedJumpsLeft = 0;
protected bool isAllowedToCancelJump = false;
protected bool wantToRun = false;
protected float currentPlanarSpeedLimit = 0f;
protected bool groundedJumpAvailable = true;
protected Vector3 jumpDirection = default(Vector3);
protected Vector3 targetLookingDirection = default(Vector3);
protected float targetHeight = 1f;
protected bool wantToCrouch = false;
protected bool isCrouched = false;
protected PlanarMovementParameters.PlanarMovementProperties currentMotion = new PlanarMovementParameters.PlanarMovementProperties();
bool reducedAirControlFlag = false;
float reducedAirControlInitialTime = 0f;
float reductionDuration = 0.5f;
protected override void Awake()
{
base.Awake();
notGroundedJumpsLeft = verticalMovementParameters.availableNotGroundedJumps;
materialController = this.GetComponentInBranch<CharacterActor, MaterialController>();
}
protected virtual void OnValidate()
{
verticalMovementParameters.OnValidate();
}
protected override void Start()
{
base.Start();
targetHeight = CharacterActor.DefaultBodySize.y;
float minCrouchHeightRatio = CharacterActor.BodySize.x / CharacterActor.BodySize.y;
crouchParameters.heightRatio = Mathf.Max(minCrouchHeightRatio, crouchParameters.heightRatio);
}
protected virtual void OnEnable()
{
CharacterActor.OnTeleport += OnTeleport;
}
protected virtual void OnDisable()
{
CharacterActor.OnTeleport -= OnTeleport;
}
public override string GetInfo()
{
return "This state serves as a multi purpose movement based state. It is responsible for handling gravity and jump, walk and run, crouch, " +
"react to the different material properties, etc. Basically it covers all the common movements involved " +
"in a typical game, from a 3D platformer to a first person walking simulator.";
}
void OnTeleport(Vector3 position, Quaternion rotation)
{
targetLookingDirection = CharacterActor.Forward;
isAllowedToCancelJump = false;
}
/// <summary>
/// Gets/Sets the useGravity toggle. Use this property to enable/disable the effect of gravity on the character.
/// </summary>
/// <value></value>
public bool UseGravity
{
get => verticalMovementParameters.useGravity;
set => verticalMovementParameters.useGravity = value;
}
public override void CheckExitTransition()
{
if (CharacterActions.jetPack.value)
{
CharacterStateController.EnqueueTransition<JetPack>();
}
else if (CharacterActions.dash.Started)
{
CharacterStateController.EnqueueTransition<Dash>();
}
else if (CharacterActor.Triggers.Count != 0)
{
CharacterStateController.EnqueueTransition<LadderClimbing>();
CharacterStateController.EnqueueTransition<RopeClimbing>();
}
else if (!CharacterActor.IsGrounded)
{
if (!CharacterActions.crouch.value)
CharacterStateController.EnqueueTransition<WallSlide>();
CharacterStateController.EnqueueTransition<LedgeHanging>();
}
}
public override void ExitBehaviour(float dt, CharacterState toState)
{
reducedAirControlFlag = false;
}
/// <summary>
/// Reduces the amount of acceleration and deceleration (not grounded state) until the character reaches the apex of the jump
/// (vertical velocity close to zero). This can be useful to prevent the character from accelerating/decelerating too quickly (e.g. right after performing a wall jump).
/// </summary>
public void ReduceAirControl(float reductionDuration = 0.5f)
{
reducedAirControlFlag = true;
reducedAirControlInitialTime = Time.time;
this.reductionDuration = reductionDuration;
}
void SetMotionValues(Vector3 targetPlanarVelocity)
{
float angleCurrentTargetVelocity = Vector3.Angle(CharacterActor.PlanarVelocity, targetPlanarVelocity);
switch (CharacterActor.CurrentState)
{
case CharacterActorState.StableGrounded:
currentMotion.acceleration = planarMovementParameters.stableGroundedAcceleration;
currentMotion.deceleration = planarMovementParameters.stableGroundedDeceleration;
currentMotion.angleAccelerationMultiplier = planarMovementParameters.stableGroundedAngleAccelerationBoost.Evaluate(angleCurrentTargetVelocity);
break;
case CharacterActorState.UnstableGrounded:
currentMotion.acceleration = planarMovementParameters.unstableGroundedAcceleration;
currentMotion.deceleration = planarMovementParameters.unstableGroundedDeceleration;
currentMotion.angleAccelerationMultiplier = planarMovementParameters.unstableGroundedAngleAccelerationBoost.Evaluate(angleCurrentTargetVelocity);
break;
case CharacterActorState.NotGrounded:
if (reducedAirControlFlag)
{
float time = Time.time - reducedAirControlInitialTime;
if (time <= reductionDuration)
{
currentMotion.acceleration = (planarMovementParameters.notGroundedAcceleration / reductionDuration) * time;
currentMotion.deceleration = (planarMovementParameters.notGroundedDeceleration / reductionDuration) * time;
}
else
{
reducedAirControlFlag = false;
currentMotion.acceleration = planarMovementParameters.notGroundedAcceleration;
currentMotion.deceleration = planarMovementParameters.notGroundedDeceleration;
}
}
else
{
currentMotion.acceleration = planarMovementParameters.notGroundedAcceleration;
currentMotion.deceleration = planarMovementParameters.notGroundedDeceleration;
}
currentMotion.angleAccelerationMultiplier = planarMovementParameters.notGroundedAngleAccelerationBoost.Evaluate(angleCurrentTargetVelocity);
break;
}
// Material values
if (materialController != null)
{
if (CharacterActor.IsGrounded)
{
currentMotion.acceleration *= materialController.CurrentSurface.accelerationMultiplier * materialController.CurrentVolume.accelerationMultiplier;
currentMotion.deceleration *= materialController.CurrentSurface.decelerationMultiplier * materialController.CurrentVolume.decelerationMultiplier;
}
else
{
currentMotion.acceleration *= materialController.CurrentVolume.accelerationMultiplier;
currentMotion.deceleration *= materialController.CurrentVolume.decelerationMultiplier;
}
}
}
/// <summary>
/// Processes the lateral movement of the character (stable and unstable state), that is, walk, run, crouch, etc.
/// This movement is tied directly to the "movement" character action.
/// </summary>
protected virtual void ProcessPlanarMovement(float dt)
{
//SetMotionValues();
float speedMultiplier = materialController != null ?
materialController.CurrentSurface.speedMultiplier * materialController.CurrentVolume.speedMultiplier : 1f;
bool needToAccelerate = CustomUtilities.Multiply(CharacterStateController.InputMovementReference, currentPlanarSpeedLimit).sqrMagnitude >= CharacterActor.PlanarVelocity.sqrMagnitude;
Vector3 targetPlanarVelocity = default;
switch (CharacterActor.CurrentState)
{
case CharacterActorState.NotGrounded:
if (CharacterActor.WasGrounded)
currentPlanarSpeedLimit = Mathf.Max(CharacterActor.PlanarVelocity.magnitude, planarMovementParameters.baseSpeedLimit);
//needToAccelerate = CustomUtilities.Multiply(CharacterStateController.InputMovementReference, currentPlanarSpeedLimit).sqrMagnitude >= CharacterActor.PlanarVelocity.sqrMagnitude;
targetPlanarVelocity = CustomUtilities.Multiply(CharacterStateController.InputMovementReference, speedMultiplier, currentPlanarSpeedLimit);
//GetAccelerationBoost(targetPlanarVelocity)
break;
case CharacterActorState.StableGrounded:
// Run ------------------------------------------------------------
if (planarMovementParameters.runInputMode == InputMode.Toggle)
{
if (CharacterActions.run.Started)
wantToRun = !wantToRun;
}
else
{
wantToRun = CharacterActions.run.value;
}
if (wantToCrouch || !planarMovementParameters.canRun)
wantToRun = false;
if (isCrouched)
{
currentPlanarSpeedLimit = planarMovementParameters.baseSpeedLimit * crouchParameters.speedMultiplier;
}
else
{
currentPlanarSpeedLimit = wantToRun ? planarMovementParameters.boostSpeedLimit : planarMovementParameters.baseSpeedLimit;
}
targetPlanarVelocity = CustomUtilities.Multiply(CharacterStateController.InputMovementReference, speedMultiplier, currentPlanarSpeedLimit);
//needToAccelerate = CharacterStateController.InputMovementReference != Vector3.zero;
break;
case CharacterActorState.UnstableGrounded:
currentPlanarSpeedLimit = planarMovementParameters.baseSpeedLimit;
//needToAccelerate = CustomUtilities.Multiply(CharacterStateController.InputMovementReference, currentPlanarSpeedLimit).sqrMagnitude >= CharacterActor.PlanarVelocity.sqrMagnitude;
targetPlanarVelocity = CustomUtilities.Multiply(CharacterStateController.InputMovementReference, speedMultiplier, currentPlanarSpeedLimit);
break;
}
SetMotionValues(targetPlanarVelocity);
float acceleration = currentMotion.acceleration;
if (needToAccelerate)
{
acceleration *= currentMotion.angleAccelerationMultiplier;
// Affect acceleration based on the angle between target velocity and current velocity
//float angleCurrentTargetVelocity = Vector3.Angle(CharacterActor.PlanarVelocity, targetPlanarVelocity);
//float accelerationBoost = 20f * (angleCurrentTargetVelocity / 180f);
//acceleration += accelerationBoost;
}
else
{
acceleration = currentMotion.deceleration;
}
CharacterActor.PlanarVelocity = Vector3.MoveTowards(
CharacterActor.PlanarVelocity,
targetPlanarVelocity,
acceleration * dt
);
}
protected virtual void ProcessGravity(float dt)
{
if (!verticalMovementParameters.useGravity)
return;
verticalMovementParameters.UpdateParameters();
float gravityMultiplier = 1f;
if (materialController != null)
gravityMultiplier = CharacterActor.LocalVelocity.y >= 0 ?
materialController.CurrentVolume.gravityAscendingMultiplier :
materialController.CurrentVolume.gravityDescendingMultiplier;
float gravity = gravityMultiplier * verticalMovementParameters.gravity;
if (!CharacterActor.IsStable)
CharacterActor.VerticalVelocity += CustomUtilities.Multiply(-CharacterActor.Up, gravity, dt);
}
protected bool UnstableGroundedJumpAvailable => !verticalMovementParameters.canJumpOnUnstableGround && CharacterActor.CurrentState == CharacterActorState.UnstableGrounded;
public enum JumpResult
{
Invalid,
Grounded,
NotGrounded
}
JumpResult CanJump()
{
JumpResult jumpResult = JumpResult.Invalid;
if (!verticalMovementParameters.canJump)
return jumpResult;
if (isCrouched)
return jumpResult;
switch (CharacterActor.CurrentState)
{
case CharacterActorState.StableGrounded:
if (CharacterActions.jump.StartedElapsedTime <= verticalMovementParameters.preGroundedJumpTime && groundedJumpAvailable)
jumpResult = JumpResult.Grounded;
break;
case CharacterActorState.NotGrounded:
if (CharacterActions.jump.Started)
{
// First check if the "grounded jump" is available. If so, execute a "coyote jump".
if (CharacterActor.NotGroundedTime <= verticalMovementParameters.postGroundedJumpTime && groundedJumpAvailable)
{
jumpResult = JumpResult.Grounded;
}
else if (notGroundedJumpsLeft != 0) // Do a not grounded jump
{
jumpResult = JumpResult.NotGrounded;
}
}
break;
case CharacterActorState.UnstableGrounded:
if (CharacterActions.jump.StartedElapsedTime <= verticalMovementParameters.preGroundedJumpTime && verticalMovementParameters.canJumpOnUnstableGround)
jumpResult = JumpResult.Grounded;
break;
}
return jumpResult;
}
protected virtual void ProcessJump(float dt)
{
ProcessRegularJump(dt);
ProcessJumpDown(dt);
}
#region JumpDown
protected virtual bool ProcessJumpDown(float dt)
{
if (!verticalMovementParameters.canJumpDown)
return false;
if (!CharacterActor.IsStable)
return false;
if (!CharacterActor.IsGroundAOneWayPlatform)
return false;
if (verticalMovementParameters.filterByTag)
{
if (!CharacterActor.GroundObject.CompareTag(verticalMovementParameters.jumpDownTag))
return false;
}
if (!ProcessJumpDownAction())
return false;
JumpDown(dt);
return true;
}
protected virtual bool ProcessJumpDownAction()
{
return isCrouched && CharacterActions.jump.Started;
}
protected virtual void JumpDown(float dt)
{
float groundDisplacementExtraDistance = 0f;
Vector3 groundDisplacement = CustomUtilities.Multiply(CharacterActor.GroundVelocity, dt);
// bool CharacterActor.transform.InverseTransformVectorUnscaled( Vector3.Project( groundDisplacement , CharacterActor.Up ) ).y
if (!CharacterActor.IsGroundAscending)
groundDisplacementExtraDistance = groundDisplacement.magnitude;
CharacterActor.ForceNotGrounded();
CharacterActor.Position -=
CustomUtilities.Multiply(
CharacterActor.Up,
CharacterConstants.ColliderMinBottomOffset + verticalMovementParameters.jumpDownDistance + groundDisplacementExtraDistance
);
CharacterActor.VerticalVelocity -= CustomUtilities.Multiply(CharacterActor.Up, verticalMovementParameters.jumpDownVerticalVelocity);
}
#endregion
#region Jump
protected virtual void ProcessRegularJump(float dt)
{
if (CharacterActor.IsGrounded)
{
notGroundedJumpsLeft = verticalMovementParameters.availableNotGroundedJumps;
groundedJumpAvailable = true;
}
if (isAllowedToCancelJump)
{
if (verticalMovementParameters.cancelJumpOnRelease)
{
if (CharacterActions.jump.StartedElapsedTime >= verticalMovementParameters.cancelJumpMaxTime || CharacterActor.IsFalling)
{
isAllowedToCancelJump = false;
}
else if (!CharacterActions.jump.value && CharacterActions.jump.StartedElapsedTime >= verticalMovementParameters.cancelJumpMinTime)
{
// Get the velocity mapped onto the current jump direction
Vector3 projectedJumpVelocity = Vector3.Project(CharacterActor.Velocity, jumpDirection);
CharacterActor.Velocity -= CustomUtilities.Multiply(projectedJumpVelocity, 1f - verticalMovementParameters.cancelJumpMultiplier);
isAllowedToCancelJump = false;
}
}
}
else
{
JumpResult jumpResult = CanJump();
switch (jumpResult)
{
case JumpResult.Grounded:
groundedJumpAvailable = false;
break;
case JumpResult.NotGrounded:
notGroundedJumpsLeft--;
break;
case JumpResult.Invalid:
return;
}
// Events ---------------------------------------------------
if (CharacterActor.IsGrounded)
{
if (OnGroundedJumpPerformed != null)
OnGroundedJumpPerformed(true);
}
else
{
if (OnNotGroundedJumpPerformed != null)
OnNotGroundedJumpPerformed(notGroundedJumpsLeft);
}
if (OnJumpPerformed != null)
OnJumpPerformed();
// Define the jump direction ---------------------------------------------------
jumpDirection = SetJumpDirection();
// Force "not grounded" state.
if (CharacterActor.IsGrounded)
CharacterActor.ForceNotGrounded();
// First remove any velocity associated with the jump direction.
CharacterActor.Velocity -= Vector3.Project(CharacterActor.Velocity, jumpDirection);
CharacterActor.Velocity += CustomUtilities.Multiply(jumpDirection, verticalMovementParameters.jumpSpeed);
if (verticalMovementParameters.cancelJumpOnRelease)
isAllowedToCancelJump = true;
}
}
/// <summary>
/// Returns the jump direction vector whenever the jump action is started.
/// </summary>
protected virtual Vector3 SetJumpDirection()
{
return CharacterActor.Up;
}
#endregion
void ProcessVerticalMovement(float dt)
{
ProcessGravity(dt);
ProcessJump(dt);
}
public override void EnterBehaviour(float dt, CharacterState fromState)
{
CharacterActor.alwaysNotGrounded = false;
targetLookingDirection = CharacterActor.Forward;
if (fromState == CharacterStateController.GetState<WallSlide>())
{
// "availableNotGroundedJumps + 1" because the update code will consume one jump!
notGroundedJumpsLeft = verticalMovementParameters.availableNotGroundedJumps + 1;
// Reduce the amount of air control (acceleration and deceleration) for 0.5 seconds.
ReduceAirControl(0.5f);
}
currentPlanarSpeedLimit = Mathf.Max(CharacterActor.PlanarVelocity.magnitude, planarMovementParameters.baseSpeedLimit);
CharacterActor.UseRootMotion = false;
}
protected virtual void HandleRotation(float dt)
{
HandleLookingDirection(dt);
}
void HandleLookingDirection(float dt)
{
if (!lookingDirectionParameters.changeLookingDirection)
return;
switch (lookingDirectionParameters.lookingDirectionMode)
{
case LookingDirectionParameters.LookingDirectionMode.Movement:
switch (CharacterActor.CurrentState)
{
case CharacterActorState.NotGrounded:
SetTargetLookingDirection(lookingDirectionParameters.notGroundedLookingDirectionMode);
break;
case CharacterActorState.StableGrounded:
SetTargetLookingDirection(lookingDirectionParameters.stableGroundedLookingDirectionMode);
break;
case CharacterActorState.UnstableGrounded:
SetTargetLookingDirection(lookingDirectionParameters.unstableGroundedLookingDirectionMode);
break;
}
break;
case LookingDirectionParameters.LookingDirectionMode.ExternalReference:
if (!CharacterActor.CharacterBody.Is2D)
targetLookingDirection = CharacterStateController.MovementReferenceForward;
break;
case LookingDirectionParameters.LookingDirectionMode.Target:
targetLookingDirection = (lookingDirectionParameters.target.position - CharacterActor.Position);
targetLookingDirection.Normalize();
break;
}
Quaternion targetDeltaRotation = Quaternion.FromToRotation(CharacterActor.Forward, targetLookingDirection);
Quaternion currentDeltaRotation = Quaternion.Slerp(Quaternion.identity, targetDeltaRotation, lookingDirectionParameters.speed * dt);
if (CharacterActor.CharacterBody.Is2D)
CharacterActor.SetYaw(targetLookingDirection);
else
CharacterActor.SetYaw(currentDeltaRotation * CharacterActor.Forward);
}
void SetTargetLookingDirection(LookingDirectionParameters.LookingDirectionMovementSource lookingDirectionMode)
{
if (lookingDirectionMode == LookingDirectionParameters.LookingDirectionMovementSource.Input)
{
if (CharacterStateController.InputMovementReference != Vector3.zero)
targetLookingDirection = CharacterStateController.InputMovementReference;
else
targetLookingDirection = CharacterActor.Forward;
}
else
{
if (CharacterActor.PlanarVelocity != Vector3.zero)
targetLookingDirection = Vector3.ProjectOnPlane(CharacterActor.PlanarVelocity, CharacterActor.Up);
else
targetLookingDirection = CharacterActor.Forward;
}
}
public override void UpdateBehaviour(float dt)
{
HandleSize(dt);
HandleVelocity(dt);
HandleRotation(dt);
}
public override void PreCharacterSimulation(float dt)
{
// Pre/PostCharacterSimulation methods are useful to update all the Animator parameters.
// Why? Because the CharacterActor component will end up modifying the velocity of the actor.
if (!CharacterActor.IsAnimatorValid())
return;
CharacterStateController.Animator.SetBool(groundedParameter, CharacterActor.IsGrounded);
CharacterStateController.Animator.SetBool(stableParameter, CharacterActor.IsStable);
CharacterStateController.Animator.SetFloat(horizontalAxisParameter, CharacterActions.movement.value.x);
CharacterStateController.Animator.SetFloat(verticalAxisParameter, CharacterActions.movement.value.y);
CharacterStateController.Animator.SetFloat(heightParameter, CharacterActor.BodySize.y);
}
public override void PostCharacterSimulation(float dt)
{
// Pre/PostCharacterSimulation methods are useful to update all the Animator parameters.
// Why? Because the CharacterActor component will end up modifying the velocity of the actor.
if (!CharacterActor.IsAnimatorValid())
return;
// Parameters associated with velocity are sent after the simulation.
// The PostSimulationUpdate (CharacterActor) might update velocity once more (e.g. if a "bad step" has been detected).
CharacterStateController.Animator.SetFloat(verticalSpeedParameter, CharacterActor.LocalVelocity.y);
CharacterStateController.Animator.SetFloat(planarSpeedParameter, CharacterActor.PlanarVelocity.magnitude);
}
protected virtual void HandleSize(float dt)
{
// Get the crouch input state
if (crouchParameters.enableCrouch)
{
if (crouchParameters.inputMode == InputMode.Toggle)
{
if (CharacterActions.crouch.Started)
wantToCrouch = !wantToCrouch;
}
else
{
wantToCrouch = CharacterActions.crouch.value;
}
if (!crouchParameters.notGroundedCrouch && !CharacterActor.IsGrounded)
wantToCrouch = false;
if (CharacterActor.IsGrounded && wantToRun)
wantToCrouch = false;
}
else
{
wantToCrouch = false;
}
if (wantToCrouch)
Crouch(dt);
else
StandUp(dt);
}
void Crouch(float dt)
{
CharacterActor.SizeReferenceType sizeReferenceType = CharacterActor.IsGrounded ?
CharacterActor.SizeReferenceType.Bottom : crouchParameters.notGroundedReference;
bool validSize = CharacterActor.CheckAndInterpolateHeight(
CharacterActor.DefaultBodySize.y * crouchParameters.heightRatio,
crouchParameters.sizeLerpSpeed * dt, sizeReferenceType);
if (validSize)
isCrouched = true;
}
void StandUp(float dt)
{
CharacterActor.SizeReferenceType sizeReferenceType = CharacterActor.IsGrounded ?
CharacterActor.SizeReferenceType.Bottom : crouchParameters.notGroundedReference;
bool validSize = CharacterActor.CheckAndInterpolateHeight(
CharacterActor.DefaultBodySize.y,
crouchParameters.sizeLerpSpeed * dt, sizeReferenceType);
if (validSize)
isCrouched = false;
}
protected virtual void HandleVelocity(float dt)
{
ProcessVerticalMovement(dt);
ProcessPlanarMovement(dt);
}
}
}

View File

@@ -0,0 +1,280 @@
using UnityEngine;
using Lightbug.Utilities;
using Lightbug.CharacterControllerPro.Core;
namespace Lightbug.CharacterControllerPro.Demo
{
[System.Serializable]
public class PlanarMovementParameters
{
[Min(0f)]
public float baseSpeedLimit = 6f;
[Header("Run (boost)")]
public bool canRun = true;
[Tooltip("\"Toggle\" will activate/deactivate the action when the input is \"pressed\". On the other hand, \"Hold\" will activate the action when the input is pressed, " +
"and deactivate it when the input is \"released\".")]
public InputMode runInputMode = InputMode.Hold;
[Min(0f)]
public float boostSpeedLimit = 10f;
[Header("Stable grounded parameters")]
public float stableGroundedAcceleration = 50f;
public float stableGroundedDeceleration = 40f;
public AnimationCurve stableGroundedAngleAccelerationBoost = AnimationCurve.EaseInOut(0f, 1f, 180f, 2f);
[Header("Unstable grounded parameters")]
public float unstableGroundedAcceleration = 10f;
public float unstableGroundedDeceleration = 2f;
public AnimationCurve unstableGroundedAngleAccelerationBoost = AnimationCurve.EaseInOut(0f, 1f, 180f, 1f);
[Header("Not grounded parameters")]
public float notGroundedAcceleration = 20f;
public float notGroundedDeceleration = 5f;
public AnimationCurve notGroundedAngleAccelerationBoost = AnimationCurve.EaseInOut(0f, 1f, 180f, 1f);
[System.Serializable]
public struct PlanarMovementProperties
{
[Tooltip("How fast the character increses its current velocity.")]
public float acceleration;
[Tooltip("How fast the character reduces its current velocity.")]
public float deceleration;
[Tooltip("How fast the character reduces its current velocity.")]
public float angleAccelerationMultiplier;
public PlanarMovementProperties(float acceleration, float deceleration, float angleAccelerationBoost)
{
this.acceleration = acceleration;
this.deceleration = deceleration;
this.angleAccelerationMultiplier = angleAccelerationBoost;
}
}
}
[System.Serializable]
public class VerticalMovementParameters
{
public enum UnstableJumpMode
{
Vertical,
GroundNormal
}
[Header("Gravity")]
[Tooltip("It enables/disables gravity. The gravity value is calculated based on the jump apex height and duration.")]
public bool useGravity = true;
[Header("Jump")]
public bool canJump = true;
[Space(10f)]
[Tooltip("The gravity magnitude and the jump speed will be automatically calculated based on the jump apex height and duration. Set this to false if you want to manually " +
"set those values.")]
public bool autoCalculate = true;
[Condition("autoCalculate", ConditionAttribute.ConditionType.IsTrue, ConditionAttribute.VisibilityType.NotEditable)]
[Tooltip("The height reached at the apex of the jump. The maximum height will depend on the \"jumpCancellationMode\".")]
[Min(0f)]
public float jumpApexHeight = 2.25f;
[Condition("autoCalculate", ConditionAttribute.ConditionType.IsTrue, ConditionAttribute.VisibilityType.NotEditable)]
[Tooltip("The amount of time to reach the \"base height\" (apex).")]
[Min(0f)]
public float jumpApexDuration = 0.5f;
[Condition("autoCalculate", ConditionAttribute.ConditionType.IsFalse, ConditionAttribute.VisibilityType.NotEditable)]
public float jumpSpeed = 10f;
[Condition("autoCalculate", ConditionAttribute.ConditionType.IsFalse, ConditionAttribute.VisibilityType.NotEditable)]
public float gravity = 10f;
[Space(10f)]
[Tooltip("Reduces the vertical velocity when the jump action is canceled.")]
public bool cancelJumpOnRelease = true;
[Tooltip("How much the vertical velocity is reduced when canceling the jump (0 = no effect , 1 = zero velocity).")]
[Range(0f, 1f)]
public float cancelJumpMultiplier = 0.5f;
[Tooltip("When canceling the jump (releasing the action), if the jump time is less than this value nothing is going to happen. Only when the timer is greater than this \"min time\" the jump will be affected.")]
public float cancelJumpMinTime = 0.1f;
[Tooltip("When canceling the jump (releasing the action), if the jump time is less than this value (and greater than the \"min time\") the velocity will be affected.")]
public float cancelJumpMaxTime = 0.3f;
[Space(10f)]
[Tooltip("This will help to perform the jump action after the actual input has been started. This value determines the maximum time between input and ground detection.")]
[Min(0f)]
public float preGroundedJumpTime = 0.2f;
[Tooltip("If the character is not grounded, and the \"not grounded time\" is less or equal than this value, the jump action will be performed as a grounded jump. This is also known as \"coyote time\".")]
[Min(0f)]
public float postGroundedJumpTime = 0.1f;
[Space(10f)]
[Min(0)]
[Tooltip("Number of jumps available for the character in the air.")]
public int availableNotGroundedJumps = 1;
[Space(10f)]
public bool canJumpOnUnstableGround = false;
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// public float GravityMagnitude { get; private set; } = 9.8f;
public void UpdateParameters()
{
if (autoCalculate)
{
gravity = (2 * jumpApexHeight) / Mathf.Pow(jumpApexDuration, 2);
jumpSpeed = gravity * jumpApexDuration;
}
}
public void OnValidate()
{
if (autoCalculate)
{
gravity = (2 * jumpApexHeight) / Mathf.Pow(jumpApexDuration, 2);
jumpSpeed = gravity * jumpApexDuration;
}
else
{
jumpApexDuration = jumpSpeed / gravity;
jumpApexHeight = gravity * Mathf.Pow(jumpApexDuration, 2) / 2f;
}
}
[Header("Jump Down (One Way Platforms)")]
public bool canJumpDown = true;
[Space(10f)]
public bool filterByTag = false;
public string jumpDownTag = "JumpDown";
[Space(10f)]
[Min(0f)]
public float jumpDownDistance = 0.05f;
[Min(0f)]
public float jumpDownVerticalVelocity = 0.5f;
}
[System.Serializable]
public class CrouchParameters
{
public bool enableCrouch = true;
public bool notGroundedCrouch = false;
[Tooltip("This multiplier represents the crouch ratio relative to the default height.")]
[Condition("enableCrouch", ConditionAttribute.ConditionType.IsTrue, ConditionAttribute.VisibilityType.NotEditable)]
[Min(0f)]
public float heightRatio = 0.75f;
[Tooltip("How much the crouch action affects the movement speed?.")]
[Condition("enableCrouch", ConditionAttribute.ConditionType.IsTrue, ConditionAttribute.VisibilityType.NotEditable)]
[Min(0f)]
public float speedMultiplier = 0.3f;
[Tooltip("\"Toggle\" will activate/deactivate the action when the input is \"pressed\". On the other hand, \"Hold\" will activate the action when the input is pressed, " +
"and deactivate it when the input is \"released\".")]
[Condition("enableCrouch", ConditionAttribute.ConditionType.IsTrue, ConditionAttribute.VisibilityType.NotEditable)]
public InputMode inputMode = InputMode.Hold;
[Tooltip("This field determines an anchor point in space (top, center or bottom) that can be used as a reference during size changes. " +
"For instance, by using \"top\" as a reference, the character will shrink/grow my moving only the bottom part of the body.")]
public CharacterActor.SizeReferenceType notGroundedReference = CharacterActor.SizeReferenceType.Top;
[Min(0f)]
public float sizeLerpSpeed = 8f;
}
[System.Serializable]
public class LookingDirectionParameters
{
public bool changeLookingDirection = true;
[Header("Lerp properties")]
public float speed = 10f;
[Header("Target Direction")]
public LookingDirectionMode lookingDirectionMode = LookingDirectionMode.Movement;
[Condition("lookingDirectionMode", ConditionAttribute.ConditionType.IsEqualTo, ConditionAttribute.VisibilityType.Hidden, (int)LookingDirectionMode.Target)]
[Space(5f)]
public Transform target = null;
[Space(5f)]
[Condition("lookingDirectionMode", ConditionAttribute.ConditionType.IsEqualTo, ConditionAttribute.VisibilityType.Hidden, (int)LookingDirectionMode.Movement)]
public LookingDirectionMovementSource stableGroundedLookingDirectionMode = LookingDirectionMovementSource.Input;
[Condition("lookingDirectionMode", ConditionAttribute.ConditionType.IsEqualTo, ConditionAttribute.VisibilityType.Hidden, (int)LookingDirectionMode.Movement)]
public LookingDirectionMovementSource unstableGroundedLookingDirectionMode = LookingDirectionMovementSource.Velocity;
[Condition("lookingDirectionMode", ConditionAttribute.ConditionType.IsEqualTo, ConditionAttribute.VisibilityType.Hidden, (int)LookingDirectionMode.Movement)]
public LookingDirectionMovementSource notGroundedLookingDirectionMode = LookingDirectionMovementSource.Input;
public enum LookingDirectionMode
{
Movement,
Target,
ExternalReference
}
public enum LookingDirectionMovementSource
{
Velocity,
Input
}
}
public enum InputMode
{
Toggle,
Hold
}
}

View File

@@ -0,0 +1,189 @@
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Implementation;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
public class RopeClimbing : CharacterState
{
[Header("Movement")]
[SerializeField]
protected float climbSpeed = 3f;
[SerializeField]
protected float angularSpeed = 120f;
[SerializeField]
protected float jumpVelocity = 10f;
[SerializeField]
protected float verticalAcceleration = 10f;
[SerializeField]
protected float angularAcceleration = 10f;
[Header("Offset")]
[SerializeField]
protected float forwardOffset = -0.25f;
[Header("Animation")]
[SerializeField]
protected string verticalVelocityParameter = "VerticalVelocity";
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
protected Rope currentRope = null;
protected Dictionary<Transform, Rope> ropes = new Dictionary<Transform, Rope>();
protected Vector3 verticalVelocity;
protected Vector3 angularVelocity;
Vector3 ReferencePosition => CharacterActor.Top;
Vector3 ClosestVectorToRope
{
get
{
Vector3 characterToTop = currentRope.TopPosition - CharacterActor.Position;
return Vector3.ProjectOnPlane(characterToTop, currentRope.BottomToTop);
}
}
protected override void Awake()
{
base.Awake();
#if UNITY_2023_1_OR_NEWER
Rope[] ropesArray = FindObjectsByType<Rope>(FindObjectsSortMode.None);
#else
Rope[] ropesArray = FindObjectsOfType<Rope>();
#endif
for (int i = 0; i < ropesArray.Length; i++)
ropes.Add(ropesArray[i].transform, ropesArray[i]);
}
public override void CheckExitTransition()
{
if (!currentRope.IsInRange(ReferencePosition) || CharacterActions.jump.Started)
CharacterStateController.EnqueueTransition<NormalMovement>();
}
public override bool CheckEnterTransition(CharacterState fromState)
{
for (int i = 0; i < CharacterActor.Triggers.Count; i++)
{
Trigger trigger = CharacterActor.Triggers[i];
if (!trigger.firstContact)
continue;
Rope rope = ropes.GetOrRegisterValue(trigger.transform);
if (rope != null)
{
if (!rope.IsInRange(ReferencePosition))
return false;
currentRope = rope;
return true;
}
}
return false;
}
public override void EnterBehaviour(float dt, CharacterState fromState)
{
CharacterActor.IsKinematic = false;
CharacterActor.alwaysNotGrounded = true;
CharacterActor.UseRootMotion = false;
CharacterActor.Velocity = Vector3.zero;
Vector3 closestVectorToRope = ClosestVectorToRope;
CharacterActor.SetYaw(closestVectorToRope);
CharacterActor.Position = CharacterActor.Position + closestVectorToRope + CharacterActor.Forward * forwardOffset;
}
public override void ExitBehaviour(float dt, CharacterState toState)
{
CharacterActor.alwaysNotGrounded = false;
currentRope = null;
if (CharacterActions.jump.Started)
{
if (CharacterActions.movement.Detected)
CharacterActor.Velocity = CharacterStateController.InputMovementReference * jumpVelocity;
else
CharacterActor.Velocity = CharacterStateController.MovementReferenceForward * jumpVelocity;
CharacterActor.SetYaw(Vector3.Normalize(CharacterActor.Velocity));
}
else
{
CharacterActor.Velocity = Vector3.zero;
}
}
public override void UpdateBehaviour(float dt)
{
Vector3 characterPosition = CharacterActor.Position;
float targetAngularSpeed = CharacterActions.movement.value.x * angularSpeed;
characterPosition = CustomUtilities.RotatePointAround(
characterPosition,
characterPosition + ClosestVectorToRope,
targetAngularSpeed * dt,
Vector3.Normalize(currentRope.BottomToTop)
);
Vector3 targetAngularVelocity = (characterPosition - CharacterActor.Position) / dt;
angularVelocity = Vector3.MoveTowards(angularVelocity, targetAngularVelocity, angularAcceleration * dt);
Vector3 targetVerticalVelocity = CharacterActions.movement.value.y * climbSpeed * CharacterActor.Up;
verticalVelocity = Vector3.MoveTowards(verticalVelocity, targetVerticalVelocity, verticalAcceleration * dt);
CharacterActor.Velocity = verticalVelocity + angularVelocity;
}
public override void PostUpdateBehaviour(float dt)
{
// Always look towards the rope.
CharacterActor.SetYaw(ClosestVectorToRope);
}
public override void PostCharacterSimulation(float dt)
{
if (!CharacterActor.IsAnimatorValid())
return;
CharacterActor.Animator.SetFloat(verticalVelocityParameter, CharacterActor.LocalVelocity.y);
}
}
}

View File

@@ -0,0 +1,233 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Core;
using Lightbug.CharacterControllerPro.Implementation;
using Lightbug.Utilities;
namespace Lightbug.CharacterControllerPro.Demo
{
[AddComponentMenu("Character Controller Pro/Demo/Character/States/Wall Slide")]
public class WallSlide : CharacterState
{
[Header("Filter")]
[SerializeField]
protected bool filterByTag = true;
[Condition("filterByTag", ConditionAttribute.ConditionType.IsTrue)]
[SerializeField]
protected string wallTag = "WallSlide";
[Header("Slide")]
[SerializeField]
protected float slideAcceleration = 10f;
[Range(0f, 1f)]
[SerializeField]
protected float initialIntertia = 0.4f;
[Header("Grab")]
public bool enableGrab = true;
public bool enableClimb = true;
[Condition("enableClimb", ConditionAttribute.ConditionType.IsTrue)]
public float wallClimbHorizontalSpeed = 1f;
[Condition("enableClimb", ConditionAttribute.ConditionType.IsTrue)]
public float wallClimbVerticalSpeed = 3f;
[Condition("enableClimb", ConditionAttribute.ConditionType.IsTrue)]
public float wallClimbAcceleration = 10f;
[Header("Size")]
[SerializeField]
protected bool modifySize = true;
[Condition("modifySize", ConditionAttribute.ConditionType.IsTrue)]
[SerializeField]
protected float height = 1.5f;
[Header("Jump")]
[SerializeField]
protected float jumpNormalVelocity = 5f;
[SerializeField]
protected float jumpVerticalVelocity = 10f;
[Header("Animation")]
[SerializeField]
protected string horizontalVelocityParameter = "HorizontalVelocity";
[SerializeField]
protected string verticalVelocityParameter = "VerticalVelocity";
[SerializeField]
protected string grabParameter = "Grab";
[SerializeField]
protected string movementDetectedParameter = "MovementDetected";
// ─────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────────
protected bool wallJump = false;
protected Vector2 initialSize = Vector2.zero;
public override void CheckExitTransition()
{
if (CharacterActions.crouch.value || CharacterActor.IsGrounded || !CharacterActor.WallCollision || !CheckCenterRay())
{
CharacterStateController.EnqueueTransition<NormalMovement>();
}
else if (CharacterActions.jump.Started)
{
wallJump = true;
CharacterStateController.EnqueueTransition<NormalMovement>();
}
else
{
CharacterStateController.EnqueueTransition<LedgeHanging>();
}
}
public override bool CheckEnterTransition(CharacterState fromState)
{
if (CharacterActor.IsAscending)
return false;
if (!CharacterActor.WallCollision)
return false;
if (filterByTag)
if (!CharacterActor.WallContact.gameObject.CompareTag(wallTag))
return false;
if (!CheckCenterRay())
return false;
return true;
}
protected virtual bool CheckCenterRay()
{
HitInfoFilter filter = new HitInfoFilter(
CharacterActor.PhysicsComponent.CollisionLayerMask,
true,
true
);
CharacterActor.PhysicsComponent.Raycast(
out HitInfo centerRayHitInfo,
CharacterActor.Center,
-CharacterActor.WallContact.normal * 1.2f * CharacterActor.BodySize.x,
in filter
);
return centerRayHitInfo.hit && centerRayHitInfo.transform.gameObject == CharacterActor.WallContact.gameObject;
}
public override void EnterBehaviour(float dt, CharacterState fromState)
{
CharacterActor.UseRootMotion = false;
CharacterActor.Velocity *= initialIntertia;
CharacterActor.SetYaw(-CharacterActor.WallContact.normal);
if (modifySize)
{
initialSize = CharacterActor.BodySize;
CharacterActor.SetSize(new Vector2(initialSize.x, height), CharacterActor.SizeReferenceType.Center);
}
}
public override void ExitBehaviour(float dt, CharacterState toState)
{
if (wallJump)
{
wallJump = false;
// Do a 180 degrees turn.
CharacterActor.TurnAround();
// Apply the wall jump velocity.
CharacterActor.Velocity = jumpVerticalVelocity * CharacterActor.Up + jumpNormalVelocity * CharacterActor.WallContact.normal;
}
if (modifySize)
{
CharacterActor.SizeReferenceType sizeReferenceType = CharacterActor.IsGrounded ?
CharacterActor.SizeReferenceType.Bottom : CharacterActor.SizeReferenceType.Top;
CharacterActor.SetSize(initialSize, sizeReferenceType);
}
}
protected bool IsGrabbing => CharacterActions.run.value && enableGrab;
public override void UpdateBehaviour(float dt)
{
if (IsGrabbing)
{
Vector3 rightDirection = Vector3.ProjectOnPlane(CharacterStateController.MovementReferenceRight, CharacterActor.WallContact.normal);
rightDirection.Normalize();
Vector3 upDirection = CharacterActor.Up;
Vector3 targetVelocity = enableClimb ? CharacterActions.movement.value.x * rightDirection * wallClimbHorizontalSpeed +
CharacterActions.movement.value.y * upDirection * wallClimbVerticalSpeed : Vector3.zero;
CharacterActor.Velocity = Vector3.MoveTowards(CharacterActor.Velocity, targetVelocity, wallClimbAcceleration * dt);
}
else
{
CharacterActor.VerticalVelocity += -CharacterActor.Up * slideAcceleration * dt;
}
}
public override void PostUpdateBehaviour(float dt)
{
if (!CharacterActor.IsAnimatorValid())
return;
CharacterActor.Animator.SetFloat(horizontalVelocityParameter, CharacterActor.LocalVelocity.x);
CharacterActor.Animator.SetFloat(verticalVelocityParameter, CharacterActor.LocalVelocity.y);
CharacterActor.Animator.SetBool(grabParameter, IsGrabbing);
CharacterActor.Animator.SetBool(movementDetectedParameter, CharacterActions.movement.Detected);
}
public override void UpdateIK(int layerIndex)
{
if (!CharacterActor.IsAnimatorValid())
return;
if (IsGrabbing && CharacterActions.movement.Detected)
{
CharacterActor.Animator.SetLookAtWeight(Mathf.Clamp01(CharacterActor.Velocity.magnitude), 0f, 0.2f);
CharacterActor.Animator.SetLookAtPosition(CharacterActor.Position + CharacterActor.Velocity);
}
else
{
CharacterActor.Animator.SetLookAtWeight(0f);
}
}
}
}

View File

@@ -0,0 +1,87 @@
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using Lightbug.CharacterControllerPro.Implementation;
namespace Lightbug.CharacterControllerPro.Demo
{
public class ZeroGravity : CharacterState
{
[Header("Movement")]
public float baseSpeed = 10f;
public float acceleration = 20f;
public float deceleration = 20f;
[Header("Pitch")]
public bool invertPitch = false;
public float pitchAngularSpeed = 180f;
[Min(0f)]
public float pitchLerpAcceleration = 5f;
[Header("Roll")]
public bool invertRoll = false;
public float rollAngularSpeed = 180f;
[Min(0f)]
public float rollLerpAcceleration = 5f;
float pitchModifier = 1f;
float rollModifier = 1f;
Vector3 targetVerticalVelocity;
float pitchValue;
float rollValue;
protected override void Awake()
{
base.Awake();
pitchModifier = -(invertPitch ? 1f : -1f);
rollModifier = invertRoll ? 1f : -1f;
}
public override void EnterBehaviour(float dt, CharacterState fromState)
{
CharacterActor.alwaysNotGrounded = true;
CharacterActor.UseRootMotion = false;
CharacterActor.constraintRotation = false;
targetVerticalVelocity = CharacterActor.VerticalVelocity;
}
public override void UpdateBehaviour(float dt)
{
ProcessRotation(dt);
ProcessVelocity(dt);
}
private void ProcessRotation(float dt)
{
pitchValue = Mathf.Lerp(pitchValue, pitchModifier * CharacterActions.pitch.value * pitchAngularSpeed * dt, pitchLerpAcceleration * dt);
rollValue = Mathf.Lerp(rollValue, rollModifier * CharacterActions.roll.value * rollAngularSpeed * dt, rollLerpAcceleration * dt);
CharacterActor.RotatePitch(pitchValue, CharacterActor.Center);
CharacterActor.RotateRoll(rollValue, CharacterActor.Center);
Vector3 forward = Vector3.Lerp(CharacterActor.Forward, Vector3.ProjectOnPlane(CharacterStateController.ExternalReference.forward, CharacterActor.Up), 5f * dt);
CharacterActor.SetYaw(forward);
}
private void ProcessVelocity(float dt)
{
Vector3 targetVelocity = CharacterStateController.InputMovementReference * baseSpeed;
CharacterActor.Velocity = Vector3.MoveTowards(CharacterActor.Velocity, targetVelocity, (CharacterActions.movement.Detected ? acceleration : deceleration) * dt);
if (CharacterActions.jump.value)
{
targetVerticalVelocity = CharacterActor.Up * baseSpeed;
CharacterActor.VerticalVelocity = Vector3.MoveTowards(CharacterActor.VerticalVelocity, targetVerticalVelocity, acceleration * dt);
}
else if (CharacterActions.crouch.value)
{
targetVerticalVelocity = -CharacterActor.Up * baseSpeed;
CharacterActor.VerticalVelocity = Vector3.MoveTowards(CharacterActor.VerticalVelocity, targetVerticalVelocity, acceleration * dt);
}
}
}
}