using UnityEngine;
using MalbersAnimations.Scriptables;
using UnityEngine.Serialization;
namespace MalbersAnimations.Controller
{
[HelpURL("https://malbersanimations.gitbook.io/animal-controller/main-components/manimal-controller/states/fall")]
public class Fall : State
{
public override string StateName => "Fall";
public enum FallBlending { DistanceNormalized, Distance, VerticalVelocity }
/// Air Resistance while falling
[Header("Fall Parameters")]
[Tooltip("Can the Animal be controller while falling?")]
public BoolReference AirControl = new BoolReference(true);
[Tooltip("Rotation while falling")]
public FloatReference AirRotation = new FloatReference(10);
[Tooltip("Maximum Movement while falling")]
public FloatReference AirMovement = new FloatReference(0);
[Tooltip("Lerp value for the Air Movement adjusment")]
public FloatReference AirSmooth = new FloatReference(2);
[Space]
[Tooltip("Forward Offset Position of the Fall Ray")]
public FloatReference Offset = new FloatReference();
[Tooltip("Forward Offset Multiplier Position of the Fall Ray while moving")]
[FormerlySerializedAs("FallRayForward")]
public FloatReference MoveMultiplier = new FloatReference(0.1f);
[Tooltip("Multiplier for the Fall Ray Length. The Default Value is the Animal's Height")]
[FormerlySerializedAs("fallRayMultiplier")]
public FloatReference lengthMultiplier = new FloatReference(1f);
[Tooltip("RayHits Allowed on the Raycast NonAloc (Try Fall Logic)")]
public IntReference RayHits = new IntReference(3);
[Tooltip("Decrease the Vertical Impulse when entering the Fall State")]
[FormerlySerializedAs("AirDrag")]
public float UpDrag = 1;
[Space]
public FallBlending BlendFall = FallBlending.DistanceNormalized;
[Tooltip("Used to Set fallBlend to zero before reaching the ground")]
public FloatReference LowerBlendDistance;
/// Distance to Apply a Fall Hard Animation
[Space, Header("Fall Damage")]
public StatID AffectStat;
[Tooltip("Minimum Distance to Apply a Soft Land Animation")]
public FloatReference FallMinDistance = new FloatReference(5f);
[Tooltip("Maximun Distance to Apply a Hard Land Animation")]
public FloatReference FallMaxDistance = new FloatReference(15f);
[Tooltip("The Fall State will set the Exit State Status Depending the Fall Distance (X: Distance Y:Exit Status Value)")]
#if UNITY_2020_3_OR_NEWER
[NonReorderable]
#endif
public Vector2[] landStatus;
[Tooltip("Fix the animal when is stuck on weird places (Experimental)")]
public bool StuckAnimal = true;
[Tooltip("When Falling, the animal may get stuck falling. The animal will be force to move forward.")]
public FloatReference PushForward = new FloatReference(2);
/// Stores the max heigth before going Down
public float MaxHeight { get; set; }
/// Acumulated Fall Distance
public float FallCurrentDistance { get; set; }
protected Vector3 fall_Point;
private RaycastHit[] FallHits;
private RaycastHit FallRayCast;
private GameObject GameObjectHit;
private bool IsDebree;
/// While Falling this is the distance to the ground
private float DistanceToGround;
/// Normalized Value of the Height
float Fall_Float;
public Vector3 UpImpulse { get; set; }
private MSpeed FallSpeed = MSpeed.Default;
public Vector3 FallPoint { get; private set; }
/// UP Impulse was going UP
public bool Has_UP_Impulse { get; private set; }
private bool GoingDown;
private int Hits;
public override void AwakeState()
{
base.AwakeState();
animalStats = animal.FindComponent(); //Find the Stats
}
public override bool TryActivate()
{
float SprintMultiplier = (animal.VerticalSmooth);
var fall_Pivot = animal.Main_Pivot_Point + (animal.Forward * Offset * ScaleFactor) +
(animal.Forward * SprintMultiplier * MoveMultiplier * ScaleFactor); //Calculate ahead the falling ray
fall_Pivot += animal.DeltaPos; //Check for the Next Frame
float Multiplier = animal.Pivot_Multiplier * lengthMultiplier;
return TryFallRayCasting(fall_Pivot, Multiplier);
}
private bool TryFallRayCasting(Vector3 fall_Pivot, float Multiplier)
{
FallHits = new RaycastHit[RayHits];
var Direction = animal.TerrainSlope < 0 ? Gravity : -transform.up;
var Radius = animal.RayCastRadius * ScaleFactor;
Hits = Physics.SphereCastNonAlloc(fall_Pivot, Radius, Direction, FallHits, Multiplier, GroundLayer, IgnoreTrigger);
if (debug)
{
Debug.DrawRay(fall_Pivot, Direction * Multiplier, Color.magenta);
Debug.DrawRay(FallRayCast.point, FallRayCast.normal * ScaleFactor * 0.2f, Color.magenta);
}
var TerrainSlope = 0f;
if (Hits > 0)
{
if (animal.Grounded) //Check when its grounded
{
foreach (var hit in FallHits)
{
if (hit.collider != null)
{
TerrainSlope = Vector3.SignedAngle(hit.normal, animal.UpVector, animal.Right);
MTools.DrawWireSphere(fall_Pivot + Direction * DistanceToGround, Color.magenta, Radius);
FallRayCast = hit;
if (TerrainSlope > -animal.maxAngleSlope) //Check for the first Good Fall Ray that does not break the Fall.
break;
}
}
if (FallRayCast.transform.gameObject != GameObjectHit) //Check if what the Fall Ray Hit was a Debree
{
GameObjectHit = FallRayCast.transform.gameObject;
IsDebree = GameObjectHit.CompareTag(animal.DebrisTag);
}
if (animal.DeepSlope || (TerrainSlope < -animal.maxAngleSlope && !IsDebree))
{
Debugging($"[Try] Slope is too deep [{FallRayCast.collider.transform.name}] | Hits: {Hits} | Slope: {TerrainSlope:F2}");
return true;
}
}
else //If the Animal is in the air NOT GROUNDED
{
FallRayCast = FallHits[0];
DistanceToGround = FallRayCast.distance;
var FallSlope = Vector3.Angle(FallRayCast.normal, animal.UpVector);
if (FallSlope > animal.maxAngleSlope)
{
Debugging($"[Try] The Animal is on the Air and the angle SLOPE of the ground hitted is too Deep");
return true;
}
if (Height >= DistanceToGround) //If the distance to ground is very small means that we are very close to the ground
{
if (animal.ExternalForce != Vector3.zero) return true; //Hack for external forces
Debugging($"[Try Failed] Distance to the ground is very small means that we are very close to the ground. CHECK IF GROUNDED");
animal.CheckIfGrounded();//Hack IMPORTANT HACK!!!!!!!
return false;
}
}
}
else
{
Debugging($"[Try] There's no Ground beneath the Animal");
return true;
}
//Debug.Log("fa");
return false;
}
public override void Activate()
{
StartingSpeedDirection = animal.ActiveState.Speed_Direction(); //Store the Last Speed Direction.... For AirControl False
base.Activate();
ResetStateValues();
Fall_Float = animal.State_Float;
}
public override void EnterCoreAnimation()
{
SetEnterStatus(0);
UpImpulse = Vector3.Project(animal.DeltaPos, animal.UpVector); //Clean the Vector from Forward and Horizontal Influence
IgnoreLowerStates = false;
var Speed = animal.HorizontalSpeed / ScaleFactor; //Remove the scaleFactor since it will be added later
// var passInertia = true;
if (animal.HasExternalForce)
{
var HorizontalForce = Vector3.ProjectOnPlane(animal.ExternalForce, animal.UpVector); //Remove Horizontal Force
var HorizontalInertia = Vector3.ProjectOnPlane(animal.Inertia, animal.UpVector); //Remove Horizontal Force
var HorizontalSpeed = HorizontalInertia - HorizontalForce;
Speed = HorizontalSpeed.magnitude / ScaleFactor; //Remove the scaleFactor since it will be added later
// passInertia = false;
}
//Remove all Speed if the External Force does not allows it
if (!animal.ExternalForceAirControl) Speed = 0;
FallSpeed = new MSpeed(animal.CurrentSpeedModifier)
{
name = "FallSpeed",
position = Speed,
strafeSpeed = Speed,
animator = 1,
rotation = AirRotation.Value,
lerpPosition = AirSmooth.Value,
lerpStrafe = AirSmooth.Value,
};
//Debug.Log("animal.CurrentSpeedModifier = " + animal.CurrentSpeedModifier.name);
//Debug.Log("animal.CV = " + animal.CurrentSpeedModifier.Vertical.Value);
animal.SetCustomSpeed(FallSpeed, false);
//Disable the Gravity if we are on an external Force (Wind, Spring) //IMPORTANT
if (animal.HasExternalForce && animal.Zone) animal.UseGravity = false;
Has_UP_Impulse = Vector3.Dot(UpImpulse, animal.UpVector) > 0;
//means it was on locomotion or idle //Remove Up Impulse HACK
if (MTools.CompareOR(animal.LastState.ID, 0, 1, StateEnum.Swim, StateEnum.Climb) && Has_UP_Impulse || animal.HasExternalForce)
UpImpulse = Vector3.zero;
CanExit = true; //This for long animation Transitions. Force the Fall State to exit in case if near the ground
}
public override Vector3 Speed_Direction()
{
return AirControl.Value ? (base.Speed_Direction() + animal.ExternalForce).normalized : StartingSpeedDirection;
}
Vector3 StartingSpeedDirection;
private Stats animalStats;
public override void OnStateMove(float deltaTime)
{
if (InCoreAnimation)
{
if (animal.Zone && animal.HasExternalForce) animal.GravityTime = 0; //Reset the gravity when the animal is on a Force Zone.
animal.AdditivePosition += UpImpulse;
if (Has_UP_Impulse)
UpImpulse = Vector3.Lerp(UpImpulse, Vector3.zero, deltaTime * UpDrag); //Clean the Up impulse with air Drag
if (AirControl.Value && AirMovement > 0 && AirMovement > CurrentSpeedPos)
{
if (!animal.ExternalForceAirControl) return;
CurrentSpeedPos = Mathf.Lerp(CurrentSpeedPos, AirMovement, (AirSmooth != 0 ? (deltaTime * AirSmooth) : 1));
}
// if (!CanExit) TryExitState(deltaTime);
}
}
public override void TryExitState(float DeltaTime)
{
var Radius = animal.RayCastRadius * ScaleFactor;
float SprintMultiplier = (animal.VerticalSmooth);
var FallPoint = animal.Main_Pivot_Point + (animal.Forward * Offset * ScaleFactor) +
(animal.Forward * (SprintMultiplier * MoveMultiplier * ScaleFactor)); //Calculate ahead the falling ray
//fall_Pivot += animal.DeltaPos; //Check for the Next Frame
//FallPoint = animal.Main_Pivot_Point;
float DeltaDistance = 0;
GoingDown = Vector3.Dot(DeltaPos, Gravity) > 0; //Check if is falling down
if (GoingDown)
{
DeltaDistance = Vector3.Project(DeltaPos, Gravity).magnitude;
FallCurrentDistance += DeltaDistance;
}
if (animal.debugGizmos && debug)
{
MTools.DrawWireSphere(FallPoint, Color.magenta, Radius);
MTools.DrawWireSphere(FallPoint + Gravity * Height, (Color.red + Color.blue) / 2, Radius);
Debug.DrawRay(FallPoint, Gravity * 100f, Color.magenta);
}
var FoundGround = (Physics.Raycast(FallPoint, Gravity, out FallRayCast, 100f, GroundLayer, IgnoreTrigger));
//var RBMovement = Vector3.ProjectOnPlane(animal.RB.velocity * DeltaTime, animal.UpVector); //This is for Helping on Slopes
//animal.InertiaPositionSpeed = RBMovement;
if (FoundGround)
{
DistanceToGround = FallRayCast.distance;
if (animal.debugGizmos && debug)
{
MTools.DrawWireSphere(FallRayCast.point, (Color.blue + Color.red) / 2, Radius);
MTools.DrawWireSphere(FallPoint, (Color.red), Radius);
}
switch (BlendFall)
{
case FallBlending.DistanceNormalized:
{
var realDistance = DistanceToGround - Height;
if (MaxHeight < realDistance)
{
MaxHeight = realDistance; //get the Highest Distance the first time you touch the ground
Fall_Float = Mathf.Lerp(Fall_Float, 0, DeltaTime * 5); //Small blend in case there's a new ground found
animal.State_SetFloat(Fall_Float); //Blend between High and Low Fall
}
else
{
realDistance -= LowerBlendDistance;
Fall_Float = Mathf.Lerp(Fall_Float, 1 - realDistance / MaxHeight, DeltaTime * 10); //Small blend in case there's a new ground found
animal.State_SetFloat(Fall_Float); //Blend between High and Low Fall
}
}
break;
case FallBlending.Distance:
animal.State_SetFloat(FallCurrentDistance);
break;
case FallBlending.VerticalVelocity:
var UpInertia = Vector3.Project(animal.DeltaPos, animal.UpVector).magnitude; //Clean the Vector from Forward and Horizontal Influence
animal.State_SetFloat(UpInertia / animal.DeltaTime * (GoingDown ? 1 : -1));
break;
default:
break;
}
if (Height > DistanceToGround || ((DistanceToGround - DeltaDistance) < 0)) //Means has touched the ground
{
var FallRayAngle = Vector3.Angle(FallRayCast.normal, animal.UpVector);
var DeepSlope = FallRayAngle > animal.maxAngleSlope;
if (!DeepSlope) //Check if we are not on a deep slope
{
AllowExit();
animal.CheckIfGrounded();
animal.Grounded = true;
animal.UseGravity = false;
var GroundedPos = Vector3.Project(FallRayCast.point - animal.transform.position, Gravity); //IMPORTANT HACk FOR when the Animal is falling to fast
if (DeltaDistance > 0.1f)
{
animal.Teleport_Internal(animal.transform.position + GroundedPos); //SUPER IMPORTANT!!! this is when the Animal is falling from a great height
animal.ResetUPVector(); //IMPORTANT!
}
Debugging($"[Try Exit] (Grounded) + [Terrain Angle ={FallRayAngle}]. DeltaDist: {DeltaDistance:F3}");
animal.InertiaPositionSpeed = Vector3.ProjectOnPlane(animal.RB.velocity * DeltaTime, animal.UpVector); //This is for Helping on Slopes
return;
}
else
{
FallCurrentDistance = 0;
return; //Do not check if the rigidbody has Increase Velocity
}
}
}
ResetRigidbody(DeltaTime, Gravity);
}
public override void ExitState()
{
var status = 0;
if (landStatus != null && landStatus.Length >= 1)
{
foreach (var ls in landStatus)
if (ls.x < FallCurrentDistance) status = (int)ls.y;
}
SetExitStatus(status); //Set the Landing Status!! IMPORTANT for Multiple Landing Animations
if (AffectStat != null && animalStats != null
&& FallCurrentDistance > FallMinDistance.Value && animal.Grounded) //Meaning if we are on the safe minimun distance we do not get damage from falling
{
var StatFallValue = (FallCurrentDistance) * 100 / FallMaxDistance;
animalStats.Stat_ModifyValue(AffectStat, StatFallValue, StatOption.ReduceByPercent);
}
base.ExitState();
}
private void ResetRigidbody(float DeltaTime, Vector3 Gravity)
{
if (StuckAnimal && GoingDown)
{
//Debug.Log("GoinDown");
var RBOldDown = Vector3.Project(animal.RB.velocity, Gravity);
var RBNewDown = Vector3.Project(animal.DesiredRBVelocity, Gravity);
var NewDMagn = RBNewDown.magnitude;
var Old_DMagn = RBOldDown.magnitude;
MTools.Draw_Arrow(animal.Main_Pivot_Point+Forward*0.02f, RBOldDown*0.5f,Color.red);
MTools.Draw_Arrow(animal.Main_Pivot_Point + Forward * 0.04f, RBNewDown*0.5f, Color.green);
ResetCount++;
if (NewDMagn == Old_DMagn) return;
if ( NewDMagn > (Old_DMagn * Old_DMagn) && //New Desired Velocity is greatere
Old_DMagn < 0.1f &&
ResetCount > 5) //5 seems to be good
{
if (animal.DesiredRBVelocity.magnitude > Height)
{
Debugging($"Reset Rigidbody Velocity. Animal may be stuck");
animal.ResetUPVector();
animal.GravityTime = animal.StartGravityTime;
if (PushForward > 0)
animal.InertiaPositionSpeed = animal.Forward * animal.ScaleFactor * DeltaTime * PushForward; //Force going forward HACK
ResetCount = 0;
}
}
}
//else
//{
// //ResetCount = 0;
//}
}
/// This is for cleaning the Ribidbody with unnecesary velocity
private int ResetCount;
public override void ResetStateValues()
{
DistanceToGround = float.PositiveInfinity;
GoingDown = false;
IsDebree = false;
FallSpeed = new MSpeed();
FallRayCast = new RaycastHit();
GameObjectHit = null;
FallHits = new RaycastHit[RayHits];
UpImpulse = Vector3.zero;
MaxHeight = float.NegativeInfinity; //Resets MaxHeight
FallCurrentDistance = 0;
Fall_Float = 0; //IMPORTANT
}
public override void StateGizmos(MAnimal animal)
{
if (!Application.isPlaying)
{
var fall_Pivot = animal.Main_Pivot_Point + (animal.Forward * Offset * animal.ScaleFactor); //Calculate ahead the falling ray
float Multiplier = animal.Pivot_Multiplier * lengthMultiplier;
Debug.DrawRay(fall_Pivot, animal.Gravity.normalized * Multiplier, Color.magenta);
}
}
#if UNITY_EDITOR
/// This is Executed when the Asset is created for the first time
private void Reset()
{
ID = MTools.GetInstance("Fall");
General = new AnimalModifier()
{
RootMotion = false,
AdditivePosition = true,
AdditiveRotation = true,
Grounded = false,
Sprint = false,
OrientToGround = false,
Gravity = true,
CustomRotation = false,
modify = (modifier)(-1),
};
LowerBlendDistance = 0.1f;
MoveMultiplier = 0.1f;
lengthMultiplier = 1f;
FallSpeed.name = "FallSpeed";
//SleepFromState = new System.Collections.Generic.List() { MTools.GetInstance("Fly") };
ExitFrame = false; //IMPORTANT
}
#endif
}
}