#if !NEOFPS_FORCE_QUALITY && (UNITY_ANDROID || UNITY_IOS || UNITY_TIZEN || (UNITY_WSA && NETFX_CORE) || NEOFPS_FORCE_LIGHTWEIGHT) #define NEOFPS_LIGHTWEIGHT #endif using UnityEngine; using NeoFPS.CharacterMotion.MotionData; using NeoSaveGames.Serialization; namespace NeoFPS.CharacterMotion.States { [MotionGraphElement("Ground Movement/Steep Slide", "Slide")] [HelpURL("https://docs.neofps.com/manual/motiongraphref-mgs-steepslidestate.html")] public class SteepSlideState : MotionGraphState { [SerializeField, Tooltip("The angle above which a character loses motor control and is in pure slide mode.")] private FloatDataReference m_SlideAngle = new FloatDataReference(60f); [SerializeField, Tooltip("The sliding speed the character will reach (downwards only) at the lowest slope angle for a full slide.")] private FloatDataReference m_SpeedMinimum = new FloatDataReference(25f); [SerializeField, Tooltip("The fastest possible sliding speed the character can reach (downwards only) during a near vertical slide.")] private FloatDataReference m_SpeedMaximum = new FloatDataReference(40f); [SerializeField, Tooltip("The down-slope acceleration multiplier applied to a character during a shallow slide.")] private FloatDataReference m_AccelerationMinimum = new FloatDataReference(0.25f); [SerializeField, Tooltip("The down-slope acceleration multiplier applied to a character during a near vertical slide.")] private FloatDataReference m_AccelerationMaximum = new FloatDataReference(0.75f); [SerializeField, Tooltip("The top speed the character can reach against the slide (side to side).")] private FloatDataReference m_HorizontalSpeedLimit = new FloatDataReference(5f); [SerializeField, Tooltip("The across slope accleration when trying to redirect slide sideways (0 is instant).")] private FloatDataReference m_HorizontalAcceleration = new FloatDataReference(10f); private const float k_TinyValue = 0.001f; private Vector3 m_SlideVelocity = Vector3.zero; private float m_VerticalSlideSpeed = 0f; private float m_VerticalSlideAcceleration = 0f; private float m_HorizontalSlideSpeed = 0f; private float m_HorizontalSlideAcceleration = 0f; private float m_SlopeFriction = 0f; public override Vector3 moveVector { get { return m_SlideVelocity * Time.deltaTime; } } public override bool applyGravity { get { return false; } } public override bool applyGroundingForce { get { return true; } } public override void OnValidate() { base.OnValidate(); m_SlideAngle.ClampValue(30f, 80f); m_SpeedMinimum.ClampValue(1f, 50f); m_SpeedMaximum.ClampValue(1f, 50f); m_AccelerationMinimum.ClampValue(0f, 1f); m_AccelerationMaximum.ClampValue(0f, 1f); m_HorizontalSpeedLimit.ClampValue(0f, 20f); m_HorizontalAcceleration.ClampValue(0f, 1000f); } public override void OnEnter() { base.OnEnter(); m_VerticalSlideAcceleration = 0f; m_HorizontalSlideAcceleration = 0f; Vector3 upSlope = Vector3.ProjectOnPlane(characterController.up, characterController.groundSurfaceNormal).normalized; m_VerticalSlideSpeed = Vector3.Dot(characterController.rawVelocity, upSlope); // Remove slope friction m_SlopeFriction = characterController.slopeFriction; characterController.slopeFriction = 0f; } public override void OnExit() { base.OnExit(); m_SlideVelocity = Vector3.zero; // Reset slope friction characterController.slopeFriction = m_SlopeFriction; } public override void Update() { base.Update(); // Get the up slope vector & angle Vector3 up = characterController.up; Vector3 upSlope = Vector3.ProjectOnPlane(up, characterController.groundSurfaceNormal).normalized; float slopeAngle = Vector3.Angle(up, characterController.groundSurfaceNormal); // Calculate the scale of the slide (0 to 1 = starting slide angle to straight down) float slopeMaxAngle = m_SlideAngle.value; float slideScale = Mathf.Clamp01 ((slopeAngle - slopeMaxAngle) / (90f - slopeMaxAngle)); // Calculate target slide speed & acceleration based on slope float targetVerticalSlideSpeed = Mathf.Lerp(m_SpeedMinimum.value, m_SpeedMaximum.value, slideScale); float accelerationMultiplier = Mathf.Lerp(m_AccelerationMinimum.value, m_AccelerationMaximum.value, slideScale); // Update the current down-slope slide speed m_VerticalSlideSpeed = -Vector3.Dot (characterController.velocity, upSlope); // Prevent players using steep slopes to boost jumps (due to slower downward acceleration than gravity) if (m_VerticalSlideSpeed < 0f) { var upVel = Vector3.Project(characterController.velocity, up); m_VerticalSlideSpeed = -Vector3.Dot(upVel, upSlope); accelerationMultiplier += 1f; } // Accelerate down-slope towards slide speed if (!Mathf.Approximately(m_VerticalSlideSpeed, targetVerticalSlideSpeed)) m_VerticalSlideSpeed = Mathf.SmoothDamp(m_VerticalSlideSpeed, targetVerticalSlideSpeed, ref m_VerticalSlideAcceleration, 0.01f, -Vector3.Dot(characterController.gravity, characterController.up) * accelerationMultiplier); m_VerticalSlideSpeed = Mathf.Clamp(m_VerticalSlideSpeed, -targetVerticalSlideSpeed, targetVerticalSlideSpeed); // Apply down-slope to move vector m_SlideVelocity = upSlope * -m_VerticalSlideSpeed; // Get the across slope vector Vector3 acrossSlope = Vector3.Cross(up, upSlope).normalized; // Check if across slope control is enabled float hSpeedLimit = m_HorizontalSpeedLimit.value; if (hSpeedLimit > 0f) { // Update the current across-slope slide speed m_HorizontalSlideSpeed = Vector3.Dot (characterController.velocity, acrossSlope); // Get the target across-slope speed (based on look & input) float targetHorizontalSlideSpeed = 0f; if (controller.inputMoveScale > 0f) { Vector3 inputTarget = Vector3.zero; inputTarget += controller.localTransform.forward * controller.inputMoveDirection.y; inputTarget += controller.localTransform.right * controller.inputMoveDirection.x; float acrossInput = Vector3.Dot(inputTarget, acrossSlope); targetHorizontalSlideSpeed = acrossInput * hSpeedLimit; } // Accelerate across-slope towards target speed (positive = rhs) float hAcceleration = m_HorizontalAcceleration.value; if (hAcceleration < k_TinyValue) { // Don't use acceleration (instant) m_HorizontalSlideSpeed = targetHorizontalSlideSpeed; } else { // Accelerate if required if (!Mathf.Approximately (m_HorizontalSlideSpeed, targetHorizontalSlideSpeed)) m_HorizontalSlideSpeed = Mathf.SmoothDamp (m_HorizontalSlideSpeed, targetHorizontalSlideSpeed, ref m_HorizontalSlideAcceleration, 0.1f, hAcceleration); } // Apply to move vector m_SlideVelocity += acrossSlope * m_HorizontalSlideSpeed; } } public override void CheckReferences(IMotionGraphMap map) { base.CheckReferences(map); m_SlideAngle.CheckReference(map); m_SpeedMinimum.CheckReference(map); m_SpeedMaximum.CheckReference(map); m_AccelerationMinimum.CheckReference(map); m_AccelerationMaximum.CheckReference(map); m_HorizontalSpeedLimit.CheckReference(map); m_HorizontalAcceleration.CheckReference(map); } #region SAVE / LOAD private static readonly NeoSerializationKey k_FrictionKey = new NeoSerializationKey("friction"); private static readonly NeoSerializationKey k_SlideKey = new NeoSerializationKey("slide"); private static readonly NeoSerializationKey k_VelocityKey = new NeoSerializationKey("velocity"); public override void WriteProperties(INeoSerializer writer) { base.WriteProperties(writer); writer.WriteValue(k_VelocityKey, m_SlideVelocity); writer.WriteValue(k_FrictionKey, m_SlopeFriction); writer.WriteValue(k_SlideKey, new Vector4(m_VerticalSlideSpeed, m_VerticalSlideAcceleration, m_HorizontalSlideSpeed, m_HorizontalSlideAcceleration)); } public override void ReadProperties(INeoDeserializer reader) { base.ReadProperties(reader); reader.TryReadValue(k_VelocityKey, out m_SlideVelocity, m_SlideVelocity); reader.TryReadValue(k_FrictionKey, out m_SlopeFriction, m_SlopeFriction); Vector4 slide; if (reader.TryReadValue(k_SlideKey, out slide, Vector4.zero)) { m_VerticalSlideSpeed = slide.x; m_VerticalSlideAcceleration = slide.y; m_HorizontalSlideSpeed = slide.z; m_HorizontalSlideAcceleration = slide.w; } } #endregion } }