/// --------------------------------------------- /// 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; /// /// The Jump ability allows the character to jump into the air. Jump is only active when the character has a positive y velocity. /// [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)); } } /// /// Initialize the default values. /// public override void Awake() { base.Awake(); m_Handler = m_GameObject.GetCachedComponent(); EventHandler.RegisterEvent(m_GameObject, "OnCharacterGrounded", OnGrounded); EventHandler.RegisterEvent(m_GameObject, "OnAnimatorJump", ApplyJumpForce); } /// /// Can the ability be started? /// /// True if the ability can be started. 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; } /// /// 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. /// /// The ability that is currently active. /// True if the ability should be stopped. public override bool ShouldStopActiveAbility(Ability activeAbility) { if (activeAbility is Fall) { return true; } return base.ShouldStopActiveAbility(activeAbility); } /// /// The ability has started. /// 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(); 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(); 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(); } /// /// The character has either landed or just left the ground. /// /// Is the character on the ground? 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; } } /// /// Update the Animator for the ability. /// public override void UpdateAnimator() { // Set the Float Data parameter for the blend tree. if (m_Jumping) { SetAbilityFloatDataParameter(m_CharacterLocomotion.LocalLocomotionVelocity.y); } } /// /// The character should start the jump. /// 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); } } /// /// 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. /// private void EnsureAirborne() { if (!m_CharacterLocomotion.Grounded) { return; } StopAbility(true); } /// /// The user is no longer holding the jump button down. /// private void OnReleaseHold() { m_ApplyHoldForce = false; } /// /// The ability should perform a airborne jump. /// 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(); } /// /// Allows for the Jump ability to add an extra force. /// 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); } } /// /// Can the ability be stopped? /// /// True if the ability can be stopped. 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; } /// /// The ability has stopped running. /// /// Was the ability force stopped? 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); } /// /// 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. /// /// The ability that is starting. /// True if the ability should be blocked. public override bool ShouldBlockAbilityStart(Ability startingAbility) { return startingAbility is HeightChange; } /// /// The GameObject has been destroyed. /// public override void OnDestroy() { base.OnDestroy(); EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorJump", ApplyJumpForce); EventHandler.UnregisterEvent(m_GameObject, "OnCharacterGrounded", OnGrounded); } } }