/// --------------------------------------------- /// Ultimate Character Controller /// Copyright (c) Opsive. All Rights Reserved. /// https://www.opsive.com /// --------------------------------------------- namespace Opsive.UltimateCharacterController.UI { using Opsive.Shared.Events; using Opsive.Shared.Game; using Opsive.UltimateCharacterController.Camera; using Opsive.UltimateCharacterController.Character; using Opsive.UltimateCharacterController.Items; using Opsive.UltimateCharacterController.Utility; using UnityEngine; using UnityEngine.UI; /// /// The CrosshairsMonitor will update the UI for the crosshair. /// public class CrosshairsMonitor : CharacterMonitor { #if UNITY_EDITOR [Tooltip("Draw a debug line to see the direction that the crosshairs is looking (editor only).")] [SerializeField] protected bool m_DebugDrawLookRay; #endif [Tooltip("The radius of the crosshair's collision sphere to detect if it is targetting an enemy.")] [SerializeField] protected float m_CollisionRadius = 0.05f; [Tooltip("The maximum number of colliders that the crosshairs can detect.")] [SerializeField] protected int m_MaxCollisionCount = 40; [Tooltip("Specifies if the crosshairscan detect triggers.")] [SerializeField] protected QueryTriggerInteraction m_TriggerInteraction = QueryTriggerInteraction.Ignore; [Tooltip("The crosshairs used when the item doesn't specify a crosshairs.")] [SerializeField] protected Sprite m_DefaultSprite; [Tooltip("The default color of the crosshairs.")] [SerializeField] protected Color m_DefaultColor = Color.white; [Tooltip("The color of the crosshairs when a target is in sight.")] [SerializeField] protected Color m_TargetColor = Color.red; [Tooltip("A reference to the image used for the center crosshairs.")] [SerializeField] protected Image m_CenterCrosshairsImage; [Tooltip("A reference to the image used for the left crosshairs.")] [SerializeField] protected Image m_LeftCrosshairsImage; [Tooltip("A reference to the image used for the top crosshairs.")] [SerializeField] protected Image m_TopCrosshairsImage; [Tooltip("A reference to the image used for the right crosshairs.")] [SerializeField] protected Image m_RightCrosshairsImage; [Tooltip("A reference to the image used for the bottom crosshairs.")] [SerializeField] protected Image m_BottomCrosshairsImage; [Tooltip("Should the crosshairs be disabled when the character dies?")] [SerializeField] protected bool m_DisableOnDeath = true; public float CollisionRadius { get { return m_CollisionRadius; } set { m_CollisionRadius = value; } } public QueryTriggerInteraction TriggerInteraction { get { return m_TriggerInteraction; } set { m_TriggerInteraction = value; } } public Color DefaultColor { get { return m_DefaultColor; } set { m_DefaultColor = value; } } public Color TargetColor { get { return m_TargetColor; } set { m_TargetColor = value; } } public bool DisableOnDeath { get { return m_DisableOnDeath; } set { m_DisableOnDeath = value; } } private GameObject m_GameObject; private UnityEngine.Camera m_Camera; private CameraController m_CameraController; private AimAssist m_AimAssist; private Transform m_CharacterTransform; private CharacterLayerManager m_CharacterLayerManager; private UltimateCharacterLocomotion m_CharacterLocomotion; private RectTransform m_CenterRectTransform; private RectTransform m_LeftRectTransform; private RectTransform m_TopRectTransform; private RectTransform m_RightRectTransform; private RectTransform m_BottomRectTransform; private RaycastHit[] m_RaycastHits; private UnityEngineUtility.RaycastHitComparer m_RaycastHitComparer = new UnityEngineUtility.RaycastHitComparer(); private Item m_EquippedItem; private float m_CurrentCrosshairsSpread; private float m_TargetCrosshairsSpread; private float m_CrosshairsSpreadVelocity; private bool m_Aiming; private bool m_EnableImage; private int m_EquippedItemCount; /// /// Initialize the default values. /// protected override void Awake() { base.Awake(); m_GameObject = gameObject; if (m_CenterCrosshairsImage == null) { m_CenterCrosshairsImage = GetComponent(); } m_CenterCrosshairsImage.sprite = m_DefaultSprite; m_CenterRectTransform = m_CenterCrosshairsImage.GetComponent(); if ((m_CenterCrosshairsImage.enabled = (m_DefaultSprite != null))) { UnityEngineUtility.SizeSprite(m_CenterCrosshairsImage.sprite, m_CenterRectTransform); } if (m_LeftCrosshairsImage != null) m_LeftRectTransform = m_LeftCrosshairsImage.GetComponent(); if (m_TopCrosshairsImage != null) m_TopRectTransform = m_TopCrosshairsImage.GetComponent(); if (m_RightCrosshairsImage != null) m_RightRectTransform = m_RightCrosshairsImage.GetComponent(); if (m_BottomCrosshairsImage != null) m_BottomRectTransform = m_BottomCrosshairsImage.GetComponent(); m_RaycastHits = new RaycastHit[m_MaxCollisionCount]; // Setup the crosshairs defaults. ResetMonitor(); } /// /// Attaches the monitor to the specified character. /// /// The character to attach the monitor to. protected override void OnAttachCharacter(GameObject character) { if (m_Character != null) { EventHandler.UnregisterEvent(m_Character, "OnAbilityWillEquipItem", OnEquipItem); EventHandler.UnregisterEvent(m_Character, "OnItemUpdateDominantItem", OnUpdateDominantItem); EventHandler.UnregisterEvent(m_Character, "OnAbilityUnequipItemComplete", OnUnequipItem); EventHandler.UnregisterEvent(m_Character, "OnInventoryRemoveItem", OnUnequipItem); EventHandler.UnregisterEvent(m_Character, "OnAddCrosshairsSpread", OnAddCrosshairsSpread); EventHandler.UnregisterEvent(m_Character, "OnAimAbilityStart", OnAim); EventHandler.UnregisterEvent(m_Character, "OnDeath", OnDeath); EventHandler.UnregisterEvent(m_Character, "OnRespawn", OnRespawn); ResetMonitor(); } base.OnAttachCharacter(character); if (m_Character == null) { return; } m_Camera = UnityEngineUtility.FindCamera(m_Character); m_CameraController = m_Camera.gameObject.GetCachedComponent(); m_CameraController.SetCrosshairs(transform); m_AimAssist = m_Camera.GetComponent(); m_CharacterTransform = m_Character.transform; m_CharacterLayerManager = m_Character.GetCachedComponent(); m_CharacterLocomotion = m_Character.GetCachedComponent(); m_EnableImage = false; EventHandler.RegisterEvent(m_Character, "OnAbilityWillEquipItem", OnEquipItem); EventHandler.RegisterEvent(m_Character, "OnItemUpdateDominantItem", OnUpdateDominantItem); EventHandler.RegisterEvent(m_Character, "OnAbilityUnequipItemComplete", OnUnequipItem); EventHandler.RegisterEvent(m_Character, "OnInventoryRemoveItem", OnUnequipItem); EventHandler.RegisterEvent(m_Character, "OnAddCrosshairsSpread", OnAddCrosshairsSpread); EventHandler.RegisterEvent(m_Character, "OnAimAbilityStart", OnAim); EventHandler.RegisterEvent(m_Character, "OnDeath", OnDeath); EventHandler.RegisterEvent(m_Character, "OnRespawn", OnRespawn); // An item may already be equipped. var inventory = m_Character.GetCachedComponent(); if (inventory != null) { for (int i = 0; i < inventory.SlotCount; ++i) { var item = inventory.GetActiveItem(i); if (item != null) { OnEquipItem(item, i); } } } } /// /// Determine any targets that are within the crosshairs raycast. /// private void Update() { var crosshairsColor = m_DefaultColor; var crosshairsRay = m_Camera.ScreenPointToRay(m_CenterRectTransform.position); Transform target = null; // Prevent the ray between the character and the camera from causing a false collision. if (!m_CharacterLocomotion.FirstPersonPerspective) { var direction = m_CharacterTransform.InverseTransformPoint(crosshairsRay.origin); direction.y = 0; crosshairsRay.origin = crosshairsRay.GetPoint(direction.magnitude); } #if UNITY_EDITOR // Visualize the direction of the look direction. if (m_DebugDrawLookRay) { Debug.DrawRay(crosshairsRay.origin, crosshairsRay.direction * m_CameraController.LookDirectionDistance, Color.white); } #endif var hitCount = Physics.SphereCastNonAlloc(crosshairsRay, m_CollisionRadius, m_RaycastHits, m_CameraController.LookDirectionDistance, m_CharacterLayerManager.IgnoreInvisibleLayers, m_TriggerInteraction); #if UNITY_EDITOR if (hitCount == m_MaxCollisionCount) { Debug.LogWarning("Warning: The crosshairs detected the maximum number of objects. Consider increasing the Max Collision Count on the Crosshairs Monitor."); } #endif if (hitCount > 0) { for (int i = 0; i < hitCount; ++i) { var closestRaycastHit = QuickSelect.SmallestK(m_RaycastHits, hitCount, i, m_RaycastHitComparer); var closestRaycastHitTransform = closestRaycastHit.transform; // The crosshairs cannot hit the character that is attached to the camera. if (closestRaycastHitTransform.IsChildOf(m_CharacterTransform)) { continue; } if (MathUtility.InLayerMask(closestRaycastHitTransform.gameObject.layer, m_CharacterLayerManager.EnemyLayers)) { target = closestRaycastHitTransform; crosshairsColor = m_TargetColor; } break; } } if (m_AimAssist != null) { m_AimAssist.SetTarget(target); } if (m_EquippedItem != null) { m_CurrentCrosshairsSpread = Mathf.SmoothDamp(m_CurrentCrosshairsSpread, m_TargetCrosshairsSpread, ref m_CrosshairsSpreadVelocity, m_EquippedItem.QuadrantSpreadDamping); } m_CenterCrosshairsImage.color = crosshairsColor; if (m_LeftCrosshairsImage != null && m_LeftCrosshairsImage.enabled) { m_LeftCrosshairsImage.color = crosshairsColor; PositionSprite(m_LeftRectTransform, -m_EquippedItem.QuadrantOffset - m_CurrentCrosshairsSpread, 0); } if (m_TopCrosshairsImage != null && m_TopCrosshairsImage.enabled) { m_TopCrosshairsImage.color = crosshairsColor; PositionSprite(m_TopRectTransform, 0, m_EquippedItem.QuadrantOffset + m_CurrentCrosshairsSpread); } if (m_RightCrosshairsImage != null && m_RightCrosshairsImage.enabled) { m_RightCrosshairsImage.color = crosshairsColor; PositionSprite(m_RightRectTransform, m_EquippedItem.QuadrantOffset + m_CurrentCrosshairsSpread, 0); } if (m_BottomCrosshairsImage != null && m_BottomCrosshairsImage.enabled) { m_BottomCrosshairsImage.color = crosshairsColor; PositionSprite(m_BottomRectTransform, 0, -m_EquippedItem.QuadrantOffset - m_CurrentCrosshairsSpread); } var enableImage = !m_Aiming || (m_EquippedItem != null && m_EquippedItem.ShowCrosshairsOnAim); if (enableImage != m_EnableImage) { m_EnableImage = enableImage; EnableCrosshairsImage(enableImage); } } /// /// An item has been equipped. /// /// The equipped item. /// The slot that the item now occupies. private void OnEquipItem(Item item, int slotID) { if (!item.DominantItem) { return; } m_CurrentCrosshairsSpread = m_TargetCrosshairsSpread = 0; m_EquippedItem = item; m_CenterCrosshairsImage.sprite = m_EquippedItem.CenterCrosshairs != null ? m_EquippedItem.CenterCrosshairs : m_DefaultSprite; if (m_CenterCrosshairsImage.sprite != null) { m_CenterCrosshairsImage.enabled = !m_Aiming || m_EquippedItem.ShowCrosshairsOnAim; UnityEngineUtility.SizeSprite(m_CenterCrosshairsImage.sprite, m_CenterRectTransform); } else { m_CenterCrosshairsImage.enabled = false; } if (m_LeftCrosshairsImage != null) { m_LeftCrosshairsImage.sprite = m_EquippedItem.LeftCrosshairs; if (m_LeftCrosshairsImage.sprite != null) { m_LeftCrosshairsImage.enabled = !m_Aiming || m_EquippedItem.ShowCrosshairsOnAim; PositionSprite(m_LeftRectTransform, -m_EquippedItem.QuadrantOffset, 0); UnityEngineUtility.SizeSprite(m_LeftCrosshairsImage.sprite, m_LeftRectTransform); } else { m_LeftCrosshairsImage.enabled = false; } } if (m_TopCrosshairsImage != null) { m_TopCrosshairsImage.sprite = m_EquippedItem.TopCrosshairs; if (m_TopCrosshairsImage.sprite != null) { m_TopCrosshairsImage.enabled = !m_Aiming || m_EquippedItem.ShowCrosshairsOnAim; PositionSprite(m_TopRectTransform, 0, m_EquippedItem.QuadrantOffset); UnityEngineUtility.SizeSprite(m_TopCrosshairsImage.sprite, m_TopRectTransform); } else { m_TopCrosshairsImage.enabled = false; } } if (m_RightCrosshairsImage != null) { m_RightCrosshairsImage.sprite = m_EquippedItem.RightCrosshairs; if (m_RightCrosshairsImage.sprite != null) { m_RightCrosshairsImage.enabled = !m_Aiming || m_EquippedItem.ShowCrosshairsOnAim; PositionSprite(m_RightRectTransform, m_EquippedItem.QuadrantOffset, 0); UnityEngineUtility.SizeSprite(m_RightCrosshairsImage.sprite, m_RightRectTransform); } else { m_RightCrosshairsImage.enabled = false; } } if (m_BottomCrosshairsImage != null) { m_BottomCrosshairsImage.sprite = m_EquippedItem.BottomCrosshairs; if (m_BottomCrosshairsImage.sprite != null) { m_BottomCrosshairsImage.enabled = !m_Aiming || m_EquippedItem.ShowCrosshairsOnAim; PositionSprite(m_BottomRectTransform, 0, -m_EquippedItem.QuadrantOffset); UnityEngineUtility.SizeSprite(m_BottomCrosshairsImage.sprite, m_BottomRectTransform); } else { m_BottomCrosshairsImage.enabled = false; } } m_EquippedItemCount++; } /// /// The DominantItem field has been updated for the specified item. /// /// The Item whose DominantItem field was updated. /// True if the item is now a dominant item. private void OnUpdateDominantItem(Item item, bool dominantItem) { if (item.DominantItem && item.IsActive()) { OnEquipItem(item, -1); } else if (m_EquippedItem == item) { m_EquippedItemCount--; if (m_EquippedItemCount == 0) { ResetMonitor(); } } } /// /// An item has been unequipped. /// /// The unequipped item. /// The slot that the item previously occupied. private void OnUnequipItem(Item item, int slotID) { if (item != m_EquippedItem) { return; } m_EquippedItemCount--; if (m_EquippedItemCount == 0) { ResetMonitor(); } } /// /// Resets the monitor back to the default state. /// private void ResetMonitor() { m_EquippedItem = null; m_EquippedItemCount = 0; m_CenterCrosshairsImage.sprite = m_DefaultSprite; m_CenterCrosshairsImage.enabled = m_DefaultSprite != null; if (m_LeftCrosshairsImage != null) { m_LeftCrosshairsImage.sprite = null; m_LeftCrosshairsImage.enabled = false; } if (m_TopCrosshairsImage != null) { m_TopCrosshairsImage.sprite = null; m_TopCrosshairsImage.enabled = false; } if (m_RightCrosshairsImage != null) { m_RightCrosshairsImage.sprite = null; m_RightCrosshairsImage.enabled = false; } if (m_BottomCrosshairsImage != null) { m_BottomCrosshairsImage.sprite = null; m_BottomCrosshairsImage.enabled = false; } } /// /// Positions the sprite according to the specified x and y position. /// /// The transform to position. /// The x position of the sprite. /// The y position of the sprite. private void PositionSprite(RectTransform spriteRectTransform, float xPosition, float yPosition) { var position = spriteRectTransform.localPosition; position.x = xPosition; position.y = yPosition; spriteRectTransform.localPosition = position; } /// /// Adds a force to the quadrant recoil spring. /// /// Is the spread just starting? /// Is the spread being added from a recoil? private void OnAddCrosshairsSpread(bool start, bool fromRecoil) { if (m_EquippedItem == null) { return; } if (start) { m_CurrentCrosshairsSpread = fromRecoil ? m_EquippedItem.MaxQuadrantSpread : 0; m_TargetCrosshairsSpread = fromRecoil ? 0 : m_EquippedItem.MaxQuadrantSpread; } else { m_TargetCrosshairsSpread = 0; } } /// /// The Aim ability has started or stopped. /// /// Has the Aim ability started? /// Was the ability started from input? private void OnAim(bool aim, bool inputStart) { if (!inputStart) { return; } m_Aiming = aim; } /// /// Enables or disables the crosshairs image. /// /// Should the crosshairs be enabled? private void EnableCrosshairsImage(bool enable) { m_CenterCrosshairsImage.enabled = enable && m_CenterCrosshairsImage.sprite != null; if (m_LeftCrosshairsImage != null) m_LeftCrosshairsImage.enabled = enable && m_LeftCrosshairsImage.sprite != null; if (m_TopCrosshairsImage != null) m_TopCrosshairsImage.enabled = enable && m_TopCrosshairsImage.sprite != null; if (m_RightCrosshairsImage != null) m_RightCrosshairsImage.enabled = enable && m_RightCrosshairsImage.sprite != null; if (m_BottomCrosshairsImage != null) m_BottomCrosshairsImage.enabled = enable && m_BottomCrosshairsImage.sprite != null; if (!enable) { m_TargetCrosshairsSpread = m_CurrentCrosshairsSpread = 0; } } /// /// 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) { if (m_DisableOnDeath) { m_GameObject.SetActive(false); } } /// /// The character has respawned. /// private void OnRespawn() { if (m_DisableOnDeath && base.CanShowUI()) { m_GameObject.SetActive(true); } // Force the crosshairs to update so the color will be correct. Update(); } /// /// Can the UI be shown? /// /// True if the UI can be shown. protected override bool CanShowUI() { return base.CanShowUI() && (!m_DisableOnDeath || m_CharacterLocomotion.Alive); } } }