/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.FirstPersonController.Camera.ViewTypes { using Opsive.Shared.Events; using Opsive.Shared.Game; using Opsive.Shared.Utility; using Opsive.UltimateCharacterController.Camera; using Opsive.UltimateCharacterController.Camera.ViewTypes; using Opsive.UltimateCharacterController.Game; using Opsive.UltimateCharacterController.Motion; using Opsive.UltimateCharacterController.StateSystem; using Opsive.UltimateCharacterController.Utility; using UnityEngine; /// /// The FirstPerson ViewType allows the camera to be placed in a first person perspective. /// public abstract class FirstPerson : ViewType { #if ULTIMATE_CHARACTER_CONTROLLER_LWRP || ULTIMATE_CHARACTER_CONTROLLER_UNIVERSALRP /// /// Specifies how the overlay objects are rendered. /// public enum ObjectOverlayRenderType { SecondCamera, // Use a second stacked camera to ensure the overlay objects do no clip with any other objects. RenderPipeline, // Use the LWRP/URP render pipeline to ensure the overlay objects do no clip with any other objects. None // No special rendering for the overlay objects. } #endif [Tooltip("The distance that the character should look ahead.")] [SerializeField] protected float m_LookDirectionDistance = 100; [Tooltip("The offset between the anchor and the camera.")] [SerializeField] protected Vector3 m_LookOffset = new Vector3(0, .1f, 0.27f); [Tooltip("Amount to adjust the camera position by when the character is looking down.")] [SerializeField] protected Vector3 m_LookDownOffset = new Vector3(0, 0, 0.28f); [Tooltip("The culling mask of the camera.")] [SerializeField] protected LayerMask m_CullingMask = ~(1 << LayerManager.Overlay); [Tooltip("The field of view of the main camera.")] [SerializeField] protected float m_FieldOfView = 70f; [Tooltip("The damping time of the field of view angle when changed.")] [SerializeField] protected float m_FieldOfViewDamping = 0.2f; [Tooltip("Specifies the position offset from the camera that the first person objects should render.")] [SerializeField] protected Vector3 m_FirstPersonPositionOffset; [Tooltip("Specifies the rotation offset from the camera that the first person objects should render.")] [SerializeField] protected Vector3 m_FirstPersonRotationOffset; #if ULTIMATE_CHARACTER_CONTROLLER_LWRP || ULTIMATE_CHARACTER_CONTROLLER_UNIVERSALRP [Tooltip("Specifies how the overlay objects are rendered.")] [SerializeField] protected ObjectOverlayRenderType m_OverlayRenderType = ObjectOverlayRenderType.SecondCamera; #else [Tooltip("Should the first person camera be used?")] [SerializeField] protected bool m_UseFirstPersonCamera = true; #endif [Tooltip("A reference to the first person camera.")] [SerializeField] protected UnityEngine.Camera m_FirstPersonCamera; [Tooltip("The culling mask of the first person objects.")] [SerializeField] protected LayerMask m_FirstPersonCullingMask = 1 << LayerManager.Overlay; [Tooltip("Should the first person camera's field of view be synchronized with the main camera?")] [SerializeField] protected bool m_SynchronizeFieldOfView = true; [Tooltip("Specifies the field of view for the first person camera.")] [SerializeField] protected float m_FirstPersonFieldOfView = 30f; [Tooltip("The damping time of the field of view angle when changed.")] [SerializeField] protected float m_FirstPersonFieldOfViewDamping = 0.2f; [Tooltip("A vertical limit intended to prevent the camera from intersecting with the character.")] [SerializeField] protected float m_PositionLowerVerticalLimit = 0.25f; [Tooltip("Determines how much the camera will be pushed down when the player falls onto a surface.")] [SerializeField] protected float m_PositionFallImpact = 2f; [Tooltip("The number of frames that the fall impact force should be applied.")] [SerializeField] protected int m_PositionFallImpactSoftness = 4; [Tooltip("Rotates the camera depending on the sideways local velocity of the character, resulting in the camera leaning into or away from its sideways movement direction.")] [SerializeField] protected float m_RotationStrafeRoll = 0.01f; [Tooltip("Determines how much the camera will roll when the player falls onto a surface.")] [SerializeField] protected float m_RotationFallImpact = 0.1f; [Tooltip("The number of frames that the fall impact force should be applied.")] [SerializeField] protected int m_RotationFallImpactSoftness = 1; [Tooltip("The positional spring used for regular movement.")] [SerializeField] protected Spring m_PositionSpring = new Spring(); [Tooltip("The rotational spring used for regular movement.")] [SerializeField] protected Spring m_RotationSpring = new Spring(); [Tooltip("The positional spring which returns to equilibrium after a small amount of time (for recoil).")] [SerializeField] protected Spring m_SecondaryPositionSpring = new Spring(); [Tooltip("The rotational spring which returns to equilibrium after a small amount of time (for recoil).")] [SerializeField] protected Spring m_SecondaryRotationSpring = new Spring(); [Tooltip("The minimum pitch angle (in degrees).")] [SerializeField] protected float m_MinPitchLimit = -72; [Tooltip("The maximum pitch angle (in degrees).")] [SerializeField] protected float m_MaxPitchLimit = 72; [Tooltip("The rate that the camera changes its position while the character is moving.")] [SerializeField] protected Vector3 m_BobPositionalRate = new Vector3(0.0f, 1.4f, 0.0f); [Tooltip("The strength of the positional camera bob. Determines how far the camera swings in each respective direction.")] [SerializeField] protected Vector3 m_BobPositionalAmplitude = new Vector3(0.0f, 0.35f, 0.0f); [Tooltip("The rate that the camera changes its roll rotation value while the character is moving.")] [SerializeField] protected float m_BobRollRate = 0.9f; [Tooltip("The strength of the roll within the camera bob. Determines how far the camera tilts from left to right.")] [SerializeField] protected float m_BobRollAmplitude = 1.7f; [Tooltip("This tweaking feature is useful if the bob motion gets out of hand after changing character velocity.")] [SerializeField] protected float m_BobInputVelocityScale = 1; [Tooltip("A cap on the velocity value from the bob function, preventing the camera from flipping out when the character travels at excessive speeds.")] [SerializeField] protected float m_BobMaxInputVelocity = 1000; [Tooltip("A trough should only occur when the bob vertical offset is less then the specified value.")] [SerializeField] protected float m_BobMinTroughVerticalOffset = -0.01f; [Tooltip("The amount of force to add when the bob has reached its lowest point. This can be used to add a shaking effect to the camera to mimick a giant walking.")] [SerializeField] protected Vector3 m_BobTroughForce = new Vector3(0.0f, 0.0f, 0.0f); [Tooltip("Determines whether the bob should stay in effect only when the character is on the ground.")] [SerializeField] protected bool m_BobRequireGroundContact = true; [Tooltip("The speed that the camera should shake.")] [SerializeField] protected float m_ShakeSpeed = 0.1f; [Tooltip("The strength of the shake. Determines how much the camera will tilt.")] [SerializeField] protected Vector3 m_ShakeAmplitude = new Vector3(10, 10); [Tooltip("Number of head offset values to average together. The head offset value specifies how reactive the camera is to head movements.")] [SerializeField] protected int m_SmoothHeadOffsetSteps = 2; [Tooltip("The radius of the camera's collision sphere to prevent it from clipping with other objects.")] [SerializeField] protected float m_CollisionRadius = 0.05f; [Tooltip("Should the camera rotate as the character's head rotates?")] [SerializeField] protected bool m_RotateWithHead; public Vector3 LookOffset { get { return m_LookOffset; } set { m_LookOffset = value; if (m_PositionSpring != null && m_CameraController != null) { InitializePositionSpringValue(); } } } public Vector3 LookDownOffset { get { return m_LookDownOffset; } set { m_LookDownOffset = value; } } public LayerMask CullingMask { get { return m_CullingMask; } set { if (m_CullingMask != value) { m_CullingMask = value; UpdateFirstPersonCamera(m_CharacterLocomotion.FirstPersonPerspective); } } } public float FieldOfView { get { return m_FieldOfView; } set { m_FieldOfView = value; } } public float FieldOfViewDamping { get { return m_FieldOfViewDamping; } set { m_FieldOfViewDamping = value; } } public Vector3 FirstPersonPositionOffset { get { return m_FirstPersonPositionOffset; } set { m_FirstPersonPositionOffset = value; if (m_FirstPersonCameraTransform != null) { m_FirstPersonCameraTransform.localPosition = m_FirstPersonPositionOffset; } } } public Vector3 FirstPersonRotationOffset { get { return m_FirstPersonRotationOffset; } set { m_FirstPersonRotationOffset = value; if (m_FirstPersonCameraTransform != null) { m_FirstPersonCameraTransform.localRotation = Quaternion.Euler(m_FirstPersonRotationOffset); } } } public override float LookDirectionDistance { get { return m_LookDirectionDistance; } } #if ULTIMATE_CHARACTER_CONTROLLER_LWRP || ULTIMATE_CHARACTER_CONTROLLER_UNIVERSALRP [NonSerialized] public ObjectOverlayRenderType OverlayRenderType { get { return m_OverlayRenderType; } set { m_OverlayRenderType = value; } } #else public bool UseFirstPersonCamera { get { return m_UseFirstPersonCamera; } set { m_UseFirstPersonCamera = value; if (m_FirstPersonCamera == null || m_CharacterLocomotion == null) { return; } UpdateFirstPersonCamera(m_CharacterLocomotion.FirstPersonPerspective); } } #endif [NonSerialized] public UnityEngine.Camera FirstPersonCamera { get { return m_FirstPersonCamera; } set { m_FirstPersonCamera = value; } } public LayerMask FirstPersonCullingMask { get { return m_FirstPersonCullingMask; } set { if (m_FirstPersonCamera != null && m_FirstPersonCullingMask != value) { m_FirstPersonCullingMask = value; m_FirstPersonCamera.cullingMask = m_FirstPersonCullingMask; } } } public bool SynchronizeFieldOfView { get { return m_SynchronizeFieldOfView; } set { m_SynchronizeFieldOfView = value; } } public float FirstPersonFieldOfView { get { return m_FirstPersonFieldOfView; } set { m_FirstPersonFieldOfView = value; } } public float FirstPersonFieldOfViewDamping { get { return m_FirstPersonFieldOfViewDamping; } set { m_FirstPersonFieldOfViewDamping = value; } } public float PositionLowerVerticalLimit { get { return m_PositionLowerVerticalLimit; } set { m_PositionLowerVerticalLimit = value; if (m_PositionSpring != null && m_CameraController != null) { InitializePositionSpringValue(); } } } public float PositionFallImpact { get { return m_PositionFallImpact; } set { m_PositionFallImpact = value; } } public int PositionFallImpactSoftness { get { return m_PositionFallImpactSoftness; } set { m_PositionFallImpactSoftness = value; } } public float RotationStrafeRoll { get { return m_RotationStrafeRoll; } set { m_RotationStrafeRoll = value; } } public float RotationFallImpact { get { return m_RotationFallImpact; } set { m_RotationFallImpact = value; } } public int RotationFallImpactSoftness { get { return m_RotationFallImpactSoftness; } set { m_RotationFallImpactSoftness = value; } } public Spring PositionSpring { get { return m_PositionSpring; } set { m_PositionSpring = value; if (m_CameraController != null) { m_PositionSpring.Initialize(false, true); InitializePositionSpringValue(); } } } public Spring RotationSpring { get { return m_RotationSpring; } set { m_RotationSpring = value; if (m_RotationSpring != null) { m_RotationSpring.Initialize(true, true); } } } public Spring SecondaryPositionSpring { get { return m_SecondaryPositionSpring; } set { m_SecondaryPositionSpring = value; if (m_SecondaryPositionSpring != null) { m_SecondaryPositionSpring.Initialize(false, true); } } } public Spring SecondaryRotationSpring { get { return m_SecondaryRotationSpring; } set { m_SecondaryRotationSpring = value; if (m_SecondaryRotationSpring != null) { m_SecondaryRotationSpring.Initialize(true, true); } } } public float MinPitchLimit { get { return m_MinPitchLimit; } set { m_MinPitchLimit = value; } } public float MaxPitchLimit { get { return m_MaxPitchLimit; } set { m_MaxPitchLimit = value; } } public Vector3 BobPositionalRate { get { return m_BobPositionalRate; } set { m_BobPositionalRate = value; } } public Vector3 BobPositionalAmplitude { get { return m_BobPositionalAmplitude; } set { m_BobPositionalAmplitude = value; } } public float BobRollRate { get { return m_BobRollRate; } set { m_BobRollRate = value; } } public float BobRollAmplitude { get { return m_BobRollAmplitude; } set { m_BobRollAmplitude = value; } } public float BobInputVelocityScale { get { return m_BobInputVelocityScale; } set { m_BobInputVelocityScale = value; } } public float BobMaxInputVelocity { get { return m_BobMaxInputVelocity; } set { m_BobMaxInputVelocity = value; } } public float BobMinTroughVerticalOffset { get { return m_BobMinTroughVerticalOffset; } set { m_BobMinTroughVerticalOffset = value; } } public Vector3 BobTroughForce { get { return m_BobTroughForce; } set { m_BobTroughForce = value; } } public bool BobRequireGroundContact { get { return m_BobRequireGroundContact; } set { m_BobRequireGroundContact = value; } } public float ShakeSpeed { get { return m_ShakeSpeed; } set { m_ShakeSpeed = value; } } public Vector3 ShakeAmplitude { get { return m_ShakeAmplitude; } set { m_ShakeAmplitude = value; } } public int SmoothHeadOffsetSteps { get { return m_SmoothHeadOffsetSteps; } set { m_SmoothHeadOffsetSteps = value; if (value == 0) { m_SmoothHeadBufferCount = 0; } } } public bool RotateWithHead { get { return m_RotateWithHead; } set { m_RotateWithHead = value; } } private UnityEngine.Camera m_Camera; private Transform m_CrosshairsTransform; private AimAssist m_AimAssist; private Transform m_FirstPersonCameraTransform; protected float m_Pitch; protected float m_Yaw; private float m_PrevBobSpeed; protected Vector3 m_Shake; protected Quaternion m_CharacterRotation; private Quaternion m_CharacterPlatformRotationOffset = Quaternion.identity; private Quaternion m_PlatformRotation = Quaternion.identity; private bool m_AppendingZoomState; private Vector3 m_CrosshairsLocalPosition; private Quaternion m_CrosshairsDeltaRotation; private Transform m_CharacterAnchor; private Vector3 m_CharacterAnchorOffset; private Quaternion m_CharacterAnchorRotation; private Vector3[] m_SmoothHeadOffsetBuffer; private int m_SmoothHeadBufferIndex; private int m_SmoothHeadBufferCount; private ScheduledEventBase m_SmoothHeadBufferEvent; private RaycastHit m_RaycastHit; private float m_FieldOfViewChangeTime; private float m_FirstPersonFieldOfViewChangeTime; private float m_BobVerticalOffset = float.MaxValue; private bool m_BobVerticalOffsetDecreasing = false; private float m_VerticalOffsetAdjustment; private Vector3 m_PrevPositionSpringValue; private Vector3 m_PrevPositionSpringVelocity; private Vector3 m_PrevRotationSpringValue; private Vector3 m_PrevRotationSpringVelocity; private Vector3 m_PrevSecondaryPositionSpringValue; private Vector3 m_PrevSecondaryPositionSpringVelocity; private Vector3 m_PrevSecondaryRotationSpringValue; private Vector3 m_PrevSecondaryRotationSpringVelocity; private float m_PrevFieldOfViewDamping; private float m_PrevFirstPersonFieldOfViewDamping; private int m_StateChangeFrame = -1; public override float Pitch { get { return m_Pitch; } } public override float Yaw { get { return m_Yaw; } } public override Quaternion CharacterRotation { get { return m_CharacterRotation; } } public override bool FirstPersonPerspective { get { return true; } } public Vector3 Shake { get { return m_Shake; } } /// /// Initializes the view type to the specified camera controller. /// /// The camera controller to initialize the view type to. public override void Initialize(CameraController cameraController) { base.Initialize(cameraController); m_Camera = cameraController.gameObject.GetCachedComponent(); m_AimAssist = m_GameObject.GetCachedComponent(); m_Camera.depth = 0; #if ULTIMATE_CHARACTER_CONTROLLER_LWRP || ULTIMATE_CHARACTER_CONTROLLER_UNIVERSALRP if (m_OverlayRenderType != ObjectOverlayRenderType.None) { #else if (m_UseFirstPersonCamera) { #endif m_Camera.cullingMask &= m_CullingMask; } // Setup the overlay camera. if (m_FirstPersonCamera != null) { if (!UnityEngine.XR.XRSettings.enabled) { m_Camera.fieldOfView = m_FieldOfView; m_FirstPersonCamera.fieldOfView = m_SynchronizeFieldOfView ? m_FieldOfView : m_FirstPersonFieldOfView; } m_FirstPersonCamera.clearFlags = CameraClearFlags.Depth; m_FirstPersonCamera.cullingMask = m_FirstPersonCullingMask; m_FirstPersonCamera.depth = m_Camera.depth + 1; m_FirstPersonCamera.rect = m_Camera.rect; m_FirstPersonCameraTransform = m_FirstPersonCamera.transform; m_FirstPersonCameraTransform.parent = m_Transform; m_FirstPersonCameraTransform.localPosition = m_FirstPersonPositionOffset; m_FirstPersonCameraTransform.localRotation = Quaternion.Euler(m_FirstPersonRotationOffset); } // Using the buffer is optional. if (m_SmoothHeadOffsetSteps > 0) { m_SmoothHeadOffsetBuffer = new Vector3[m_SmoothHeadOffsetSteps]; } // Initialize the springs. m_PositionSpring.Initialize(false, false); m_RotationSpring.Initialize(true, true); m_SecondaryPositionSpring.Initialize(false, false); m_SecondaryRotationSpring.Initialize(true, true); InitializePositionSpringValue(); m_PositionSpring.Reset(); // Register for any interested events. EventHandler.RegisterEvent(m_GameObject, "OnAnchorOffsetUpdated", InitializePositionSpringValue); EventHandler.RegisterEvent(m_GameObject, "OnCameraChangeViewTypes", OnChangeViewType); } /// /// Initializes the position spring value. /// private void InitializePositionSpringValue() { m_PositionSpring.RestValue = m_CameraController.AnchorOffset + m_LookOffset; var value = m_PositionSpring.MinValue; value.y = m_PositionSpring.RestValue.y - m_PositionLowerVerticalLimit; m_PositionSpring.MinValue = value; } /// /// Attaches the camera to the specified character. /// /// The character to attach the camera to. public override void AttachCharacter(GameObject character) { // Unregister from any events on the previous character. if (m_Character != null) { EventHandler.UnregisterEvent(m_Character, "OnCharacterChangeMovingPlatforms", OnCharacterChangeMovingPlatforms); EventHandler.UnregisterEvent(m_Character, "OnCameraChangePerspectives", UpdateFirstPersonCamera); EventHandler.UnregisterEvent(m_Character, "OnCharacterLand", OnCharacterLand); EventHandler.UnregisterEvent(m_Character, "OnCharacterLean", OnCharacterLean); EventHandler.UnregisterEvent(m_Character, "OnHeightChangeAdjustHeight", AdjustVerticalOffset); if (m_SmoothHeadBufferEvent != null) { Scheduler.Cancel(m_SmoothHeadBufferEvent); m_SmoothHeadBufferEvent = null; } m_CharacterAnchor = null; } base.AttachCharacter(character); // Initialize the camera with the new character. if (m_Character != null) { var characterAnimator = m_Character.GetCachedComponent(); if (characterAnimator != null && m_SmoothHeadOffsetBuffer != null) { m_CharacterAnchor = characterAnimator.GetBoneTransform(HumanBodyBones.Neck); // If the neck doesn't exist then try to get the head. if (m_CharacterAnchor == null) { m_CharacterAnchor = characterAnimator.GetBoneTransform(HumanBodyBones.Head); } if (m_CharacterAnchor != null) { if (m_SmoothHeadBufferEvent != null) { Scheduler.Cancel(m_SmoothHeadBufferEvent); m_SmoothHeadBufferEvent = null; } InitializeSmoothHeadBuffer(); } } else { EventHandler.RegisterEvent(m_Character, "OnHeightChangeAdjustHeight", AdjustVerticalOffset); } m_SmoothHeadBufferIndex = -1; m_SmoothHeadBufferCount = 0; EventHandler.RegisterEvent(m_Character, "OnCharacterChangeMovingPlatforms", OnCharacterChangeMovingPlatforms); EventHandler.RegisterEvent(m_Character, "OnCameraChangePerspectives", UpdateFirstPersonCamera); EventHandler.RegisterEvent(m_Character, "OnCharacterLand", OnCharacterLand); EventHandler.RegisterEvent(m_Character, "OnCharacterLean", OnCharacterLean); } } /// /// Initializes the SmoothHeadBuffer after the character is grounded. This will allow the animator to be the correct height. /// private void InitializeSmoothHeadBuffer() { if (!m_CharacterLocomotion.Grounded || m_CharacterLocomotion.GroundRaycastHit.distance > m_CharacterLocomotion.ColliderSpacing + 0.011f) { m_SmoothHeadBufferEvent = Scheduler.Schedule(Time.fixedDeltaTime, InitializeSmoothHeadBuffer); return; } m_CharacterAnchorOffset = m_CharacterTransform.InverseTransformPoint(m_CharacterAnchor.position); m_SmoothHeadBufferEvent = null; } /// /// The view type has changed. /// /// Should the current view type be activated? /// The pitch of the camera (in degrees). /// The yaw of the camera (in degrees). /// The rotation of the character. public override void ChangeViewType(bool activate, float pitch, float yaw, Quaternion characterRotation) { if (activate) { m_Pitch = pitch; m_Yaw = yaw; m_CharacterRotation = characterRotation; if (m_CharacterLocomotion.Platform != null) { UpdatePlatformRotationOffset(m_CharacterLocomotion.Platform); } UpdateFirstPersonCamera(m_CharacterLocomotion.FirstPersonPerspective); } } /// /// Reset the ViewType's variables. /// /// The rotation of the character. public override void Reset(Quaternion characterRotation) { m_Pitch = 0; m_Yaw = 0; m_Shake = Vector3.zero; m_CharacterRotation = characterRotation; m_BobVerticalOffset = float.MaxValue; m_BobVerticalOffsetDecreasing = false; m_PrevBobSpeed = 0; if (m_CharacterLocomotion.Platform != null) { UpdatePlatformRotationOffset(m_CharacterLocomotion.Platform); } if (m_CharacterAnchor != null) { m_CharacterAnchorRotation = MathUtility.InverseTransformQuaternion(m_CharacterRotation, m_CharacterAnchor.rotation); } InitializePositionSpringValue(); m_PositionSpring.Reset(); m_RotationSpring.Reset(); m_SecondaryPositionSpring.Reset(); m_SecondaryRotationSpring.Reset(); // The head position should be reset to the current values. m_SmoothHeadBufferIndex = -1; m_SmoothHeadBufferCount = 0; UpdateHeadOffset(); } /// /// Sets the crosshairs to the specified transform. /// /// The transform of the crosshairs. public override void SetCrosshairs(Transform crosshairs) { m_CrosshairsTransform = crosshairs; if (m_CrosshairsTransform != null) { var screenPoint = RectTransformUtility.WorldToScreenPoint(null, m_CrosshairsTransform.position); m_CrosshairsDeltaRotation = Quaternion.LookRotation(m_Camera.ScreenPointToRay(screenPoint).direction, m_Transform.up) * Quaternion.Inverse(m_Transform.rotation); m_CrosshairsLocalPosition = m_CrosshairsTransform.localPosition; } } /// /// Returns the delta rotation caused by the crosshairs. /// /// The delta rotation caused by the crosshairs. public override Quaternion GetCrosshairsDeltaRotation() { if (m_CrosshairsTransform == null) { return Quaternion.identity; } // The crosshairs direction should only be updated when it changes. if (m_CrosshairsLocalPosition != m_CrosshairsTransform.localPosition) { var screenPoint = RectTransformUtility.WorldToScreenPoint(null, m_CrosshairsTransform.position); m_CrosshairsDeltaRotation = Quaternion.LookRotation(m_Camera.ScreenPointToRay(screenPoint).direction, m_Transform.up) * Quaternion.Inverse(m_Transform.rotation); m_CrosshairsLocalPosition = m_CrosshairsTransform.localPosition; } return m_CrosshairsDeltaRotation; } /// /// The moving platform object has changed. /// /// The moving platform to set. Can be null. private void OnCharacterChangeMovingPlatforms(Transform movingPlatform) { if (movingPlatform != null) { UpdatePlatformRotationOffset(movingPlatform); } else { m_PlatformRotation = Quaternion.identity; } } /// /// Updates the Character Platform Rotation Offset variable. /// /// The platform that the character is on top of. private void UpdatePlatformRotationOffset(Transform platform) { m_CharacterPlatformRotationOffset = m_CharacterRotation * Quaternion.Inverse(platform.rotation); if (!m_CharacterLocomotion.AlignToGravity) { // Only the local y rotation should affect the character's rotation. var localPlatformRotationOffset = MathUtility.InverseTransformQuaternion(m_CharacterRotation, m_CharacterPlatformRotationOffset).eulerAngles; localPlatformRotationOffset.x = localPlatformRotationOffset.z = 0; m_CharacterPlatformRotationOffset = MathUtility.TransformQuaternion(m_CharacterRotation, Quaternion.Euler(localPlatformRotationOffset)); } } /// /// Updates the first person camera and culling mask depending on if the first person camera is in use. /// /// Is the character in a first person perspective? private void UpdateFirstPersonCamera(bool firstPersonPerspective) { #if ULTIMATE_CHARACTER_CONTROLLER_LWRP || ULTIMATE_CHARACTER_CONTROLLER_UNIVERSALRP if (m_OverlayRenderType == ObjectOverlayRenderType.None) { #else if (!m_UseFirstPersonCamera) { #endif return; } if (firstPersonPerspective) { if (m_FirstPersonCamera != null) { m_FirstPersonCamera.gameObject.SetActive(true); } #if ULTIMATE_CHARACTER_CONTROLLER_LWRP || ULTIMATE_CHARACTER_CONTROLLER_UNIVERSALRP if (m_OverlayRenderType == ObjectOverlayRenderType.RenderPipeline) { m_Camera.cullingMask |= m_FirstPersonCullingMask; } #endif } else { if (m_FirstPersonCamera != null) { m_FirstPersonCamera.gameObject.SetActive(false); } #if ULTIMATE_CHARACTER_CONTROLLER_LWRP || ULTIMATE_CHARACTER_CONTROLLER_UNIVERSALRP if (m_OverlayRenderType == ObjectOverlayRenderType.RenderPipeline) { m_Camera.cullingMask &= ~m_FirstPersonCullingMask; } #endif } } /// /// The view type has changed. /// /// The ViewType that was activated or deactivated. /// Should the current view type be activated? private void OnChangeViewType(ViewType viewType, bool activate) { if (activate || viewType == this) { return; } // Apply the spring/shake values so when switching between first person view types it is a continuous motion. if (viewType is FirstPerson) { var firstPersonViewType = (viewType as FirstPerson); m_PositionSpring.Value = firstPersonViewType.PositionSpring.Value; m_PositionSpring.Velocity = firstPersonViewType.PositionSpring.Velocity; m_RotationSpring.Value = firstPersonViewType.RotationSpring.Value; m_RotationSpring.Velocity = firstPersonViewType.RotationSpring.Velocity; m_Shake = firstPersonViewType.Shake; } } /// /// Updates the camera field of view. /// /// Should the camera be updated immediately? public override void UpdateFieldOfView(bool immediateUpdate) { if (m_Camera.fieldOfView != m_FieldOfView) { var zoom = (immediateUpdate || m_FieldOfViewDamping == 0) ? 1 : ((Time.time - m_FieldOfViewChangeTime) / (m_FieldOfViewDamping / m_CharacterLocomotion.TimeScale)); m_Camera.fieldOfView = Mathf.SmoothStep(m_Camera.fieldOfView, m_FieldOfView, zoom); } if (m_FirstPersonCamera != null) { if (m_SynchronizeFieldOfView) { m_FirstPersonCamera.fieldOfView = m_Camera.fieldOfView; } else if (m_FirstPersonCamera.fieldOfView != m_FirstPersonFieldOfView) { var zoom = immediateUpdate ? 1 : ((Time.time - m_FirstPersonFieldOfViewChangeTime) / (m_FirstPersonFieldOfViewDamping / m_CharacterLocomotion.TimeScale)); m_FirstPersonCamera.fieldOfView = Mathf.SmoothStep(m_FirstPersonCamera.fieldOfView, m_FirstPersonFieldOfView, zoom); } } } /// /// Rotates the camera according to the horizontal and vertical movement values. /// /// -1 to 1 value specifying the amount of horizontal movement. /// -1 to 1 value specifying the amount of vertical movement. /// Should the camera be updated immediately? /// The updated rotation. public override Quaternion Rotate(float horizontalMovement, float verticalMovement, bool immediateUpdate) { UpdateShakes(); // Rotate with the moving platform. if (m_CharacterLocomotion.Platform != null) { m_PlatformRotation = MathUtility.InverseTransformQuaternion(m_CharacterLocomotion.Platform.rotation, m_CharacterPlatformRotationOffset) * Quaternion.Inverse(MathUtility.InverseTransformQuaternion(m_CharacterLocomotion.Platform.rotation, m_CharacterRotation * Quaternion.Inverse(m_CharacterLocomotion.Platform.rotation))); if (!m_CharacterLocomotion.AlignToGravity) { // Only the local y rotation should affect the character's rotation. var localPlatformTorque = MathUtility.InverseTransformQuaternion(m_CharacterTransform.rotation, m_PlatformRotation).eulerAngles; localPlatformTorque.x = localPlatformTorque.z = 0; m_PlatformRotation = MathUtility.TransformQuaternion(m_CharacterTransform.rotation, Quaternion.Euler(localPlatformTorque)); } m_CharacterRotation *= m_PlatformRotation; } // The camera should always stay aligned to the character's up direction. if (m_CharacterLocomotion.AlignToGravity) { var localRotation = MathUtility.InverseTransformQuaternion(m_CharacterTransform.rotation, m_CharacterRotation).eulerAngles; localRotation.x = localRotation.z = 0; m_CharacterRotation = MathUtility.TransformQuaternion(m_CharacterTransform.rotation, Quaternion.Euler(localRotation)); } // Remember the offset so the delta can be compared the next update. if (m_CharacterLocomotion.Platform != null) { UpdatePlatformRotationOffset(m_CharacterLocomotion.Platform); } // Update the rotation. The pitch may have a limit. if (Mathf.Abs(m_MinPitchLimit - m_MaxPitchLimit) < 180) { m_Pitch = MathUtility.ClampAngle(m_Pitch, -verticalMovement, m_MinPitchLimit, m_MaxPitchLimit); } else { m_Pitch -= verticalMovement; } // Prevent the values from getting too large. m_Pitch = MathUtility.ClampInnerAngle(m_Pitch); m_Yaw = MathUtility.ClampInnerAngle(m_Yaw); // If aim assist has a target then the camera should look in the specified direction. if (m_AimAssist != null) { m_AimAssist.UpdateBreakForce(Mathf.Abs(horizontalMovement) + Mathf.Abs(verticalMovement)); if (m_AimAssist.HasTarget()) { var rotation = MathUtility.TransformQuaternion(m_CharacterRotation, Quaternion.Euler(m_Pitch, m_Yaw, 0)); var assistRotation = rotation * MathUtility.InverseTransformQuaternion(rotation, m_AimAssist.TargetRotation(rotation)); // Set the pitch and yaw so when the target is lost the view type won't snap back to the previous rotation value. var localAssistRotation = MathUtility.InverseTransformQuaternion(m_CharacterRotation, assistRotation).eulerAngles; m_Pitch = MathUtility.ClampInnerAngle(localAssistRotation.x); m_Yaw = MathUtility.ClampInnerAngle(localAssistRotation.y); } } var headRotation = Quaternion.identity; if (m_RotateWithHead) { headRotation = MathUtility.InverseTransformQuaternion(m_CharacterRotation, m_CharacterAnchor.rotation) * Quaternion.Inverse(m_CharacterAnchorRotation); // The camera should only follow the pitch of the head rotation. var eulerHeadRotation = headRotation.eulerAngles; eulerHeadRotation.y = eulerHeadRotation.z = 0; headRotation = Quaternion.Euler(eulerHeadRotation); } // Return the rotation. return MathUtility.TransformQuaternion(m_CharacterRotation, Quaternion.Euler(m_Pitch, m_Yaw, 0)) * Quaternion.Euler(m_RotationSpring.Value) * Quaternion.Euler(m_SecondaryRotationSpring.Value) * headRotation; } /// /// Updates the procedular shaking of the camera. /// private void UpdateShakes() { if (m_ShakeSpeed == 0) { return; } // Subtract shake from the last frame or the camera will drift. m_Yaw -= m_Shake.x; m_Pitch -= m_Shake.y; // Apply the new shake. m_Shake = Vector3.Scale(SmoothRandom.GetVector3Centered(m_ShakeSpeed), m_ShakeAmplitude); m_Yaw += m_Shake.x; m_Pitch += m_Shake.y; m_RotationSpring.AddForce(Vector3.forward * m_Shake.z * m_CharacterLocomotion.TimeScale * Time.timeScale); } /// /// Moves the camera according to the current pitch and yaw values. /// /// Should the camera be updated immediately? /// The updated position. public override Vector3 Move(bool immediateUpdate) { UpdateSway(); UpdateBob(); UpdateHeadOffset(); var targetPosition = GetTargetPosition(); // Adjust the camera position to prevent the body from clipping with the camera's spring-based motions. if (m_MaxPitchLimit != 0) { var lookDown = Mathf.Max(0, m_Pitch / m_MaxPitchLimit); lookDown = Mathf.SmoothStep(0, 1, lookDown); targetPosition += m_Transform.TransformDirection(m_LookDownOffset * lookDown); } // Ensure there aren't any objects obstructing the distance between the anchor offset and the target position. var collisionLayerEnabled = m_CharacterLocomotion.CollisionLayerEnabled; m_CharacterLocomotion.EnableColliderCollisionLayer(false); var offset = m_PositionSpring.Value + GetHeadOffset(); offset.x = offset.z = 0; var startPosition = GetAnchorTransformPoint(offset); var direction = targetPosition - startPosition; if (Physics.SphereCast(startPosition, m_CollisionRadius, direction.normalized, out m_RaycastHit, direction.magnitude + m_Camera.nearClipPlane, m_CharacterLayerManager.IgnoreInvisibleCharacterWaterLayers, QueryTriggerInteraction.Ignore)) { // Move the camera in if an object obstructed the view. targetPosition -= direction.normalized * ((direction.magnitude + m_Camera.nearClipPlane) - m_RaycastHit.distance); } m_CharacterLocomotion.EnableColliderCollisionLayer(collisionLayerEnabled); return targetPosition; } /// /// Returns the target position accounting for the springs and head offset. /// /// The target position accounting for the springs and head offset. public Vector3 GetTargetPosition() { var targetPosition = GetAnchorTransformPoint(m_PositionSpring.Value + m_SecondaryPositionSpring.Value + Vector3.up * m_VerticalOffsetAdjustment); // The camera should move with the head offset of the character. targetPosition += GetHeadOffset(); return targetPosition; } /// /// Updates the head offset to match the current animator head position. /// private void UpdateHeadOffset() { // Allow the camera to move with the character's neck/head so the body doesn't clip the camera. if (m_SmoothHeadBufferEvent == null && m_CharacterAnchor != null && m_SmoothHeadOffsetBuffer != null && m_SmoothHeadOffsetSteps > 0) { var offset = m_CharacterTransform.InverseTransformPoint(m_CharacterAnchor.position) - m_CharacterAnchorOffset; // Allow the offset to settle before storing the difference. if (offset.sqrMagnitude > 0) { // Add the current offset to the buffer. m_SmoothHeadBufferIndex = (m_SmoothHeadBufferIndex + 1) % m_SmoothHeadOffsetBuffer.Length; m_SmoothHeadOffsetBuffer[m_SmoothHeadBufferIndex] = offset; if (m_SmoothHeadBufferCount <= m_SmoothHeadBufferIndex) { m_SmoothHeadBufferCount++; } } } } /// /// Returns the smoothed head offset. /// /// The smoothed head offset private Vector3 GetHeadOffset() { if (m_SmoothHeadBufferCount > 0) { // Find the average. var total = Vector3.zero; for (int i = 0; i < m_SmoothHeadBufferCount; ++i) { var index = m_SmoothHeadBufferIndex - i; if (index < 0) { index = m_SmoothHeadBufferCount + m_SmoothHeadBufferIndex - i; } total += m_SmoothHeadOffsetBuffer[index]; } return m_CharacterTransform.TransformDirection(total / m_SmoothHeadBufferCount); } return Vector3.zero; } /// /// Applies a sway force on the camera in response to character controller motion. /// private void UpdateSway() { var localVelocity = m_Transform.InverseTransformDirection(m_CharacterLocomotion.LocomotionVelocity * 0.016f) * m_CharacterLocomotion.TimeScale * Time.timeScale; m_RotationSpring.AddForce(Vector3.forward * localVelocity.x * m_RotationStrafeRoll); } /// /// Updates the first person bob. /// private void UpdateBob() { if ((m_BobPositionalRate == Vector3.zero || m_BobPositionalAmplitude == Vector3.zero) && (m_BobRollRate == 0 || m_BobRollAmplitude == 0)) { return; } var bobSpeed = ((m_BobRequireGroundContact && !m_CharacterLocomotion.Grounded) ? 0 : m_CharacterLocomotion.LocomotionVelocity.sqrMagnitude); // Scale and limit the input velocity. bobSpeed = Mathf.Min(bobSpeed * m_BobInputVelocityScale, m_BobMaxInputVelocity); // Reduce the number of decimals to avoid floating point imprecision issues. bobSpeed = Mathf.Round(bobSpeed * 1000) / 1000; // If the bob speed is zero then fade out the last speed value. It is important to clamp the speed to the // last bob speed value because a preset may have changed since the last last bob. if (bobSpeed == 0) { bobSpeed = Mathf.Min((m_PrevBobSpeed * 0.93f), m_BobMaxInputVelocity); } // Update the positional and roll bob value. var currentPositionalBobAmplitude = (bobSpeed * (m_BobPositionalAmplitude * -0.0001f)); var currentRollBobAmplitude = (bobSpeed * (m_BobRollAmplitude * -0.0001f)); Vector3 currentBobOffsetValue; currentBobOffsetValue.x = Mathf.Cos(m_BobPositionalRate.x * m_CharacterLocomotion.TimeScale * Time.time * 10) * currentPositionalBobAmplitude.x; currentBobOffsetValue.y = Mathf.Cos(m_BobPositionalRate.y * m_CharacterLocomotion.TimeScale * Time.time * 10) * currentPositionalBobAmplitude.y; currentBobOffsetValue.z = Mathf.Cos(m_BobPositionalRate.z * m_CharacterLocomotion.TimeScale * Time.time * 10) * currentPositionalBobAmplitude.z; var currentBobRollValue = Mathf.Cos(m_BobRollRate * m_CharacterLocomotion.TimeScale * Time.time * 10) * currentRollBobAmplitude; // Add the bob value to the positional and rotational spring. m_PositionSpring.AddForce(currentBobOffsetValue); m_RotationSpring.AddForce(Vector3.forward * currentBobRollValue); m_PrevBobSpeed = bobSpeed; // Detect if the bob was previously decreasing and is now moving back up. This will indicate that the bob was at the lowest position. if (currentBobOffsetValue.y < m_BobMinTroughVerticalOffset && m_BobVerticalOffset < currentBobOffsetValue.y && m_BobVerticalOffsetDecreasing) { m_SecondaryPositionSpring.AddForce(m_BobTroughForce); } m_BobVerticalOffsetDecreasing = currentBobOffsetValue.y < m_BobVerticalOffset; m_BobVerticalOffset = currentBobOffsetValue.y; } /// /// Returns the direction that the character is looking. /// /// Is the character look direction being retrieved? /// The direction that the character is looking. public override Vector3 LookDirection(bool characterLookDirection) { var crosshairsDeltaRotation = characterLookDirection ? Quaternion.identity : GetCrosshairsDeltaRotation(); var shake = characterLookDirection ? Quaternion.Inverse(Quaternion.Euler(m_Shake.y, m_Shake.x, 0)) : Quaternion.identity; var platformRotation = characterLookDirection ? Quaternion.Inverse(m_PlatformRotation) : Quaternion.identity; return (m_Transform.rotation * crosshairsDeltaRotation * shake * platformRotation) * Vector3.forward; } /// /// Returns the direction that the character is looking. /// /// The position that the character is looking from. /// Is the character look direction being retrieved? /// The LayerMask value of the objects that the look direction can hit. /// Should recoil be included in the look direction? /// The direction that the character is looking. public override Vector3 LookDirection(Vector3 lookPosition, bool characterLookDirection, int layerMask, bool useRecoil) { var collisionLayerEnabled = m_CharacterLocomotion.CollisionLayerEnabled; m_CharacterLocomotion.EnableColliderCollisionLayer(false); // If a crosshairs is specified then the character should look at the crosshairs. Do not use the crosshairs delta for character look directions to prevent // the character's rotation from being affected by the crosshairs. var crosshairsDeltaRotation = characterLookDirection ? Quaternion.identity : GetCrosshairsDeltaRotation(); var shake = characterLookDirection ? Quaternion.Inverse(Quaternion.Euler(m_Shake.y, m_Shake.x, 0)) : Quaternion.identity; var platformRotation = characterLookDirection ? Quaternion.Inverse(m_PlatformRotation) : Quaternion.identity; // Cast a ray from the camera point in the forward direction. The look direction is then the vector from the look position to the hit point. RaycastHit hit; Vector3 hitPoint; var rotation = (useRecoil ? m_Transform.rotation : MathUtility.TransformQuaternion(m_CharacterRotation, Quaternion.Euler(m_Pitch, m_Yaw, 0))) * crosshairsDeltaRotation * shake * platformRotation; if (Physics.Raycast(m_Transform.position, rotation * Vector3.forward, out hit, m_LookDirectionDistance, layerMask, QueryTriggerInteraction.Ignore)) { hitPoint = hit.point; } else { Vector3 position; if (useRecoil) { position = GetAnchorTransformPoint(m_PositionSpring.Value + m_SecondaryPositionSpring.Value); } else { position = lookPosition; } var lookDirection = Vector3.zero; lookDirection.Set(0, 0, m_LookDirectionDistance); hitPoint = MathUtility.TransformPoint(position, rotation, lookDirection); } m_CharacterLocomotion.EnableColliderCollisionLayer(collisionLayerEnabled); return (hitPoint - lookPosition).normalized; } /// /// Adds a positional force to the ViewType. /// /// The force to add. public override void AddPositionalForce(Vector3 force) { m_PositionSpring.AddForce(force); } /// /// Adds a rotational force to the ViewType. The secondary position spring is more stiff compared to the primary positional spring. /// /// The force to add. public override void AddRotationalForce(Vector3 force) { m_RotationSpring.AddForce(force); } /// /// Adds a secondary positional force to the ViewType. /// /// The force to add. /// The percent of the force to accumulate to the rest value. public override void AddSecondaryPositionalForce(Vector3 force, float restAccumulation) { if (restAccumulation > 0 && (m_AimAssist == null || !m_AimAssist.HasTarget())) { m_SecondaryPositionSpring.RestValue += force * restAccumulation; } m_SecondaryPositionSpring.AddForce(force); } /// /// Adds a delayed rotational force to the ViewType. /// /// The force to add. /// The percent of the force to accumulate to the rest value. public override void AddSecondaryRotationalForce(Vector3 force, float restAccumulation) { if (restAccumulation > 0 && (m_AimAssist == null || !m_AimAssist.HasTarget())) { m_Pitch += force.x * restAccumulation; m_Yaw += force.y * restAccumulation; var springRest = m_SecondaryRotationSpring.RestValue; springRest.z += force.z * restAccumulation; m_SecondaryRotationSpring.RestValue = springRest; } m_SecondaryRotationSpring.AddForce(force); } /// /// The character has landed on the ground. /// /// The height of the fall. private void OnCharacterLand(float height) { var positionImpact = height * m_PositionFallImpact; var rotationImpact = height * m_RotationFallImpact; // Apply impact to camera position spring. m_PositionSpring.AddForce(positionImpact * -Vector3.up, m_PositionFallImpactSoftness); // Apply impact to camera rotation spring. Randomize the rotation upon landing. var roll = Random.value > 0.5f ? (rotationImpact * 2) : -(rotationImpact * 2); m_RotationSpring.AddForce(Vector3.forward * roll, m_RotationFallImpactSoftness); } /// /// The character has started to lean. /// /// The distance that the character is leaning. /// The amount of tilt to apply to the lean. /// The multiplier to apply to the tilt of an item. private void OnCharacterLean(float distance, float tilt, float itemTiltMultiplier) { m_PositionSpring.RestValue = m_CameraController.AnchorOffset + m_LookOffset + distance * Vector3.right; m_RotationSpring.RestValue = tilt * Vector3.forward; } /// /// Adjusts the vertical offset by the given amount. /// /// The amount to adjust the vertical offset height by. private void AdjustVerticalOffset(float amount) { m_VerticalOffsetAdjustment += amount; } /// /// Callback when the StateManager will change the active state on the current object. /// public override void StateWillChange() { // Remember the interal spring values so they can be restored if a new spring is applied during the state change. m_PrevPositionSpringValue = m_PositionSpring.Value; m_PrevPositionSpringVelocity = m_PositionSpring.Velocity; m_PrevRotationSpringValue = m_RotationSpring.Value; m_PrevRotationSpringVelocity = m_RotationSpring.Velocity; m_PrevSecondaryPositionSpringValue = m_SecondaryPositionSpring.Value; m_PrevSecondaryPositionSpringVelocity = m_SecondaryPositionSpring.Velocity; m_PrevSecondaryRotationSpringValue = m_SecondaryRotationSpring.Value; m_PrevSecondaryRotationSpringVelocity = m_SecondaryRotationSpring.Velocity; // Multiple state changes can occur within the same frame. Only remember the first damping value. if (m_StateChangeFrame != Time.frameCount) { m_PrevFieldOfViewDamping = m_FieldOfViewDamping; m_PrevFirstPersonFieldOfViewDamping = m_FirstPersonFieldOfViewDamping; } m_StateChangeFrame = Time.frameCount; } /// /// Callback when the StateManager has changed the active state on the current object. /// public override void StateChange() { // Append the zoom state name so the combination of state names will be called, such as "CrouchZoom". if (!string.IsNullOrEmpty(m_CameraController.ZoomState) && !m_AppendingZoomState) { m_AppendingZoomState = true; for (int i = 0; i < m_States.Length; ++i) { StateManager.SetState(m_GameObject, m_States[i].Name + m_CameraController.ZoomState, m_States[i].Active && m_CameraController.ZoomInput); } m_AppendingZoomState = false; } if (m_Camera.fieldOfView != m_FieldOfView) { m_FieldOfViewChangeTime = Time.time; // The field of view should get a head start if the damping was previously 0. This will allow the field of view to move back to the original value even // when the state is no longer active. if (m_CameraController.ActiveViewType == this && m_PrevFieldOfViewDamping == 0) { m_Camera.fieldOfView = (m_Camera.fieldOfView + m_FieldOfView) * 0.5f; if (m_FirstPersonCamera != null && m_SynchronizeFieldOfView) { m_FirstPersonCamera.fieldOfView = m_Camera.fieldOfView; } } } if (m_FirstPersonCamera != null && m_FirstPersonCamera.fieldOfView != m_FirstPersonFieldOfView) { m_FirstPersonFieldOfViewChangeTime = Time.time; // The field of view should get a head start if the damping was previously 0. This will allow the field of view to move back to the original value even // when the state is no longer active. if (m_CameraController.ActiveViewType == this && m_PrevFirstPersonFieldOfViewDamping == 0) { m_FirstPersonCamera.fieldOfView = (m_FirstPersonCamera.fieldOfView + m_FirstPersonFieldOfView) * 0.5f; } } m_PositionSpring.Value = m_PrevPositionSpringValue; m_PositionSpring.Velocity = m_PrevPositionSpringVelocity; m_RotationSpring.Value = m_PrevRotationSpringValue; m_RotationSpring.Velocity = m_PrevRotationSpringVelocity; m_SecondaryPositionSpring.Value = m_PrevSecondaryPositionSpringValue; m_SecondaryPositionSpring.Velocity = m_PrevSecondaryPositionSpringVelocity; m_SecondaryRotationSpring.Value = m_PrevSecondaryRotationSpringValue; m_SecondaryRotationSpring.Velocity = m_PrevSecondaryRotationSpringVelocity; } /// /// The camera has been destroyed. /// public override void OnDestroy() { base.OnDestroy(); m_PositionSpring.Destroy(); m_RotationSpring.Destroy(); m_SecondaryPositionSpring.Destroy(); m_SecondaryRotationSpring.Destroy(); EventHandler.UnregisterEvent(m_GameObject, "OnAnchorOffsetUpdated", InitializePositionSpringValue); EventHandler.UnregisterEvent(m_GameObject, "OnCameraChangeViewTypes", OnChangeViewType); } } }