using System; using System.Collections.Generic; using System.Linq; using UnityEngine; using UnityEngine.EventSystems; using UnityEngine.UI; using Studio; namespace Studio.Manage { [DefaultExecutionOrder(int.MinValue)] public class Raycaster : MonoBehaviour { public enum EventType { FirstEnter, FirstClick, FirstExit, Enter, Click, Exit, Stay, RightClick, FirstRightClick, FirstStay, FirstLeftClickOnly, FirstRightClickOnly, } PointerEventData pointerEvent = new(EventSystem.current); List uiRaycastResults = new(); RaycastHit[] hitInfo = new RaycastHit[100]; RaycastHit[] tempInfo; bool onLeftClick; bool onRightClick; Dictionary firstHit = new(); Dictionary tempFirstHit = new(); Dictionary fth = new(); int hitCount; Camera cam; public RaycastHit hit => hitInfo[0]; HashSet hitTransform = new(); HashSet tempHit = new(); Dictionary transformToHitinfo = new(); List<(Action, RaycastHit, Component)> eventList = new(); public float uiHoverTime; float uiHoverTimer; #pragma warning disable IDE0044 // 읽기 전용 한정자 추가 #pragma warning disable CS0649 // 'Raycaster.onUIHoverEvent' 필드에는 할당되지 않으므로 항상 null 기본값을 사용합니다. public Action onUIHoverEvent; public Action onUIHoverExitEvent; #pragma warning restore CS0649 #pragma warning restore IDE0044 Vector2 prevMousePos; public override void AfterAwake() { cam = Camera.main; } void Update() { onLeftClick = Input.GetMouseButtonDown(0); onRightClick = Input.GetMouseButtonDown(1); if (!UIRaycast()) { PhysicsRaycast(); } EventInvoking(); } void EventInvoking() { foreach(var e in eventList) { e.Item1?.Invoke(e.Item2, e.Item3); } eventList.Clear(); } bool UIRaycast() { Vector2 currentMousePos = Input.mousePosition; pointerEvent.position = currentMousePos; EventSystem.current.RaycastAll(pointerEvent, uiRaycastResults); uiRaycastResults = uiRaycastResults.Where(r => r.gameObject.GetComponent() != null).ToList(); if (uiRaycastResults.Count == 0) { ResetHoverTime(); return false; } if (onLeftClick) { } if (prevMousePos != currentMousePos) { ResetHoverTime(); prevMousePos = currentMousePos; } if (uiHoverTimer >= uiHoverTime) { onUIHoverEvent?.Invoke(uiRaycastResults[0]); } else { uiHoverTimer += Time.deltaTime; } return true; } void PhysicsRaycast() { Rayfire(); SingleCasting(); MultiCasting(); } void Rayfire() { var ray = cam.ScreenPointToRay(Input.mousePosition); //Physics.Raycast(ray, out singleHit, Mathf.Infinity); tempInfo = new RaycastHit[100]; hitCount = Physics.RaycastNonAlloc(ray, tempInfo, Mathf.Infinity); hitInfo = SortingHitInfos(tempInfo); } public bool Casting(LayerMask layer, out RaycastHit hit) { hit = new RaycastHit(); if (hitCount == 0) { return false; } foreach (var h in hitInfo) { if (h.transform == null) continue; if ((layer.value >> h.transform.gameObject.layer) == 1) { hit = h; return true; } } return false; } void SingleCasting(in RaycastHit hitInfo) { fth.TryAdd(hitInfo.transform, hitInfo); foreach (var tl in eventTable.Keys) { SingleCasting(hitInfo, tl); } } void SingleCasting(in RaycastHit hitInfo, Type tl) { if (tempFirstHit.ContainsKey(tl)) { return; } if (!hitInfo.transform.TryGetComponent(tl, out var value)) return; if (firstHit.Remove(tl, out var prev)) { if (prev.transform == hitInfo.transform) { //Debug.Log($"OnStayFirst : {prev.transform.name}"); //EventInvoke(onStayFirst, tl, hitInfo, value); Invoking(EventType.FirstStay, tl, hitInfo, value); } else { //Debug.Log($"OnExitFirst : {prev.transform.name}"); if (prev.transform.TryGetComponent(tl, out var prevValue)) { //EventInvoke(onExitFirst, tl, prev, prevValue); Invoking(EventType.FirstExit, tl, hitInfo, value); } //Debug.Log($"OnEnterFirst : {hitInfo.transform.name}"); //EventInvoke(onEnterFirst, tl, hitInfo, value); Invoking(EventType.FirstEnter, tl, hitInfo, value); fth.Remove(prev.transform); } } else { //Debug.Log($"OnEnterFirst : {hitInfo.transform.name}"); //EventInvoke(onEnterFirst, tl, hitInfo, value); Invoking(EventType.FirstEnter, tl, hitInfo, value); } fth[hitInfo.transform] = hitInfo; tempFirstHit.Add(tl, hitInfo); } void SingleCasting() { //fth.Clear(); tempFirstHit.Clear(); for (int i = 0; i < hitCount; ++i) { SingleCasting(hitInfo[i]); } FirstExitEvent(); FirstClickEvent(); FirstClickOnlyEvent(); } void FirstClickEvent() { foreach (var p in tempFirstHit) { firstHit.Add(p.Key, p.Value); if (onLeftClick) { Invoking(EventType.FirstClick, p.Key, p.Value, p.Value.transform.GetComponent(p.Key)); } if (onRightClick) { Invoking(EventType.FirstRightClick, p.Key, p.Value, p.Value.transform.GetComponent(p.Key)); } } } void FirstClickOnlyEvent() { foreach (var p in tempFirstHit) { if (onLeftClick) { Invoking(EventType.FirstLeftClickOnly, p.Key, p.Value, p.Value.transform.GetComponent(p.Key)); //EventInvoke(onLeftClickOnlyFirst, p.Key, p.Value, p.Value.transform.GetComponent(p.Key)); return; } if (onRightClick) { Invoking(EventType.FirstRightClickOnly, p.Key, p.Value, p.Value.transform.GetComponent(p.Key)); return; } } } void FirstExitEvent() { foreach (var f in firstHit) { //Debug.Log($"OnExitFirst :{f.Value.transform.name}"); if (f.Value.transform == null) continue; Invoking(EventType.FirstExit, f.Key, f.Value, f.Value.transform.GetComponent(f.Key)); fth.Remove(f.Value.transform); //tempFirstHit.Remove(f.Key); } firstHit.Clear(); } RaycastHit[] SortingHitInfos(in RaycastHit[] hitInfo) { if (hitInfo[0].transform == null) return null; var sortHitInfo = hitInfo.Where(hi => hi.transform != null).OrderBy(hi => hi.distance).ToArray(); //var sortHitInfo = hitInfo.OrderBy(hi => hi.distance).ToArray(); return sortHitInfo; } void MultiCasting() { tempHit.Clear(); for (int i = 0; i < hitCount; ++i) { var ht = hitInfo[i].transform; tempHit.Add(ht); transformToHitinfo.TryAdd(ht, hitInfo[i]); bool isStay = hitTransform.Remove(ht); foreach (var tl in eventTable.Keys) { if (!ht.TryGetComponent(tl, out var value)) continue; if (onLeftClick) { Invoking(EventType.Click, tl, hitInfo[i], value); //Debug.Log($"OnClick {tl} {value}"); } if(onRightClick) { Invoking(EventType.RightClick, tl, hitInfo[i], value); } if (!isStay) { //Debug.Log($"OnEnter {tl} {value}"); Invoking(EventType.Enter, tl, hitInfo[i], value); } else { //EventInvoke(onStayEvent_TypeLayer, tl, hitInfo[i], value); Invoking(EventType.Stay, tl, hitInfo[i], value); //Debug.Log($"OnStay {tl} {value}"); } } } foreach (var h in hitTransform) { if (h == null) continue; foreach (var tl in eventTable.Keys) { if (!h.TryGetComponent(tl, out var value)) continue; Invoking(EventType.Exit, tl, transformToHitinfo[h], value); //EventInvoke(onExitEvent_TypeLayer, tl, transformToHitinfo[h], value); //Debug.Log($"OnExit {tl} {value}"); } transformToHitinfo.Remove(h); } hitTransform.Clear(); foreach (var p in tempHit) { hitTransform.Add(p); } } void Invoking(EventType et, Type t, in RaycastHit hitInfo, Component value) { if(eventTable.TryGetValue(t, out var actionTable)) { if(actionTable.TryGetValue(et, out var action)) { eventList.Add((action, hitInfo, value)); } } } public bool IsFirstHit(Transform target) { if (hitInfo.Length == 0) return false; return hitInfo[0].transform == target; } Dictionary>> eventTable = new(); public void AddEvent(EventType et, Type layer, Action action) { if (!eventTable.ContainsKey(layer)) eventTable.Add(layer, new Dictionary>()); if (!eventTable[layer].ContainsKey(et)) eventTable[layer].Add(et, null); eventTable[layer][et] += action; } public void RemoveEvent(EventType et, Type layer, Action action) { eventTable[layer][et]-=action; } void ResetHoverTime() { onUIHoverExitEvent?.Invoke(); uiHoverTimer = 0f; } } }