using CCG.Bigfoot.InputSystem; using System.Collections.Generic; using UnityEngine; using UnityEngine.EventSystems; namespace CCG.Bigfoot.Utility { /// <summary> /// Provides utility methods for raycasting GUI and Collider /// </summary> /// <remarks> Must call Initialize before use. /// Authors: CS /// Created: 2024-01-05 /// </remarks> public static class RaycasterHelper { //static readonly string _logger = Log.GetClassLogger(); static List<GameObject> _hitsWithTag = null; static List<GameObject> _hitsWithLayer = null; static Camera _camera; static bool _isInitialized; /// <summary> /// Initialize must be called before the system is used on scene-by-scene basis. /// </summary> /// <param name="raycastCam"></param> public static void Initialize(Camera raycastCam) { _isInitialized = false; _camera = raycastCam; //--Camera.main may return the wrong camera, which could result in bad raycast results. _hitsWithTag = new List<GameObject>(8); _hitsWithLayer = new List<GameObject>(8); if (_camera == null) Debug.LogWarning("[Initialize] - passed a null camera. Will default to Camera.main, which may result in bad results!"); if (InputManager.Instance == null) Debug.LogError("[Initialize] - Failed to initialize. Raycasting won't work."); else _isInitialized = true; } public static void ClearAllMouseOverTag() { _hitsWithTag.Clear(); } public static void ClearAllMouseOverLayerIndex() { _hitsWithLayer.Clear(); } /// <summary> /// Returns true if pointer is over at least one GameObject with tag. Uses Physics.Raycast. /// Use if only one collider is in your target layer. /// </summary> /// <param name="tag"></param> /// <returns></returns> public static bool IsMouseOverTag(string tag, int layerIndex) { if(!_isInitialized) return false; int layermask = 1 << layerIndex; Vector3 pointerPos = InputManager.Instance.InputModule.point.action.ReadValue<Vector2>(); if (_camera == null) _camera = Camera.main; Ray origin = _camera.ScreenPointToRay(pointerPos); try { //return Physics.Raycast(origin, out RaycastHit hit, Mathf.Infinity, layermask) && hit.transform.CompareTag(tag); bool result = Physics.Raycast(origin, out RaycastHit hit, Mathf.Infinity, layermask) && hit.transform.CompareTag(tag); if (result) Debug.Log("[IsMouseOverTag] - Raycast hit object " + hit.transform.name + " with tag " + hit.transform.gameObject.tag); return result; } catch (System.Exception e) { Debug.Log("[IsMouseOverTag] - caught " + e.Message); return false; } } /// <summary> /// Returns true if pointer is over at least one GameObject with tag. Uses Physics.RaycastNonAlloc. /// Use if more than one collider is in your targeted layer. /// </summary> /// <param name="tag"></param> /// <returns></returns> public static bool IsMouseOverTagNonAlloc(string tag, int layerIndex, int numMatchingTagsMouseBeOver = 1) { if (!_isInitialized) return false; int layermask = 1 << layerIndex; Vector3 pointerPos = InputManager.Instance.InputModule.point.action.ReadValue<Vector2>(); if (_camera == null) _camera = Camera.main; Ray origin = _camera.ScreenPointToRay(pointerPos); RaycastHit[] hits = new RaycastHit[5]; int numHits = Physics.RaycastNonAlloc(origin, hits, Mathf.Infinity, layermask); int numMatchingTagHits = 0; if (numHits < 1) return false; for (int i = 0; i < numHits; i++) { //Log.Debug(_logger, "[IsMouseOverTagNonAlloc] - RaycastNonAlloc hit object " + hits[i].transform.name + " with tag " + hits[i].transform.gameObject.tag); if (hits[i].transform.CompareTag(tag)) numMatchingTagHits++; } return numMatchingTagHits >= numMatchingTagsMouseBeOver; } /// <summary> /// Uses Physics.Raycast to return true if the pointer is over a collider in targetMask. /// </summary> /// <param name="tag"></param> /// <returns></returns> public static bool IsMouseOverLayerMask(LayerMask targetMask, Vector2 mousePos) { if (!_isInitialized) return false; Ray ray = _camera.ScreenPointToRay(mousePos); return Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity, targetMask); } /// <summary> /// Uses Physics.Raycast to return true if the pointer is over a collider in targetMask. /// Attempts to retrieve the Type passed in using reflection first looking at self then upwards. /// Outs back the component if it was found. /// </summary> /// <typeparam name="T"></typeparam> /// <param name="targetMask"> The LayerMask you're sending a ray through.</param> /// <param name="mousePos">The screen space coordinates representing the mouse position.</param> /// <param name="targetComponent">The component you are expecting to grab.</param> /// <returns></returns> public static bool IsMouseOverLayerMask<T>(LayerMask targetMask, Vector2 mousePos, out T targetComponent) where T : Component { targetComponent = null; if (!_isInitialized) return false; Ray ray = _camera.ScreenPointToRay(mousePos); if (Physics.Raycast(ray, out RaycastHit hit, Mathf.Infinity, targetMask)) { T foundComponent = hit.collider.GetComponentInParent<T>(); if (foundComponent != null) { targetComponent = foundComponent; return true; } } return false; } /// <summary> /// Returns true if pointer is over at least one GameObject with tag. /// </summary> /// <param name="tag"></param> /// <returns></returns> public static bool IsMouseOverTag(string tag) { if (!_isInitialized) return false; List<RaycastResult> results = RaycastAll(); if(results == null) return false; foreach (RaycastResult hit in results) { if (hit.isValid && hit.gameObject.CompareTag(tag)) return true; } return false; } /// <summary> /// Returns true if pointer is over at least one GameObject with tag and out's it back. /// </summary> /// <param name="tag"></param> /// <param name="hitGo"></param> /// <returns></returns> public static bool GetFirstIsMouseOverTag(string tag, out GameObject hitGo) { if (!_isInitialized) { hitGo = null; return false; } List<RaycastResult> results = RaycastAll(); if (results == null) { hitGo = null; return false; } foreach (RaycastResult hit in results) { if (hit.isValid && hit.gameObject.CompareTag(tag)) { hitGo = hit.gameObject; return true; } } hitGo = null; return false; } /// <summary> /// Returns true if pointer is over at least 1 Gameobject with the tag and out's a supplied list of all GameObjects with tag hit. /// Callers must call the ClearAllMouseOverTag() after results are operated on. /// </summary> /// <param name="tag"></param> /// <param name="hits"></param> /// <returns></returns> public static bool GetAllMouseOverTag(string tag, out List<GameObject> hits) { if (!_isInitialized) { hits = null; return false; } List<RaycastResult> results = RaycastAll(); hits = _hitsWithTag;//--use reserved memory address for this method so don't have to create new if(results == null)return false; foreach (RaycastResult hit in results) { if (hit.isValid && hit.gameObject.CompareTag(tag)) { hits.Add(hit.gameObject); } } return hits.Count > 0; } /// <summary> /// Returns true if pointer is over at least 1 Gameobject with the inspector layer index and out's a supplied list of all GameObjects with layer hit. /// Callers must call the ClearAllMouseOverLayer() after results are operated on. /// </summary> /// <param name="layerIndex"></param> /// <param name="hits"></param> /// <returns></returns> public static bool GetAllMouseOverLayerIndex(int layerIndex, out List<GameObject> hits) { if (!_isInitialized) { hits = null; return false; } List<RaycastResult> results = RaycastAll(); hits = _hitsWithLayer;//--use reserved memory address for this method so don't have to create new if (results == null) return false; foreach (RaycastResult hit in results) { if (hit.isValid && layerIndex == hit.gameObject.layer) { hits.Add(hit.gameObject); } } return hits.Count > 0; } /// <summary> /// Returns true if pointer is over at least 1 Gameobject with the LayerMask and out's a supplied list of all GameObjects with layer hit. /// Callers must call the ClearAllMouseOverLayerMask() after results are operated on. /// </summary> /// <param name="layerMask"></param> /// <param name="hits"></param> /// <returns></returns> public static bool GetAllMouseOverLayerMask(LayerMask layerMask, out List<GameObject> hits) { if (!_isInitialized) { hits = null; return false; } List<RaycastResult> results = RaycastAll(); hits = _hitsWithLayer;//--use reserved memory address for this method so don't have to create new if (results == null) return false; foreach (RaycastResult hit in results) { //--Convert layer to bitfield for comparison int hitLayerMask = (1 << hit.gameObject.layer); if (hit.isValid && (layerMask & hitLayerMask) > 0) { hits.Add(hit.gameObject); } } return hits.Count > 0; } /// <summary> /// Performs a RaycastAll with the current EventSystem to tell you the UI that was underneath the pointer. /// </summary> /// <returns></returns> public static List<RaycastResult> RaycastAll() { if (!_isInitialized || InputManager.Instance == null) { Debug.LogError("[RaycastAll] - ERROR"); return null; } PointerEventData pointerData = new(EventSystem.current) { pointerId = -1, }; pointerData.position = InputManager.Instance.InputModule.point.action.ReadValue<Vector2>(); List<RaycastResult> results = new(); EventSystem.current.RaycastAll(pointerData, results); return results; } } }