/// ---------------------------------------------
/// 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.SurfaceSystem;
using Opsive.UltimateCharacterController.Utility;
using UnityEngine;
///
/// The Fall ability allows the character to play a falling animation when the character has a negative y velocity.
///
[DefaultStopType(AbilityStopType.Automatic)]
[DefaultAbilityIndex(2)]
[DefaultUseRootMotionPosition(AbilityBoolOverride.False)]
[DefaultUseRootMotionRotation(AbilityBoolOverride.False)]
public class Fall : Ability
{
[Tooltip("The minimum height between the ground and character that the ability can start. Set to 0 to start at any height.")]
[SerializeField] protected float m_MinFallHeight = 0.2f;
[Tooltip("A reference to the Surface Impact triggered when the character hits the ground.")]
[SerializeField] protected SurfaceImpact m_LandSurfaceImpact;
[Tooltip("The minimum velocity required for the Surface Impact to play.")]
[SerializeField] protected float m_MinSurfaceImpactVelocity = -4f;
[Tooltip("Specifies if the ability should wait for the OnAnimatorFallComplete animation event or wait for the specified duration before ending the fall.")]
[SerializeField] protected AnimationEventTrigger m_LandEvent = new AnimationEventTrigger(true, 0f);
public float MinFallHeight { get { return m_MinFallHeight; } set { m_MinFallHeight = value; } }
public SurfaceImpact LandSurfaceImpact { get { return m_LandSurfaceImpact; } set { m_LandSurfaceImpact = value; } }
public float MinSurfaceImpactVelocity { get { return m_MinSurfaceImpactVelocity; } set { m_MinSurfaceImpactVelocity = value; } }
public AnimationEventTrigger LandEvent { get { return m_LandEvent; } set { m_LandEvent = value; } }
public override int AbilityIntData { get { return m_StateIndex; } }
public override float AbilityFloatData { get { return m_CharacterLocomotion.LocalLocomotionVelocity.y; } }
private int m_StateIndex;
private bool m_Landed;
[Snapshot] protected bool Landed { get { return m_Landed; } set { m_Landed = value; } }
///
/// 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;
}
// Fall can't be started if the character is on the ground.
if (m_CharacterLocomotion.Grounded) {
return false;
}
// The ground distance must be greater then the minimum fall height if a value is set.
RaycastHit hit;
if (m_MinFallHeight != 0 && Physics.Raycast(m_Transform.position, -m_CharacterLocomotion.Up, out hit, m_MinFallHeight, m_CharacterLayerManager.SolidObjectLayers, QueryTriggerInteraction.Ignore)) {
return false;
}
return true;
}
///
/// The ability has started.
///
protected override void AbilityStarted()
{
base.AbilityStarted();
m_StateIndex = 0;
m_Landed = false;
EventHandler.RegisterEvent(m_GameObject, "OnCharacterGrounded", OnGrounded);
EventHandler.RegisterEvent(m_GameObject, "OnAnimatorFallComplete", Land);
EventHandler.RegisterEvent(m_GameObject, "OnCharacterImmediateTransformChange", OnImmediateTransformChange);
}
///
/// 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 character has changed grounded states.
///
/// Is the character on the ground?
private void OnGrounded(bool grounded)
{
if (grounded) {
// Allow the SurfaceManager to play an effect when the character hits the ground.
if (m_LandSurfaceImpact != null && m_CharacterLocomotion.LocalVelocity.y < m_MinSurfaceImpactVelocity) {
SurfaceManager.SpawnEffect(m_CharacterLocomotion.GroundRaycastHit, m_LandSurfaceImpact, m_CharacterLocomotion.GravityDirection, m_CharacterLocomotion.TimeScale, m_GameObject, m_Transform.forward, false);
}
// Move to the fall end state when the character lands.
m_StateIndex = 1;
if (!m_LandEvent.WaitForAnimationEvent) {
Scheduler.ScheduleFixed(m_LandEvent.Duration, Land);
}
} else {
m_StateIndex = 0;
}
}
///
/// Update the Animator for the ability.
///
public override void UpdateAnimator()
{
SetAbilityIntDataParameter(m_StateIndex);
}
///
/// The character has landed.
///
private void Land()
{
m_Landed = true;
// The controller may no longer be grounded during the time that it takes for the land animation to send the OnAnimatorFallComplete event.
if (m_CharacterLocomotion.Grounded) {
StopAbility();
}
}
///
/// Can the ability be stopped?
///
/// True if the ability can be stopped.
public override bool CanStopAbility()
{
return m_Landed;
}
///
/// The character's position or rotation has been teleported.
///
/// Should the animator be snapped?
private void OnImmediateTransformChange(bool snapAnimator)
{
if (!m_CharacterLocomotion.Grounded && !snapAnimator) {
return;
}
// The character is on the ground but fall is still active. Stop the fall.
m_Landed = true;
StopAbility();
}
///
/// The ability has stopped running.
///
/// Was the ability force stopped?
protected override void AbilityStopped(bool force)
{
base.AbilityStopped(force);
EventHandler.UnregisterEvent(m_GameObject, "OnCharacterGrounded", OnGrounded);
EventHandler.UnregisterEvent(m_GameObject, "OnAnimatorFallComplete", Land);
EventHandler.UnregisterEvent(m_GameObject, "OnCharacterImmediateTransformChange", OnImmediateTransformChange);
}
}
}