/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.FirstPersonController.Character { using Opsive.Shared.Events; using Opsive.Shared.Game; using Opsive.UltimateCharacterController.Camera; using Opsive.UltimateCharacterController.Character; using Opsive.UltimateCharacterController.Items; using Opsive.UltimateCharacterController.StateSystem; using Opsive.UltimateCharacterController.Utility; using Opsive.UltimateCharacterController.FirstPersonController.Character.Identifiers; using System.Collections.Generic; using UnityEngine; /// /// Manages the location of the first person objects while in first or third person view. /// public class FirstPersonObjects : StateBehavior { [Tooltip("The minimum pitch angle (in degrees).")] [SerializeField] protected float m_MinPitchLimit = -90; [Tooltip("The maximum pitch angle (in degrees).")] [SerializeField] protected float m_MaxPitchLimit = 90; [Tooltip("Should the object's pitch be locked to the character's rotation?")] [SerializeField] protected bool m_LockPitch; [Tooltip("The minimum yaw angle (in degrees).")] [SerializeField] protected float m_MinYawLimit = -180; [Tooltip("The maximum yaw angle (in degrees).")] [SerializeField] protected float m_MaxYawLimit = 180; [Tooltip("Should the object's yaw be locked to the character's rotation?")] [SerializeField] protected bool m_LockYaw; [Tooltip("Should the object rotate with a change in crosshairs rotation?")] [SerializeField] protected bool m_RotateWithCrosshairs = true; [Tooltip("The speed at which the object rotates towards the target position.")] [SerializeField] protected float m_RotationSpeed = 15; [Tooltip("Should the objects be positioned according to the target position of the camera ignorning the look offset?")] [SerializeField] protected bool m_IgnorePositionalLookOffset; [Tooltip("If ignoring the look offset, specifies the offset from the target position that the first person objects should move towards.")] [SerializeField] protected Vector3 m_PositionOffset; [Tooltip("If ignoring the look offset, specifies the speed that the first person objects should move towards the target position.")] [SerializeField] protected float m_MoveSpeed; public float MinPitchLimit { get { return m_MinPitchLimit; } set { m_MinPitchLimit = value; if (Application.isPlaying) { if (m_LockPitch) { UpdateLockedPitchAngle(); } enabled = IsActive(); } } } public float MaxPitchLimit { get { return m_MaxPitchLimit; } set { m_MaxPitchLimit = value; if (Application.isPlaying) { if (m_LockPitch) { UpdateLockedPitchAngle(); } enabled = IsActive(); } } } public bool LockPitch { get { return m_LockPitch; } set { m_LockPitch = value; if (Application.isPlaying) { if (m_LockPitch) { UpdateLockedPitchAngle(); } enabled = IsActive(); } } } public float MinYawLimit { get { return m_MinYawLimit; } set { m_MinYawLimit = value; if (Application.isPlaying) { if (m_LockYaw) { UpdateLockedYawAngle(); } enabled = IsActive(); } } } public float MaxYawLimit { get { return m_MaxYawLimit; } set { m_MaxYawLimit = value; if (Application.isPlaying) { if (m_LockYaw) { UpdateLockedYawAngle(); } enabled = IsActive(); } } } public bool LockYaw { get { return m_LockYaw; } set { m_LockYaw = value; if (Application.isPlaying) { if (m_LockYaw) { UpdateLockedYawAngle(); } enabled = IsActive(); } } } public bool RotateWithCrosshairs { get { return m_RotateWithCrosshairs; } set { m_RotateWithCrosshairs = value; if (Application.isPlaying) { enabled = IsActive(); } } } public float RotationSpeed { get { return m_RotationSpeed; } set { m_RotationSpeed = value; } } public bool IgnorePositionalLookOffset { get { return m_IgnorePositionalLookOffset; } set { m_IgnorePositionalLookOffset = value; if (Application.isPlaying) { enabled = IsActive(); } } } public Vector3 PositionOffset { get { return m_PositionOffset; } set { m_PositionOffset = value; } } public float MoveSpeed { get { return m_MoveSpeed; } set { m_MoveSpeed = value; } } private Transform m_Transform; private GameObject m_GameObject; private Transform m_CharacterTransform; private UltimateCharacterLocomotion m_CharacterLocomotion; private CameraController m_CameraController; private GameObject m_Character; private Transform m_CameraTransform; private GameObject[] m_FirstPersonBaseObjects; private HashSet m_ShouldActivateObject = new HashSet(); private Dictionary m_ItemBaseObjectMap = new Dictionary(); private Item[] m_EquippedItems; private float m_Pitch; private float m_Yaw; public GameObject Character { get { return m_Character; } } /// /// Initialize the default values. /// protected override void Awake() { base.Awake(); m_GameObject = gameObject; m_Transform = transform; m_CharacterLocomotion = gameObject.GetCachedParentComponent(); m_CharacterTransform = m_CharacterLocomotion.transform; m_Character = m_CharacterTransform.gameObject; var baseObjects = GetComponentsInChildren(); var count = 0; m_FirstPersonBaseObjects = new GameObject[baseObjects.Length]; for (int i = 0; i < baseObjects.Length; ++i) { if (baseObjects[i].AlwaysActive) { continue; } m_FirstPersonBaseObjects[count] = baseObjects[i].gameObject; m_FirstPersonBaseObjects[count].SetActive(false); count++; } if (count != baseObjects.Length) { System.Array.Resize(ref m_FirstPersonBaseObjects, count); } var inventory = m_Character.GetCachedComponent(); m_EquippedItems = new Item[inventory.SlotCount]; EventHandler.RegisterEvent(m_Character, "OnCharacterAttachCamera", OnAttachCamera); EventHandler.RegisterEvent(m_Character, "OnInventoryAddItem", OnAddItem); EventHandler.RegisterEvent(m_Character, "OnDeath", OnDeath); EventHandler.RegisterEvent(m_Character, "OnRespawn", OnRespawn); EventHandler.RegisterEvent(m_Character, "OnCharacterActivate", OnActivate); enabled = false; } /// /// The character has been attached to the camera. Initialze the camera-related values. /// /// The camera controller attached to the character. Can be null. private void OnAttachCamera(CameraController cameraController) { m_CameraController = cameraController; m_Transform.parent = (m_CameraController != null ? m_CameraController.Transform : m_CharacterTransform); m_Transform.localPosition = Vector3.zero; m_Transform.localRotation = Quaternion.identity; m_Pitch = m_Yaw = 0; m_CameraTransform = (m_CameraController != null ? m_CameraController.Transform : null); enabled = IsActive(); } /// /// Is the component active? /// /// True if the component is active. private bool IsActive() { // The component should be active if any values can update the rotation. return m_CameraTransform != null && (Mathf.Abs(m_MinPitchLimit - m_MaxPitchLimit) < 180 || m_LockPitch || Mathf.Abs(m_MinYawLimit - m_MaxYawLimit) < 360 || m_LockYaw || m_RotateWithCrosshairs || m_IgnorePositionalLookOffset || m_Transform.localPosition != Vector3.zero); } #if ULTIMATE_CHARACTER_CONTROLLER_MULTIPLAYER /// /// Disables the GameObject if the character is remote. /// public void Start() { // Remote players should never see the first person objects. var networkInfo = m_Character.GetComponentInParent(); if (networkInfo != null && !networkInfo.IsLocalPlayer()) { m_GameObject.SetActive(false); EventHandler.UnregisterEvent(m_Character, "OnCharacterActivate", OnActivate); } } #endif /// /// Updates the internal pitch angle while ensuring it is within the pitch limits. /// private void UpdateLockedPitchAngle() { var localRotation = MathUtility.InverseTransformQuaternion(m_CharacterTransform.rotation, m_CameraTransform.rotation).eulerAngles; if (Mathf.Abs(m_MinPitchLimit - m_MaxPitchLimit) < 180) { m_Pitch = MathUtility.ClampAngle(localRotation.x, m_MinPitchLimit, m_MaxPitchLimit); } else { m_Pitch = localRotation.x; } } /// /// Updates the internal yaw angle while ensuring it is within the yaw limits. /// private void UpdateLockedYawAngle() { var localRotation = MathUtility.InverseTransformQuaternion(m_CharacterTransform.rotation, m_CameraTransform.rotation).eulerAngles; if (Mathf.Abs(m_MinYawLimit - m_MaxYawLimit) < 360) { m_Yaw = MathUtility.ClampAngle(localRotation.y, m_MinYawLimit, m_MaxYawLimit); } else { m_Yaw = localRotation.y; } } /// /// Adjusts the location of the transform according to the enabled toggles. /// private void LateUpdate() { var localRotation = MathUtility.InverseTransformQuaternion(m_CharacterTransform.rotation, m_CameraTransform.rotation).eulerAngles; if (m_LockPitch) { localRotation.x = m_Pitch; } else if (Mathf.Abs(m_MinPitchLimit - m_MaxPitchLimit) < 180) { localRotation.x = MathUtility.ClampAngle(localRotation.x, m_MinPitchLimit, m_MaxPitchLimit); } if (m_LockYaw) { localRotation.y = m_Yaw; } else if (Mathf.Abs(m_MinYawLimit - m_MaxYawLimit) < 360) { localRotation.y = MathUtility.ClampAngle(localRotation.y, m_MinYawLimit, m_MaxYawLimit); } var rotation = MathUtility.TransformQuaternion(m_CharacterTransform.rotation, Quaternion.Euler(localRotation)); if (m_RotateWithCrosshairs) { rotation = m_CameraController.GetCrosshairsDeltaRotation() * rotation; } m_Transform.rotation = Quaternion.Slerp(m_Transform.rotation, rotation, m_RotationSpeed * m_CharacterLocomotion.TimeScale * Time.timeScale * Time.deltaTime); if (m_IgnorePositionalLookOffset && m_CameraController.ActiveViewType is FirstPersonController.Camera.ViewTypes.FirstPerson) { var firstPersonViewType = m_CameraController.ActiveViewType as FirstPersonController.Camera.ViewTypes.FirstPerson; var targetPosition = firstPersonViewType.GetTargetPosition() + m_CharacterTransform.TransformDirection(m_PositionOffset); m_Transform.position = Vector3.MoveTowards(m_Transform.position, targetPosition, m_MoveSpeed * m_CharacterLocomotion.TimeScale * Time.timeScale * Time.deltaTime); } else if (m_Transform.localPosition != Vector3.zero) { m_Transform.localPosition = Vector3.MoveTowards(m_Transform.localPosition, Vector3.zero, m_MoveSpeed * m_CharacterLocomotion.TimeScale * Time.timeScale * Time.deltaTime); } } /// /// The inventory has added the specified item. /// /// The item that was added. private void OnAddItem(Item item) { if (m_ItemBaseObjectMap.ContainsKey(item)) { return; } var firstPersonPerspective = item.GetComponent(); if (firstPersonPerspective != null && firstPersonPerspective.Object != null) { // If the item contains a first person object then the item will have a parent FirstPersonBaseObject. This object should be enabled/disabled depending on // the item active status. var firstPersonBaseObject = firstPersonPerspective.Object.transform.GetComponentInParentIncludeInactive(); // A base object may not exist in VR. if (firstPersonBaseObject == null) { return; } var baseObjects = new GameObject[firstPersonPerspective.AdditionalControlObjects.Length + 1]; baseObjects[0] = firstPersonBaseObject.gameObject; for (int i = 0; i < firstPersonPerspective.AdditionalControlObjects.Length; ++i) { baseObjects[i + 1] = firstPersonPerspective.AdditionalControlObjects[i]; } m_ItemBaseObjectMap.Add(item, baseObjects); } } /// /// The specified item will be equipped. /// public void StartEquip(Item item, int slotID) { m_EquippedItems[slotID] = item; CheckActiveBaseObjects(); } /// /// An item has been unequipped. /// public void UnequipItem(Item item, int slotID) { if (item != m_EquippedItems[slotID]) { return; } m_EquippedItems[slotID] = null; CheckActiveBaseObjects(); } /// /// Loops through the base objects determining if it should be active. /// private void CheckActiveBaseObjects() { // Loop through the equipped items to determine which base objects should be activated. // Once the loop is complete do the activation/deactivation based on the equipped items. m_ShouldActivateObject.Clear(); for (int i = 0; i < m_EquippedItems.Length; ++i) { if (m_EquippedItems[i] != null) { if (!m_ItemBaseObjectMap.TryGetValue(m_EquippedItems[i], out var baseObjects)) { Debug.LogError($"Error: Unable to find the base object for item {m_EquippedItems[i].name}. Ensure the item specifies a base object under the First Person Perspective Item component."); continue; } for (int j = 0; j < baseObjects.Length; ++j) { m_ShouldActivateObject.Add(baseObjects[j]); } } } for (int i = 0; i < m_FirstPersonBaseObjects.Length; ++i) { m_FirstPersonBaseObjects[i].SetActive(m_ShouldActivateObject.Contains(m_FirstPersonBaseObjects[i])); } } /// /// The character has died. /// /// The position of the force. /// The amount of force which killed the character. /// The GameObject that killed the character. private void OnDeath(Vector3 position, Vector3 force, GameObject attacker) { enabled = false; } /// /// The character has respawned. /// private void OnRespawn() { enabled = m_CameraTransform != null && (m_LockPitch || m_LockYaw || m_RotateWithCrosshairs); } /// /// The character has been activated or deactivated. /// /// Was the character activated? private void OnActivate(bool activate) { m_GameObject.SetActive(activate); } /// /// The GameObject was destroyed. Unregister for any registered events. /// private void OnDestroy() { EventHandler.UnregisterEvent(m_Character, "OnCharacterAttachCamera", OnAttachCamera); EventHandler.UnregisterEvent(m_Character, "OnInventoryAddItem", OnAddItem); EventHandler.UnregisterEvent(m_Character, "OnDeath", OnDeath); EventHandler.UnregisterEvent(m_Character, "OnRespawn", OnRespawn); EventHandler.UnregisterEvent(m_Character, "OnCharacterActivate", OnActivate); } } }