#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.Parameters; using NeoFPS.CharacterMotion.MotionData; using NeoSaveGames.Serialization; using NeoCC; namespace NeoFPS.CharacterMotion.States { [MotionGraphElement("Ladders/Contact Ladder", "Ladder")] [HelpURL("https://docs.neofps.com/manual/motiongraphref-mgs-contactladderstate.html")] public class ContactLadderState : MotionGraphState { [SerializeField, Tooltip("The transform property on the graph that is used to attach to ladder transforms in the scene.")] private TransformParameter m_TransformParameter = null; [SerializeField, Tooltip("The maximum climb speed (based on input and aim)")] private FloatDataReference m_ClimbSpeed = new FloatDataReference(2f); [SerializeField, Tooltip("The top movement speed with ground under foot (top and bottom of the ladder)")] private FloatDataReference m_GroundSpeed = new FloatDataReference(5f); [SerializeField, Tooltip("The multiplier applied to the max movement speed when strafing")] private FloatDataReference m_StrafeMultiplier = new FloatDataReference(0.75f); [SerializeField, Tooltip("The multiplier applied to the max movement speed when moving in reverse")] private FloatDataReference m_ReverseMultiplier = new FloatDataReference(0.5f); [SerializeField, Tooltip("The acceleration while climbing the ladder or dismounting")] private FloatDataReference m_Acceleration = new FloatDataReference(50f); [SerializeField, Tooltip("Should the direction of the camera aim influence the vertical move direction? IgnoreAimer = No; AimerAbsolute = Only direction; AimerSmooth = Direction and speed; AimerHeading = Direction and speed based on yaw (uses strafe); AimerAllAxes = Direction and speed based on yaw and pitch (uses strafe)")] private MovementStyle m_UseAimerV = MovementStyle.AimerAbsolute; [SerializeField, Tooltip("For AimerAbsolute, the angle past the horizontal you need to aim to flip directions. For AimerSmooth or AimerAllAxes, the angle past the horizontal that reaches full speed.")] private float m_CenterZone = 15f; [SerializeField, Tooltip("Should the aimer influence the horizontal move direction? IgnoreAimer = No; AimerAbsolute = Looking away switches axes; AimerSmooth = Direction and speed; AimerHeading/AimerAllAxes = As AimerSmooth, but forward / back input counts also.")] private MovementStyle m_UseAimerH = MovementStyle.AimerSmooth; // NOTES: // - Could also grab the jump trigger property and use it to correctly jump away from the ladder // - Could make single sided by setting complete / null as soon as the character passes behind the ladder // - This would mean 1 frame hitch of being in ladder state (just maintain velocity?) // - Could inherit from standard movement to for a better version of the grounded move private const float k_TinyValue = 0.001f; private ILadder m_Ladder = null; private Vector3 m_OutVelocity = Vector3.zero; private Vector3 m_MotorAcceleration = Vector3.zero; private Vector3 m_RadiusOffset = Vector3.zero; private float m_Radius = 0f; private float m_QuarterCircleLength = 0f; private float m_SinDeadzone = 0f; private bool m_LookingUp = false; private bool m_WasUpLocked = false; public enum MovementStyle { IgnoreAimer, // W/S = up/down OR A/D = left/right AimerAbsolute, // Direction of aim changes direction of move AimerSmooth, // Direction of aim changes direction and speed of move AimerHeading, // Direction and speed based on yaw only (factors in both input axes) AimerAllAxes // Direction and speed based on yaw and pitch (factors in both input axes) } public override Vector3 moveVector { get { return m_OutVelocity * Time.deltaTime; } } public override bool applyGravity { get { return false; } } public override bool applyGroundingForce { get { return false; } } public override bool ignorePlatformMove { get { return true; } } public override bool ignoreExternalForces { get { return false; } // Simple ladder does not lock you in place } public override bool completed { get { return m_Ladder == null || m_TransformParameter.value == null; } } #if UNITY_EDITOR public override void OnValidate() { base.OnValidate(); m_GroundSpeed.ClampValue(0.1f, 50f); m_StrafeMultiplier.ClampValue(0f, 2f); m_ReverseMultiplier.ClampValue(0f, 2f); m_Acceleration.ClampValue(0f, 1000f); if (m_UseAimerV == MovementStyle.AimerSmooth) m_CenterZone = Mathf.Clamp(m_CenterZone, 5f, 90f); else m_CenterZone = Mathf.Clamp(m_CenterZone, 0f, 60f); } #endif public override void OnEnter() { base.OnEnter(); m_MotorAcceleration = Vector3.zero; // Get ladder component from transform in property if (m_TransformParameter != null && m_TransformParameter.value != null) m_Ladder = m_TransformParameter.value.GetComponent(); // Get character radius etc m_Radius = characterController.radius + 0.05f; m_RadiusOffset = new Vector3(0f, m_Radius, 0f); m_QuarterCircleLength = 0.5f * Mathf.PI * m_Radius; // Get relative position, etc Quaternion correction = Quaternion.Inverse((m_Ladder as MonoBehaviour).transform.rotation); Vector3 relativePosition = correction * (controller.localTransform.position + m_RadiusOffset - m_Ladder.worldTop); //float aimPitchRad = (correction * controller.aimController.rotation).eulerAngles.x * Mathf.Deg2Rad; // Check if at top of ladder if (relativePosition.y > 0f) { // Get the angle from forwards (don't just use trig as the character is not guaranteed to be on the surface) float angle = Vector3.Angle(Vector3.forward, new Vector3(0f, relativePosition.y, relativePosition.z)); // Wrap the position relativePosition.y = m_Radius * angle * Mathf.Deg2Rad; // Set to mount and block dismount until complete //aimPitchRad -= angle * Mathf.Deg2Rad; // Check if the character is looking up the ladder Vector3 wrappedUp = Quaternion.AngleAxis(angle, m_Ladder.across) * m_Ladder.up; m_LookingUp = Vector3.Dot(controller.aimController.forward, wrappedUp) > 0f; } else { // Check if the character is looking up the ladder m_LookingUp = Vector3.Dot(controller.aimController.forward, m_Ladder.up) > 0f; } // Calculate the deadzone for aiming m_SinDeadzone = Mathf.Sin(m_CenterZone * Mathf.Deg2Rad); // Make sure the character doesn't rotate on the ladder var variableUp = characterController as INeoCharacterVariableGravity; if (variableUp != null) { m_WasUpLocked = variableUp.lockUpVector; variableUp.lockUpVector = true; } } public override void OnExit() { base.OnExit(); m_OutVelocity = Vector3.zero; m_Ladder = null; // Allow the character to change up vector again var variableUp = characterController as INeoCharacterVariableGravity; if (variableUp != null) variableUp.lockUpVector = m_WasUpLocked; } public override void Update() { base.Update(); if (completed) return; float maxClimbSpeed = m_ClimbSpeed.value; // Get the ladder rotations Quaternion rotation = m_Ladder.localTransform.rotation; Quaternion correction = Quaternion.Inverse(rotation); // Get the character details relative to the ladder Vector3 relativePosition = correction * (controller.localTransform.position + m_RadiusOffset - m_Ladder.worldTop); Vector3 aimForward = correction * controller.aimController.forward; Vector3 aimHeading = correction * controller.aimController.heading; // Check if at the top of the ladder and wrap around if (relativePosition.y > 0f) { // Get the angle from forwards (don't just use trig as the character is not guaranteed to be on the surface) float angle = Vector3.Angle(Vector3.forward, new Vector3(0f, relativePosition.y, relativePosition.z)); if (angle > 90f) angle = 90f; // Wrap vectors around Quaternion wrapRotation = Quaternion.AngleAxis(angle, Vector3.right); aimForward = wrapRotation * aimForward; relativePosition.y = m_Radius * angle * Mathf.Deg2Rad; } // Get dot products of character aim against axes Vector3 aimDots = new Vector3( Vector3.Dot(aimHeading, Vector3.right), Vector3.Dot(aimForward, Vector3.up), Vector3.Dot(aimHeading, Vector3.forward) ); // Get vertical movement Vector3 targetVelocity = Vector3.zero; switch (m_UseAimerV) { case MovementStyle.IgnoreAimer: { targetVelocity.y += controller.inputMoveDirection.y * maxClimbSpeed; break; } case MovementStyle.AimerAbsolute: { // Check if looking up (with deadzone) if (m_CenterZone <= Mathf.Epsilon) { // No deadzone m_LookingUp = aimDots.y >= 0f; } else { // Only flip if outside deadzone if (m_LookingUp && aimDots.y < -m_SinDeadzone) m_LookingUp = false; if (!m_LookingUp && aimDots.y > m_SinDeadzone) m_LookingUp = true; } // Get the climb speed float speed = controller.inputMoveDirection.y * maxClimbSpeed; if (!m_LookingUp) speed *= -1f; targetVelocity.y += speed; break; } case MovementStyle.AimerSmooth: { // Get the aim speed based on center zone float aimAngle = Mathf.Asin(aimDots.y) * Mathf.Rad2Deg; float aimSpeed = Mathf.Clamp(aimAngle / m_CenterZone, -1f, 1f); // Get the climb speed float speed = controller.inputMoveDirection.y * maxClimbSpeed * aimSpeed; targetVelocity.y += speed; break; } case MovementStyle.AimerHeading: { // Get the climb speed (W/S) float speed = controller.inputMoveDirection.y * maxClimbSpeed * Mathf.Abs(aimDots.z); // Get the climb speed (A/D) speed += controller.inputMoveDirection.x * maxClimbSpeed * aimDots.x; targetVelocity.y += speed; break; } case MovementStyle.AimerAllAxes: { // Get the aim speed based on center zone float aimAngle = Mathf.Asin(aimDots.y) * Mathf.Rad2Deg; float aimSpeed = Mathf.Clamp(aimAngle / m_CenterZone, -1f, 1f); // Get the climb speed (W/S) float speed = controller.inputMoveDirection.y * maxClimbSpeed * Mathf.Abs(aimDots.z) * aimSpeed; // Get the climb speed (A/D) speed += controller.inputMoveDirection.x * maxClimbSpeed * aimDots.x * aimSpeed; targetVelocity.y += speed; break; } } // Check if at the base of the ladder and moving down, and "walk" as normal if so. if (relativePosition.y < 0f && targetVelocity.y < 0f && IsCharacterGrounded()) { SimpleBaseWalk(); return; } // Get lateral movement switch (m_UseAimerH) { case MovementStyle.IgnoreAimer: { targetVelocity.x -= controller.inputMoveDirection.x * maxClimbSpeed * m_StrafeMultiplier.value; break; } case MovementStyle.AimerAbsolute: { // Get the climb speed float speed = controller.inputMoveDirection.x * maxClimbSpeed; if (aimDots.z < 0f) speed *= -1f; targetVelocity.x += speed; break; } case MovementStyle.AimerSmooth: { // Get the climb speed float speed = controller.inputMoveDirection.x * maxClimbSpeed * aimDots.z; targetVelocity.x += speed; break; } case MovementStyle.AimerHeading: { // Get the climb speed (W/S) float speed = controller.inputMoveDirection.y * maxClimbSpeed * aimDots.x; // Get the climb speed (A/D) speed += controller.inputMoveDirection.x * maxClimbSpeed * aimDots.z; targetVelocity.x += speed; break; } case MovementStyle.AimerAllAxes: { // Get the climb speed (W/S) float speed = controller.inputMoveDirection.y * maxClimbSpeed * aimDots.x; // Get the climb speed (A/D) speed += controller.inputMoveDirection.x * maxClimbSpeed * aimDots.z; // Scale based on aim speed *= 1f - Mathf.Abs(aimDots.y) * 0.75f; targetVelocity.x += speed; break; } } // Get the target world position relativePosition += targetVelocity * Time.deltaTime; // Check if off top and detach if so if (relativePosition.y > m_QuarterCircleLength && targetVelocity.y > 0.25f * maxClimbSpeed) { m_TransformParameter.value = null; } if (relativePosition.y > 0f) { // Get overrun from top of curve float remainder = relativePosition.y - m_QuarterCircleLength; // Get angle float angle = relativePosition.y / (Mathf.Deg2Rad * m_Radius); angle = Mathf.Clamp(angle, 0f, 90f); angle *= Mathf.Deg2Rad; // relativePosition = Quaternion.AngleAxis(angle, Vector3.left) * relativePosition; relativePosition.y = Mathf.Sin(angle) * m_Radius; // Add overrun back on if (remainder > 0f) relativePosition.z = -remainder; else relativePosition.z = Mathf.Cos(angle) * m_Radius; } else { relativePosition.z = m_Radius; } Vector3 targetWorldPosition = m_Ladder.worldTop + (rotation * relativePosition); Vector3 targetWorldVelocity = Vector3.ClampMagnitude((targetWorldPosition - (controller.localTransform.position + m_RadiusOffset)) / Time.deltaTime, maxClimbSpeed); float accel = m_Acceleration.value; if (accel < k_TinyValue) m_OutVelocity = targetWorldVelocity; else m_OutVelocity = Vector3.SmoothDamp(characterController.velocity, targetWorldVelocity, ref m_MotorAcceleration, 0.01f, accel); float mag = m_OutVelocity.magnitude; if (mag > maxClimbSpeed) { float ratio = maxClimbSpeed / mag; m_OutVelocity *= ratio; } } private void SimpleBaseWalk() { //m_TransformParameter.value = null; //return; // Update the current velocity Vector3 currentVelocity = characterController.velocity; currentVelocity = Vector3.ProjectOnPlane(currentVelocity, characterController.up); // Calculate speed based on move direction float directionMultiplier = 1f; if (controller.inputMoveDirection.y < 0f) directionMultiplier *= Mathf.Lerp(1f, m_ReverseMultiplier.value, -controller.inputMoveDirection.y); directionMultiplier *= Mathf.Lerp(1f, m_StrafeMultiplier.value, Mathf.Abs(controller.inputMoveDirection.x)); // Get target velocity float groundSpeed = m_GroundSpeed.value; Vector3 targetVelocity = Vector3.zero; targetVelocity += controller.localTransform.forward * controller.inputMoveDirection.y * groundSpeed * directionMultiplier; targetVelocity += controller.localTransform.right * controller.inputMoveDirection.x * groundSpeed * directionMultiplier; // Accelerate if required if (targetVelocity != currentVelocity) { // Get maximum acceleration float maxAccel = m_Acceleration.value * directionMultiplier; // Accelerate the velocity m_OutVelocity = Vector3.SmoothDamp(currentVelocity, targetVelocity, ref m_MotorAcceleration, 0.01f, maxAccel); } // Groundiness if (characterController.velocity.y < m_OutVelocity.y)// 0f) m_OutVelocity.y = characterController.velocity.y; } private bool IsCharacterGrounded() { return characterController.isGrounded && Vector3.Angle(characterController.up, characterController.groundNormal) < characterController.slopeLimit; } public override void CheckReferences(IMotionGraphMap map) { base.CheckReferences(map); m_TransformParameter = map.Swap(m_TransformParameter); m_ClimbSpeed.CheckReference(map); m_GroundSpeed.CheckReference(map); m_StrafeMultiplier.CheckReference(map); m_ReverseMultiplier.CheckReference(map); m_Acceleration.CheckReference(map); } #region SAVE / LOAD private static readonly NeoSerializationKey k_LookingUpKey = new NeoSerializationKey("lookingUp"); private static readonly NeoSerializationKey k_WasUpLockedKey = new NeoSerializationKey("wasUpLocked"); private static readonly NeoSerializationKey k_AccelerationKey = new NeoSerializationKey("acceleration"); private static readonly NeoSerializationKey k_VelocityKey = new NeoSerializationKey("velocity"); public override void WriteProperties(INeoSerializer writer) { base.WriteProperties(writer); writer.WriteValue(k_LookingUpKey, m_LookingUp); writer.WriteValue(k_WasUpLockedKey, m_WasUpLocked); writer.WriteValue(k_AccelerationKey, m_MotorAcceleration); writer.WriteValue(k_VelocityKey, m_OutVelocity); } public override void ReadProperties(INeoDeserializer reader) { Debug.Log("base.ReadProperties"); base.ReadProperties(reader); Debug.Log("ReadProperties internal"); reader.TryReadValue(k_LookingUpKey, out m_LookingUp, m_LookingUp); reader.TryReadValue(k_WasUpLockedKey, out m_WasUpLocked, m_WasUpLocked); reader.TryReadValue(k_AccelerationKey, out m_MotorAcceleration, m_MotorAcceleration); reader.TryReadValue(k_VelocityKey, out m_OutVelocity, m_OutVelocity); Debug.Log("ReadProperties done"); } #endregion } }