using NeoFPS.CharacterMotion; using NeoFPS.CharacterMotion.Parameters; using NeoSaveGames; using NeoSaveGames.Serialization; using UnityEngine; namespace NeoFPS { [HelpURL("https://docs.neofps.com/manual/fpcamref-mb-bodylean.html")] public class BodyLean : MonoBehaviour, IAdditiveTransform, INeoSerializableComponent { [SerializeField, Tooltip("The maximum angle the character can lean")] private float m_MaxLeanAngle = 20f; [SerializeField, Tooltip("The vertical offset of the pivot")] private float m_PivotOffset = -1f; [SerializeField, Range(0f, 1f), Tooltip("The speed the character can change lean amount")] private float m_LeanSpeed = 0.4f; [SerializeField, Range(0f, 1f), Tooltip("How much of the lean rotation is reflected in the weapon")] private float m_WeaponTilt = 1f; [SerializeField, Range(0f, 1f), Tooltip("A counter rotation of the head compared to the weapon.")] private float m_HeadCounterTilt = 0.5f; [Header("Clearance")] [SerializeField, Tooltip("The clearance required on the lean side to reach full lean")] private float m_RequiredClearance = 1f; [SerializeField, Tooltip("Should the lean be cancelled if physically blocked")] private bool m_CancelIfBlocked = true; [Header("Motion Graph")] [SerializeField, Tooltip("The maximum speed the character can travel before the lean is cancelled. (0 = no max speed)")] private float m_ResetSpeedStanding = 4f; [SerializeField, Tooltip("The maximum speed the character can travel before the lean is cancelled. (0 = no max speed)")] private float m_ResetSpeedCrouching = 1.5f; [SerializeField, Tooltip("The key to a motion graph switch parameter that dictates if the character can lean or not")] private string m_CanLeanKey = "canLean"; [SerializeField, Tooltip("The key to a motion graph switch parameter that dictates if the character is crouching or standing")] private string m_IsCrouchingKey = "isCrouching"; private static readonly NeoSerializationKey k_CurrentLeanKey = new NeoSerializationKey("currentLean"); private static readonly NeoSerializationKey k_TargetKey = new NeoSerializationKey("targetLean"); private static readonly NeoSerializationKey k_VelocityKey = new NeoSerializationKey("velocity"); private IAdditiveTransformHandler m_Handler = null; private MotionController m_MotionController = null; private SwitchParameter m_CanLeanSwitch = null; private SwitchParameter m_IsCrouchingSwitch = null; private Transform m_ParentTransform = null; private Vector3 m_LeanPosition = Vector3.zero; private Quaternion m_LeanRotation = Quaternion.identity; private RaycastHit s_Hit = new RaycastHit(); private float m_CurrentLean = 0f; private float m_TargetLean = 0f; private float m_LeanVelocity = 0f; private float m_Radius = 0.5f; public Vector3 position { get { return m_LeanPosition; } } public Quaternion rotation { get { return m_LeanRotation; } } public bool bypassPositionMultiplier { get { return true; } } public bool bypassRotationMultiplier { get { return true; } } public float currentLean { get { return -m_CurrentLean; } } public float targetLean { get { return -m_TargetLean; } set { m_TargetLean = -Mathf.Clamp(value, -1f, 1f); } } void OnValidate() { m_MaxLeanAngle = Mathf.Clamp(m_MaxLeanAngle, 1f, 45f); m_PivotOffset = Mathf.Clamp(m_PivotOffset, -2f, 0f); m_RequiredClearance = Mathf.Clamp(m_RequiredClearance, 0f, 2f); m_ResetSpeedCrouching = Mathf.Clamp(m_ResetSpeedCrouching, 0f, 50f); } void Awake() { m_Handler = GetComponent(); m_MotionController = GetComponentInParent(); m_ParentTransform = transform.parent; } void Start() { if (m_MotionController != null) { m_Radius = m_MotionController.characterController.radius; m_CanLeanSwitch = m_MotionController.motionGraph.GetSwitchProperty(m_CanLeanKey); m_IsCrouchingSwitch = m_MotionController.motionGraph.GetSwitchProperty(m_IsCrouchingKey); } if (m_HeadCounterTilt > 0.001f) { var character = GetComponentInParent(); if (character.headTransformHandler != null) { var counter = character.headTransformHandler.gameObject.AddComponent(); counter.AttachToBodyLean(this); character.headTransformHandler.ApplyAdditiveEffect(counter); } } } void OnEnable() { m_Handler.ApplyAdditiveEffect(this); } void OnDisable() { ResetLean(); m_Handler.RemoveAdditiveEffect(this); } public void UpdateTransform() { float constrainedLean = m_TargetLean; if (Mathf.Approximately(constrainedLean, 0f) && (Mathf.Abs(constrainedLean - m_CurrentLean) < 0.001f)) { m_LeanPosition = Vector3.zero; m_LeanRotation = Quaternion.identity; } else { // Check if can lean bool blocked = false; if (m_CanLeanSwitch != null) blocked = !m_CanLeanSwitch.on; if (blocked) { constrainedLean = 0f; m_TargetLean = 0f; } else { // Check if speed limit reached if (m_MotionController != null) { float resetSpeed = (m_IsCrouchingSwitch != null && m_IsCrouchingSwitch.on) ? m_ResetSpeedCrouching : m_ResetSpeedStanding; if (m_MotionController.characterController.velocity.sqrMagnitude > (resetSpeed * resetSpeed)) { constrainedLean = 0f; m_TargetLean = 0f; } } // Check left if (m_RequiredClearance > 0.0001f && m_TargetLean > 0.05f) { if (Physics.Raycast(m_ParentTransform.position, -m_ParentTransform.right, out s_Hit, m_Radius + m_RequiredClearance, PhysicsFilter.Masks.CharacterBlockers, QueryTriggerInteraction.Ignore)) { constrainedLean = (s_Hit.distance - m_Radius) / m_RequiredClearance; if (m_CancelIfBlocked && constrainedLean < 0.05f) { constrainedLean = 0f; m_TargetLean = 0f; } } } // Check right if (m_RequiredClearance > 0.0001f && m_TargetLean < -0.05f) { if (Physics.Raycast(m_ParentTransform.position, m_ParentTransform.right, out s_Hit, m_Radius + m_RequiredClearance, PhysicsFilter.Masks.CharacterBlockers, QueryTriggerInteraction.Ignore)) { constrainedLean = -(s_Hit.distance - m_Radius) / m_RequiredClearance; if (m_CancelIfBlocked && constrainedLean > -0.05f) { constrainedLean = 0f; m_TargetLean = 0f; } } } } // Get damping parameters float maxSpeed = Mathf.Lerp(2.5f, 50f, m_LeanSpeed * m_LeanSpeed); float leanTime = Mathf.Lerp(0.25f, 0.01f, m_LeanSpeed); // Get damped lean m_CurrentLean = Mathf.SmoothDamp(m_CurrentLean, constrainedLean, ref m_LeanVelocity, leanTime, maxSpeed, Time.deltaTime); // Calculate position and rotation m_LeanRotation = Quaternion.Euler(0f, 0f, m_MaxLeanAngle * m_CurrentLean); Vector3 leanPivot = new Vector3(0f, m_PivotOffset, 0f); m_LeanPosition = (m_LeanRotation * -leanPivot) + leanPivot; // Counter tilt if (m_WeaponTilt < 0.99f) m_LeanRotation = Quaternion.Slerp(Quaternion.identity, m_LeanRotation, m_WeaponTilt); } } public void LeanLeft (float amount) { m_TargetLean = amount; } public void LeanRight (float amount) { m_TargetLean = -amount; } public void ResetLean() { m_TargetLean = 0f; } public void WriteProperties(INeoSerializer writer, NeoSerializedGameObject nsgo, SaveMode saveMode) { writer.WriteValue(k_CurrentLeanKey, m_CurrentLean); writer.WriteValue(k_TargetKey, m_TargetLean); writer.WriteValue(k_VelocityKey, m_LeanVelocity); } public void ReadProperties(INeoDeserializer reader, NeoSerializedGameObject nsgo) { reader.TryReadValue(k_CurrentLeanKey, out m_CurrentLean, m_CurrentLean); reader.TryReadValue(k_TargetKey, out m_TargetLean, m_TargetLean); reader.TryReadValue(k_VelocityKey, out m_LeanVelocity, m_LeanVelocity); } #region COUNTER-ROTATION class BodyLeanCounterRotation : MonoBehaviour, IAdditiveTransform { public bool bypassPositionMultiplier { get { return true; } } public bool bypassRotationMultiplier { get { return true; } } public Vector3 position { get { return Vector3.zero; } } public void UpdateTransform() { } private BodyLean m_BodyLean = null; public Quaternion rotation { get { if (m_BodyLean != null && m_BodyLean.currentLean != 0f) return Quaternion.Slerp(Quaternion.identity, Quaternion.Inverse(m_BodyLean.m_LeanRotation), m_BodyLean.m_HeadCounterTilt); else return Quaternion.identity; } } public void AttachToBodyLean(BodyLean body) { m_BodyLean = body; } } #endregion } }