429 lines
21 KiB
C#
429 lines
21 KiB
C#
/// ---------------------------------------------
|
|
/// Ultimate Character Controller
|
|
/// Copyright (c) Opsive. All Rights Reserved.
|
|
/// https://www.opsive.com
|
|
/// ---------------------------------------------
|
|
|
|
namespace Opsive.UltimateCharacterController.Character.Abilities
|
|
{
|
|
using Opsive.Shared.Events;
|
|
using Opsive.Shared.Game;
|
|
using Opsive.Shared.Utility;
|
|
using Opsive.UltimateCharacterController.Audio;
|
|
using Opsive.UltimateCharacterController.Input;
|
|
using Opsive.UltimateCharacterController.SurfaceSystem;
|
|
using Opsive.UltimateCharacterController.Utility;
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// The Jump ability allows the character to jump into the air. Jump is only active when the character has a positive y velocity.
|
|
/// </summary>
|
|
[DefaultInputName("Jump")]
|
|
[DefaultStartType(AbilityStartType.ButtonDown)]
|
|
[DefaultStopType(AbilityStopType.Automatic)]
|
|
[DefaultAbilityIndex(1)]
|
|
[DefaultUseRootMotionPosition(AbilityBoolOverride.False)]
|
|
[DefaultUseRootMotionRotation(AbilityBoolOverride.False)]
|
|
public class Jump : Ability
|
|
{
|
|
[Tooltip("Prevents the jump ability from starting if there is an object above the character within the specified distance. Set to -1 to disable.")]
|
|
[SerializeField] protected float m_MinCeilingJumpHeight = 0.05f;
|
|
[Tooltip("The amount of time after the character is airborne that the character can still jump. Also known as coyote time. " +
|
|
"Set to -1 to allow for the character to jump at any time after being airborne.")]
|
|
[SerializeField] protected float m_GroundedGracePeriod = 0.05f;
|
|
[Tooltip("Should the jump be prevented when the character is on a slope greater than the slope limit?")]
|
|
[SerializeField] protected bool m_PrevntSlopeLimitJump = true;
|
|
[Tooltip("The amount of force that should be applied when the character jumps.")]
|
|
[SerializeField] protected float m_Force = 0.22f;
|
|
[Tooltip("A multiplier applied to the force while moving sideways.")]
|
|
[SerializeField] protected float m_SidewaysForceMultiplier = 0.8f;
|
|
[Tooltip("A multiplier applied to the force while moving backwards.")]
|
|
[SerializeField] protected float m_BackwardsForceMultiplier = 0.7f;
|
|
[Tooltip("The number of frames that the force is applied in.")]
|
|
[SerializeField] protected int m_Frames = 1;
|
|
[Tooltip("Determines how quickly the jump force wears off.")]
|
|
[SerializeField] protected float m_ForceDamping = 0.08f;
|
|
[Tooltip("Specifies if the ability should wait for the OnAnimatorJump animation event or wait for the specified duration before applying the jump force.")]
|
|
[SerializeField] protected AnimationEventTrigger m_JumpEvent = new AnimationEventTrigger(true, 0f);
|
|
[Tooltip("The Surface Impact triggered when the character jumps.")]
|
|
[SerializeField] protected SurfaceImpact m_JumpSurfaceImpact;
|
|
[Tooltip("The amount of force to add per frame if the jump button is being held down continuously. This is a common feature for providing increased jump control in platform games.")]
|
|
[SerializeField] protected float m_ForceHold = 0.003f;
|
|
[Tooltip("Determines how quickly the jump hold force wears off.")]
|
|
[SerializeField] protected float m_ForceDampingHold = 0.5f;
|
|
[Tooltip("Specifies the number of times the character can perform a airborne jump (double jump, triple jump, etc). Set to -1 to allow an infinite number of airborne jumps.")]
|
|
[SerializeField] protected int m_MaxAirborneJumpCount;
|
|
[Tooltip("The amount of force that applied when the character performs an airborne jump.")]
|
|
[SerializeField] protected float m_AirborneJumpForce = 0.6f;
|
|
[Tooltip("The number of frames that the repeated jump force is applied in.")]
|
|
[SerializeField] protected int m_AirborneJumpFrames = 10;
|
|
[Tooltip("Contains an array of AudioClips toat can be played when a repeated jump occurs.")]
|
|
[SerializeField] protected AudioClipSet m_AirborneJumpAudioClipSet = new AudioClipSet();
|
|
[Tooltip("A vertical velocity value below the specified amount will stop the ability.")]
|
|
[SerializeField] protected float m_VerticalVelocityStopThreshold = -0.001f;
|
|
[Tooltip("The number of seconds that the jump ability has to wait after it can start again (includes repeated jumps).")]
|
|
[SerializeField] protected float m_RecurrenceDelay = 0.2f;
|
|
|
|
public float MinCeilingJumpHeight { get { return m_MinCeilingJumpHeight; } set { m_MinCeilingJumpHeight = value; } }
|
|
public float GroundedGracePeriod { get { return m_GroundedGracePeriod; } set { m_GroundedGracePeriod = value; } }
|
|
public bool PrevntSlopeLimitJump { get { return m_PrevntSlopeLimitJump; } set { m_PrevntSlopeLimitJump = value; } }
|
|
public float Force { get { return m_Force; } set { m_Force = value; } }
|
|
public float SidewaysForceMultiplier { get { return m_SidewaysForceMultiplier; } set { m_SidewaysForceMultiplier = value; } }
|
|
public float BackwardsForceMultiplier { get { return m_BackwardsForceMultiplier; } set { m_BackwardsForceMultiplier = value; } }
|
|
public int Frames { get { return m_Frames; } set { m_Frames = value; } }
|
|
public float ForceDamping { get { return m_ForceDamping; } set { m_ForceDamping = value; } }
|
|
public AnimationEventTrigger JumpEvent { get { return m_JumpEvent; } set { m_JumpEvent = value; } }
|
|
public SurfaceImpact JumpSurfaceImpact { get { return m_JumpSurfaceImpact; } set { m_JumpSurfaceImpact = value; } }
|
|
public float ForceHold { get { return m_ForceHold; } set { m_ForceHold = value; } }
|
|
public float ForceDampingHold { get { return m_ForceDampingHold; } set { m_ForceDampingHold = value; } }
|
|
public int MaxAirborneJumpCount { get { return m_MaxAirborneJumpCount; } set { m_MaxAirborneJumpCount = value; } }
|
|
public float AirborneJumpForce { get { return m_AirborneJumpForce; } set { m_AirborneJumpForce = value; } }
|
|
public int AirborneJumpFrames { get { return m_AirborneJumpFrames; } set { m_AirborneJumpFrames = value; } }
|
|
public AudioClipSet AirborneJumpAudioClipSet { get { return m_AirborneJumpAudioClipSet; } set { m_AirborneJumpAudioClipSet = value; } }
|
|
public float VerticalVelocityStopThreshold { get { return m_VerticalVelocityStopThreshold; } set { m_VerticalVelocityStopThreshold = value; } }
|
|
public float RecurrenceDelay { get { return m_RecurrenceDelay; } set { m_RecurrenceDelay = value; } }
|
|
|
|
private UltimateCharacterLocomotionHandler m_Handler;
|
|
private ActiveInputEvent m_HoldInput;
|
|
private ActiveInputEvent m_AirborneJumpInput;
|
|
|
|
private RaycastHit m_RaycastResult;
|
|
private bool m_ForceImmediateJump;
|
|
private bool m_JumpApplied;
|
|
private bool m_Jumping;
|
|
private bool m_ApplyHoldForce;
|
|
private float m_HoldForce;
|
|
private int m_AirborneJumpCount;
|
|
private bool m_AirborneJumpApplied;
|
|
private float m_JumpTime = -1;
|
|
private float m_LandTime = -1;
|
|
private float m_InAirTime = -1;
|
|
private bool m_AirborneJumpRegistered;
|
|
|
|
[Snapshot] protected float HoldForce { get { return m_HoldForce; } set { m_HoldForce = value; } }
|
|
[Snapshot] protected bool JumpApplied { get { return m_JumpApplied; } set { m_JumpApplied = value; } }
|
|
[Snapshot] protected bool Jumping { get { return m_Jumping; } set { m_Jumping = value; } }
|
|
public bool ForceImmediateJump { set { m_ForceImmediateJump = value; } }
|
|
|
|
public override float AbilityFloatData { get { if (m_Jumping) { return m_CharacterLocomotion.LocalLocomotionVelocity.y; } return -1; } }
|
|
public override int AbilityIntData { get { return (m_AirborneJumpCount > 0 ? 2 : (m_JumpApplied ? 0 : 1)); } }
|
|
|
|
/// <summary>
|
|
/// Initialize the default values.
|
|
/// </summary>
|
|
public override void Awake()
|
|
{
|
|
base.Awake();
|
|
|
|
m_Handler = m_GameObject.GetCachedComponent<UltimateCharacterLocomotionHandler>();
|
|
|
|
EventHandler.RegisterEvent<bool>(m_GameObject, "OnCharacterGrounded", OnGrounded);
|
|
EventHandler.RegisterEvent(m_GameObject, "OnAnimatorJump", ApplyJumpForce);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Can the ability be started?
|
|
/// </summary>
|
|
/// <returns>True if the ability can be started.</returns>
|
|
public override bool CanStartAbility()
|
|
{
|
|
// An attribute may prevent the ability from starting.
|
|
if (!base.CanStartAbility()) {
|
|
return false;
|
|
}
|
|
|
|
if (m_MinCeilingJumpHeight != -1) {
|
|
// Ensure the space above is clear to get off of the ground.
|
|
if (m_CharacterLocomotion.SingleCast(m_CharacterLocomotion.Up * (m_MinCeilingJumpHeight + m_CharacterLocomotion.SkinWidth + m_CharacterLocomotion.ColliderSpacing),
|
|
Vector3.zero, m_CharacterLayerManager.SolidObjectLayers, ref m_RaycastResult)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
if (m_CharacterLocomotion.Grounded) {
|
|
// The character can't jump if they aren't on the ground nor if they recently landed.
|
|
if (m_LandTime + m_RecurrenceDelay > Time.time) {
|
|
return false;
|
|
}
|
|
|
|
// The character can't jump if the slope is too steep.
|
|
if (m_PrevntSlopeLimitJump) {
|
|
var slope = Vector3.Angle(m_CharacterLocomotion.Up, m_CharacterLocomotion.GroundRaycastHit.normal);
|
|
if (slope > m_CharacterLocomotion.SlopeLimit) {
|
|
return false;
|
|
}
|
|
}
|
|
} else {
|
|
// The airborne jump should play if the character walks off a ledge and did not initially jump.
|
|
if (m_AirborneJumpCount< m_MaxAirborneJumpCount) {
|
|
m_JumpApplied = true;
|
|
return true;
|
|
}
|
|
|
|
// Allow the ability to start if the character is in the air before the grounded grace period. This allows the character to run off a ledge but still
|
|
// be able to jump. The character may also be able to do a repeated jump even if the character isn't grounded.
|
|
if ((!m_JumpApplied && m_GroundedGracePeriod != -1 && m_InAirTime + m_GroundedGracePeriod <= Time.time) ||
|
|
(m_AirborneJumpCount != -1 && m_AirborneJumpCount > m_MaxAirborneJumpCount) || (m_JumpTime != -1 && m_JumpTime + m_RecurrenceDelay > Time.time)) {
|
|
return false;
|
|
}
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when the current ability is attempting to start and another ability is active.
|
|
/// Returns true or false depending on if the active ability should be stopped.
|
|
/// </summary>
|
|
/// <param name="activeAbility">The ability that is currently active.</param>
|
|
/// <returns>True if the ability should be stopped.</returns>
|
|
public override bool ShouldStopActiveAbility(Ability activeAbility)
|
|
{
|
|
if (activeAbility is Fall) {
|
|
return true;
|
|
}
|
|
return base.ShouldStopActiveAbility(activeAbility);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The ability has started.
|
|
/// </summary>
|
|
protected override void AbilityStarted()
|
|
{
|
|
m_ApplyHoldForce = true;
|
|
m_HoldForce = 0;
|
|
|
|
// If the jump has already been applied then it is a repeated jump.
|
|
if (m_JumpApplied) {
|
|
OnAirborneJump();
|
|
} else {
|
|
if (!m_JumpEvent.WaitForAnimationEvent || m_ForceImmediateJump) {
|
|
Scheduler.ScheduleFixed(m_ForceImmediateJump ? 0 : m_JumpEvent.Duration, ApplyJumpForce);
|
|
}
|
|
}
|
|
|
|
if (m_ForceHold > 0) {
|
|
if (m_Handler != null && InputIndex != -1) {
|
|
m_HoldInput = GenericObjectPool.Get<ActiveInputEvent>();
|
|
m_HoldInput.Initialize(ActiveInputEvent.Type.ButtonUp, InputNames[InputIndex], "OnJumpAbilityReleaseHold");
|
|
m_Handler.RegisterInputEvent(m_HoldInput);
|
|
}
|
|
EventHandler.RegisterEvent(m_GameObject, "OnJumpAbilityReleaseHold", OnReleaseHold);
|
|
}
|
|
|
|
// The character can do a repeated jump after the character is already in the air.
|
|
if (!m_AirborneJumpRegistered) {
|
|
if (m_Handler != null && InputIndex != -1) {
|
|
m_AirborneJumpInput = GenericObjectPool.Get<ActiveInputEvent>();
|
|
m_AirborneJumpInput.Initialize(ActiveInputEvent.Type.ButtonDown, InputNames[InputIndex], "OnJumpAbilityAirborneJump");
|
|
m_Handler.RegisterInputEvent(m_AirborneJumpInput);
|
|
}
|
|
EventHandler.RegisterEvent(m_GameObject, "OnJumpAbilityAirborneJump", OnAirborneJump);
|
|
m_AirborneJumpRegistered = true;
|
|
}
|
|
m_ForceImmediateJump = false;
|
|
|
|
base.AbilityStarted();
|
|
}
|
|
|
|
/// <summary>
|
|
/// The character has either landed or just left the ground.
|
|
/// </summary>
|
|
/// <param name="grounded">Is the character on the ground?</param>
|
|
private void OnGrounded(bool grounded)
|
|
{
|
|
if (grounded) {
|
|
if (IsActive) {
|
|
StopAbility(true);
|
|
}
|
|
m_AirborneJumpCount = 0;
|
|
m_JumpApplied = false;
|
|
// Remember the land time to prevent jumping more than the JumpReoccuranceDelay.
|
|
m_LandTime = Time.time;
|
|
m_InAirTime = -1;
|
|
|
|
// Unregister the jump input within OnGrounded so a repeated jump can be applied when fall is active.
|
|
if (m_AirborneJumpInput != null) {
|
|
m_Handler.UnregisterInputEvent(m_AirborneJumpInput);
|
|
GenericObjectPool.Return(m_AirborneJumpInput);
|
|
m_AirborneJumpInput = null;
|
|
}
|
|
EventHandler.UnregisterEvent(m_GameObject, "OnJumpAbilityAirborneJump", OnAirborneJump);
|
|
m_AirborneJumpRegistered = false;
|
|
} else if (!IsActive) {
|
|
m_InAirTime = Time.time;
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Update the Animator for the ability.
|
|
/// </summary>
|
|
public override void UpdateAnimator()
|
|
{
|
|
// Set the Float Data parameter for the blend tree.
|
|
if (m_Jumping) {
|
|
SetAbilityFloatDataParameter(m_CharacterLocomotion.LocalLocomotionVelocity.y);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// The character should start the jump.
|
|
/// </summary>
|
|
private void ApplyJumpForce()
|
|
{
|
|
if (IsActive && !m_JumpApplied) {
|
|
// A surface effect can optionally play when the character leaves the ground.
|
|
if (m_JumpSurfaceImpact != null) {
|
|
SurfaceManager.SpawnEffect(m_CharacterLocomotion.GroundRaycastHit, m_JumpSurfaceImpact, m_CharacterLocomotion.GravityDirection, m_CharacterLocomotion.TimeScale, m_GameObject);
|
|
}
|
|
// Do not set the Jumping variable because the ability should be active for at least one frame. If Jumping was set there is a chance
|
|
// the ability could stop right away if the character jumps while moving down a slope.
|
|
m_JumpApplied = true;
|
|
m_JumpTime = Time.time;
|
|
var force = m_Force;
|
|
// Prevent the character from jumping as high when moving backwards or sideways.
|
|
if (m_CharacterLocomotion.InputVector.y < 0) {
|
|
force *= Mathf.Lerp(1, m_BackwardsForceMultiplier, Mathf.Abs(m_CharacterLocomotion.InputVector.y));
|
|
} else {
|
|
// The character's forward movement will contribute to a full jump force.
|
|
force *= Mathf.Lerp(1, m_SidewaysForceMultiplier, Mathf.Abs(m_CharacterLocomotion.InputVector.x) - Mathf.Abs(m_CharacterLocomotion.InputVector.y));
|
|
}
|
|
AddForce(m_CharacterLocomotion.Up * force, m_Frames, false, true);
|
|
|
|
// Ensure the character is in the air after jumping.
|
|
Scheduler.ScheduleFixed(Time.fixedDeltaTime + .001f, EnsureAirborne);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// After jumping the character should be in the air. If the character is not in the air then another object prevented the character from jumping and the
|
|
/// jump ability should be stopped.
|
|
/// </summary>
|
|
private void EnsureAirborne()
|
|
{
|
|
if (!m_CharacterLocomotion.Grounded) {
|
|
return;
|
|
}
|
|
|
|
StopAbility(true);
|
|
}
|
|
|
|
/// <summary>
|
|
/// The user is no longer holding the jump button down.
|
|
/// </summary>
|
|
private void OnReleaseHold()
|
|
{
|
|
m_ApplyHoldForce = false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The ability should perform a airborne jump.
|
|
/// </summary>
|
|
private void OnAirborneJump()
|
|
{
|
|
if (m_MaxAirborneJumpCount != -1 && m_AirborneJumpCount >= m_MaxAirborneJumpCount) {
|
|
return;
|
|
}
|
|
|
|
// Reset the accumulated gravity to allow for a full airborne jump.
|
|
m_CharacterLocomotion.GravityAmount = 0;
|
|
AddForce(m_CharacterLocomotion.Up * m_AirborneJumpForce, m_AirborneJumpFrames, false, true);
|
|
m_AirborneJumpCount++;
|
|
m_JumpTime = Time.time;
|
|
// The repeated jump may be applied just as the fall ability is about to start. Prevent the jump ability from stopping immediately after starting a repeated jump.
|
|
m_AirborneJumpApplied = true;
|
|
m_AirborneJumpAudioClipSet.PlayAudioClip(m_GameObject);
|
|
m_CharacterLocomotion.UpdateAbilityAnimatorParameters();
|
|
}
|
|
|
|
/// <summary>
|
|
/// Allows for the Jump ability to add an extra force.
|
|
/// </summary>
|
|
public override void UpdatePosition()
|
|
{
|
|
if (!m_Jumping) {
|
|
if (m_JumpApplied) {
|
|
m_Jumping = true;
|
|
}
|
|
return;
|
|
}
|
|
if (m_AirborneJumpApplied) {
|
|
m_AirborneJumpApplied = false;
|
|
}
|
|
|
|
var force = 0f;
|
|
var deltaTime = m_CharacterLocomotion.TimeScaleSquared * Time.timeScale * TimeUtility.FramerateDeltaTime;
|
|
// Continuously apply a damping force while in the air.
|
|
if (m_ForceDamping > 0) {
|
|
var localExternalForce = m_CharacterLocomotion.LocalExternalForce;
|
|
var targetForce = localExternalForce.y / (1 + m_ForceDamping * deltaTime);
|
|
force = (targetForce - localExternalForce.y);
|
|
}
|
|
|
|
// Allow a force and damping to be applied when the input button is held down.
|
|
if (m_ForceHold > 0 && m_ApplyHoldForce) {
|
|
m_HoldForce += m_ForceHold;
|
|
m_HoldForce /= (1 + m_ForceDampingHold * deltaTime);
|
|
force += m_HoldForce;
|
|
}
|
|
|
|
// When the jump force is added it is added to the character's external force. Dampen this external force.
|
|
if (force != 0) {
|
|
AddForce(m_CharacterLocomotion.Up * force, 1, false, false);
|
|
}
|
|
}
|
|
|
|
/// <summary>
|
|
/// Can the ability be stopped?
|
|
/// </summary>
|
|
/// <returns>True if the ability can be stopped.</returns>
|
|
public override bool CanStopAbility()
|
|
{
|
|
// The Jump ability is done if the velocity is less than a the specified value.
|
|
if (m_Jumping && m_CharacterLocomotion.LocalLocomotionVelocity.y <= m_VerticalVelocityStopThreshold && !m_AirborneJumpApplied) {
|
|
return true;
|
|
}
|
|
return false;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The ability has stopped running.
|
|
/// </summary>
|
|
/// <param name="force">Was the ability force stopped?</param>
|
|
protected override void AbilityStopped(bool force)
|
|
{
|
|
base.AbilityStopped(force);
|
|
|
|
m_JumpApplied = m_Jumping = false;
|
|
|
|
// Unregister for the ability input events.
|
|
if (m_HoldInput != null) {
|
|
m_Handler.UnregisterInputEvent(m_HoldInput);
|
|
GenericObjectPool.Return(m_HoldInput);
|
|
}
|
|
EventHandler.UnregisterEvent(m_GameObject, "OnJumpAbilityReleaseHold", OnReleaseHold);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Called when another ability is attempting to start and the current ability is active.
|
|
/// Returns true or false depending on if the new ability should be blocked from starting.
|
|
/// </summary>
|
|
/// <param name="startingAbility">The ability that is starting.</param>
|
|
/// <returns>True if the ability should be blocked.</returns>
|
|
public override bool ShouldBlockAbilityStart(Ability startingAbility)
|
|
{
|
|
return startingAbility is HeightChange;
|
|
}
|
|
|
|
/// <summary>
|
|
/// The GameObject has been destroyed.
|
|
/// </summary>
|
|
public override void OnDestroy()
|
|
{
|
|
base.OnDestroy();
|
|
|
|
EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorJump", ApplyJumpForce);
|
|
EventHandler.UnregisterEvent<bool>(m_GameObject, "OnCharacterGrounded", OnGrounded);
|
|
}
|
|
}
|
|
} |