HighlightTrigger.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using UnityEngine;
  5. using UnityEngine.EventSystems;
  6. namespace HighlightPlus {
  7. public enum TriggerMode {
  8. ColliderEventsOnlyOnThisObject = 0,
  9. RaycastOnThisObjectAndChildren = 1,
  10. Volume = 2
  11. }
  12. public enum RayCastSource {
  13. MousePosition = 0,
  14. CameraDirection = 1
  15. }
  16. [RequireComponent(typeof(HighlightEffect))]
  17. [ExecuteInEditMode]
  18. [HelpURL("https://kronnect.com/guides/highlight-plus-introduction/")]
  19. public class HighlightTrigger : MonoBehaviour {
  20. [Tooltip("Enables highlight when pointer is over this object.")]
  21. public bool highlightOnHover = true;
  22. [Tooltip("Used to trigger automatic highlighting including children objects.")]
  23. #if ENABLE_INPUT_SYSTEM
  24. public TriggerMode triggerMode = TriggerMode.RaycastOnThisObjectAndChildren;
  25. #else
  26. public TriggerMode triggerMode = TriggerMode.ColliderEventsOnlyOnThisObject;
  27. #endif
  28. public Camera raycastCamera;
  29. public RayCastSource raycastSource = RayCastSource.MousePosition;
  30. public LayerMask raycastLayerMask = -1;
  31. [Tooltip("Minimum distance for target.")]
  32. public float minDistance;
  33. [Tooltip("Maximum distance for target. 0 = infinity")]
  34. public float maxDistance;
  35. [Tooltip("Blocks interaction if pointer is over an UI element")]
  36. public bool respectUI = true;
  37. public LayerMask volumeLayerMask;
  38. const int MAX_RAYCAST_HITS = 100;
  39. [Tooltip("If the object will be selected by clicking with mouse or tapping on it.")]
  40. public bool selectOnClick;
  41. [Tooltip("Profile to use when object is selected by clicking on it.")]
  42. public HighlightProfile selectedProfile;
  43. [Tooltip("Profile to use whtn object is selected and highlighted.")]
  44. public HighlightProfile selectedAndHighlightedProfile;
  45. [Tooltip("Automatically deselects any other selected object prior selecting this one")]
  46. public bool singleSelection;
  47. [Tooltip("Toggles selection on/off when clicking object")]
  48. public bool toggle;
  49. [Tooltip("Keeps current selection when clicking outside of any selectable object")]
  50. public bool keepSelection = true;
  51. [NonSerialized] public Collider[] colliders;
  52. [NonSerialized] public Collider2D[] colliders2D;
  53. public bool hasColliders => (colliders != null && colliders.Length > 0);
  54. public bool hasColliders2D => (colliders2D != null && colliders2D.Length > 0);
  55. UnityEngine.Object currentCollider;
  56. static RaycastHit[] hits;
  57. static RaycastHit2D[] hits2D;
  58. HighlightEffect hb;
  59. public HighlightEffect highlightEffect { get { return hb; } }
  60. public event OnObjectSelectionEvent OnObjectSelected;
  61. public event OnObjectSelectionEvent OnObjectUnSelected;
  62. public event OnObjectHighlightEvent OnObjectHighlightStart;
  63. public event OnObjectHighlightEvent OnObjectHighlightEnd;
  64. TriggerMode currentTriggerMode;
  65. [RuntimeInitializeOnLoadMethod]
  66. static void DomainReloadDisabledSupport() {
  67. HighlightManager.selectedObjects.Clear();
  68. }
  69. void OnEnable() {
  70. Init();
  71. }
  72. private void OnValidate() {
  73. if (currentTriggerMode != triggerMode) {
  74. UpdateTriggers();
  75. }
  76. }
  77. void UpdateTriggers() {
  78. currentTriggerMode = triggerMode;
  79. if (currentTriggerMode == TriggerMode.RaycastOnThisObjectAndChildren) {
  80. colliders = GetComponentsInChildren<Collider>();
  81. colliders2D = GetComponentsInChildren<Collider2D>();
  82. if (hits == null || hits.Length != MAX_RAYCAST_HITS) {
  83. hits = new RaycastHit[MAX_RAYCAST_HITS];
  84. }
  85. if (hits2D == null || hits2D.Length != MAX_RAYCAST_HITS) {
  86. hits2D = new RaycastHit2D[MAX_RAYCAST_HITS];
  87. }
  88. if (Application.isPlaying) {
  89. StopAllCoroutines();
  90. if (gameObject.activeInHierarchy) {
  91. StartCoroutine(DoRayCast());
  92. }
  93. }
  94. }
  95. }
  96. public void Init() {
  97. if (raycastCamera == null) {
  98. raycastCamera = HighlightManager.GetCamera();
  99. }
  100. UpdateTriggers();
  101. if (hb == null) {
  102. hb = GetComponent<HighlightEffect>();
  103. }
  104. InputProxy.Init();
  105. }
  106. void Start() {
  107. UpdateTriggers();
  108. if (triggerMode == TriggerMode.RaycastOnThisObjectAndChildren) {
  109. if (raycastCamera == null) {
  110. raycastCamera = HighlightManager.GetCamera();
  111. if (raycastCamera == null) {
  112. Debug.LogError("Highlight Trigger on " + gameObject.name + ": no camera found!");
  113. }
  114. }
  115. } else {
  116. Collider collider = GetComponent<Collider>();
  117. if (collider == null) {
  118. if (GetComponent<MeshFilter>() != null) {
  119. gameObject.AddComponent<MeshCollider>();
  120. }
  121. }
  122. }
  123. }
  124. IEnumerator DoRayCast() {
  125. yield return null;
  126. WaitForEndOfFrame w = new WaitForEndOfFrame();
  127. while (triggerMode == TriggerMode.RaycastOnThisObjectAndChildren) {
  128. if (raycastCamera == null) {
  129. yield return null;
  130. continue;
  131. }
  132. int hitCount;
  133. bool hit = false;
  134. #if ENABLE_INPUT_SYSTEM
  135. if (respectUI) {
  136. EventSystem es = EventSystem.current;
  137. if (es == null) {
  138. es = CreateEventSystem();
  139. }
  140. List<RaycastResult> raycastResults = new List<RaycastResult>();
  141. PointerEventData eventData = new PointerEventData(es);
  142. Vector3 cameraPos = raycastCamera.transform.position;
  143. if (raycastSource == RayCastSource.MousePosition) {
  144. eventData.position = InputProxy.mousePosition;
  145. } else {
  146. eventData.position = new Vector2(raycastCamera.pixelWidth * 0.5f, raycastCamera.pixelHeight * 0.5f);
  147. }
  148. es.RaycastAll(eventData, raycastResults);
  149. hitCount = raycastResults.Count;
  150. // check UI blocker
  151. bool blocked = false;
  152. for (int k = 0; k < hitCount; k++) {
  153. RaycastResult rr = raycastResults[k];
  154. if (rr.module is UnityEngine.UI.GraphicRaycaster) {
  155. blocked = true;
  156. break;
  157. }
  158. }
  159. if (blocked) {
  160. yield return null;
  161. continue;
  162. }
  163. // look for our gameobject
  164. for (int k = 0; k < hitCount; k++) {
  165. RaycastResult rr = raycastResults[k];
  166. float distance = Vector3.Distance(rr.worldPosition, cameraPos);
  167. if (distance < minDistance || (maxDistance > 0 && distance > maxDistance)) continue;
  168. GameObject theGameObject = rr.gameObject;
  169. for (int c = 0; c < colliders.Length; c++) {
  170. if (colliders[c].gameObject == theGameObject) {
  171. Collider theCollider = colliders[c];
  172. hit = true;
  173. if (selectOnClick && InputProxy.GetMouseButtonDown(0)) {
  174. ToggleSelection();
  175. break;
  176. } else if (theCollider != currentCollider) {
  177. SwitchCollider(theCollider);
  178. k = hitCount;
  179. break;
  180. }
  181. }
  182. }
  183. }
  184. }
  185. // if not blocked by UI and no hit found, fallback to raycast (required if no PhysicsRaycaster is present on the camera)
  186. #endif
  187. Ray ray;
  188. if (raycastSource == RayCastSource.MousePosition) {
  189. #if !ENABLE_INPUT_SYSTEM
  190. if (!CanInteract()) {
  191. yield return null;
  192. continue;
  193. }
  194. #endif
  195. ray = raycastCamera.ScreenPointToRay(InputProxy.mousePosition);
  196. } else {
  197. ray = new Ray(raycastCamera.transform.position, raycastCamera.transform.forward);
  198. }
  199. bool isMouseButonDown = InputProxy.GetMouseButtonDown(0);
  200. if (hasColliders2D) {
  201. if (maxDistance > 0) {
  202. hitCount = Physics2D.GetRayIntersectionNonAlloc(ray, hits2D, maxDistance, raycastLayerMask);
  203. } else {
  204. hitCount = Physics2D.GetRayIntersectionNonAlloc(ray, hits2D, float.MaxValue, raycastLayerMask);
  205. }
  206. for (int k = 0; k < hitCount; k++) {
  207. if (Vector3.Distance(hits2D[k].point, ray.origin) < minDistance) continue;
  208. Collider2D theCollider = hits2D[k].collider;
  209. int colliders2DCount = colliders2D.Length;
  210. for (int c = 0; c < colliders2DCount; c++) {
  211. if (colliders2D[c] == theCollider) {
  212. hit = true;
  213. if (selectOnClick && isMouseButonDown) {
  214. ToggleSelection();
  215. break;
  216. } else if (theCollider != currentCollider) {
  217. SwitchCollider(theCollider);
  218. k = hitCount;
  219. break;
  220. }
  221. }
  222. }
  223. }
  224. }
  225. if (hasColliders) {
  226. if (maxDistance > 0) {
  227. hitCount = Physics.RaycastNonAlloc(ray, hits, maxDistance, raycastLayerMask);
  228. } else {
  229. hitCount = Physics.RaycastNonAlloc(ray, hits, float.MaxValue, raycastLayerMask);
  230. }
  231. for (int k = 0; k < hitCount; k++) {
  232. if (Vector3.Distance(hits[k].point, ray.origin) < minDistance) continue;
  233. Collider theCollider = hits[k].collider;
  234. int collidersCount = colliders.Length;
  235. for (int c = 0; c < collidersCount; c++) {
  236. if (colliders[c] == theCollider) {
  237. hit = true;
  238. if (selectOnClick && isMouseButonDown) {
  239. ToggleSelection();
  240. break;
  241. } else if (theCollider != currentCollider) {
  242. SwitchCollider(theCollider);
  243. k = hitCount;
  244. break;
  245. }
  246. }
  247. }
  248. }
  249. }
  250. if (!hit && currentCollider != null) {
  251. SwitchCollider(null);
  252. }
  253. if (selectOnClick && isMouseButonDown && !keepSelection && !hit) {
  254. yield return w; // wait for other potential triggers to act
  255. if (HighlightManager.lastTriggerFrame < Time.frameCount) {
  256. HighlightManager.DeselectAll();
  257. }
  258. }
  259. yield return null;
  260. }
  261. }
  262. #if ENABLE_INPUT_SYSTEM
  263. EventSystem CreateEventSystem() {
  264. GameObject eo = new GameObject("Event System created by Highlight Plus", typeof(EventSystem), typeof(UnityEngine.InputSystem.UI.InputSystemUIInputModule));
  265. return eo.GetComponent<EventSystem>();
  266. }
  267. #endif
  268. void SwitchCollider(UnityEngine.Object newCollider) {
  269. if (!highlightOnHover && !hb.isSelected) return;
  270. currentCollider = newCollider;
  271. if (currentCollider != null) {
  272. Highlight(true);
  273. } else {
  274. Highlight(false);
  275. }
  276. }
  277. bool CanInteract() {
  278. if (!respectUI) return true;
  279. EventSystem es = EventSystem.current;
  280. if (es == null) return true;
  281. if (Application.isMobilePlatform && InputProxy.touchCount > 0 && es.IsPointerOverGameObject(InputProxy.GetFingerIdFromTouch(0))) {
  282. return false;
  283. } else if (es.IsPointerOverGameObject(-1))
  284. return false;
  285. return true;
  286. }
  287. void OnMouseDown() {
  288. if (isActiveAndEnabled && triggerMode == TriggerMode.ColliderEventsOnlyOnThisObject) {
  289. if (!CanInteract()) return;
  290. if (selectOnClick && InputProxy.GetMouseButtonDown(0)) {
  291. ToggleSelection();
  292. return;
  293. }
  294. Highlight(true);
  295. }
  296. }
  297. void OnMouseEnter() {
  298. if (isActiveAndEnabled && triggerMode == TriggerMode.ColliderEventsOnlyOnThisObject) {
  299. if (!CanInteract()) return;
  300. Highlight(true);
  301. }
  302. }
  303. void OnMouseExit() {
  304. if (isActiveAndEnabled && triggerMode == TriggerMode.ColliderEventsOnlyOnThisObject) {
  305. if (!CanInteract()) return;
  306. Highlight(false);
  307. }
  308. }
  309. void Highlight(bool state) {
  310. if (state) {
  311. if (!hb.highlighted) {
  312. if (OnObjectHighlightStart != null && hb.target != null) {
  313. if (!OnObjectHighlightStart(hb.target.gameObject)) return;
  314. }
  315. }
  316. } else {
  317. if (hb.highlighted) {
  318. if (OnObjectHighlightEnd != null && hb.target != null) {
  319. OnObjectHighlightEnd(hb.target.gameObject);
  320. }
  321. }
  322. }
  323. if (selectOnClick || hb.isSelected) {
  324. if (hb.isSelected) {
  325. if (state && selectedAndHighlightedProfile != null) {
  326. selectedAndHighlightedProfile.Load(hb);
  327. } else if (selectedProfile != null) {
  328. selectedProfile.Load(hb);
  329. } else {
  330. hb.previousSettings.Load(hb);
  331. }
  332. if (hb.highlighted) {
  333. hb.UpdateMaterialProperties();
  334. } else {
  335. hb.SetHighlighted(true);
  336. }
  337. return;
  338. } else if (!highlightOnHover) {
  339. hb.SetHighlighted(false);
  340. return;
  341. }
  342. }
  343. hb.SetHighlighted(state);
  344. }
  345. void ToggleSelection() {
  346. HighlightManager.lastTriggerFrame = Time.frameCount;
  347. bool newState = toggle ? !hb.isSelected : true;
  348. if (newState) {
  349. if (OnObjectSelected != null && !OnObjectSelected(gameObject)) return;
  350. } else {
  351. if (OnObjectUnSelected != null && !OnObjectUnSelected(gameObject)) return;
  352. }
  353. if (singleSelection && newState) {
  354. HighlightManager.DeselectAll();
  355. }
  356. hb.isSelected = newState;
  357. if (newState && !HighlightManager.selectedObjects.Contains(hb)) {
  358. HighlightManager.selectedObjects.Add(hb);
  359. } else if (!newState && HighlightManager.selectedObjects.Contains(hb)) {
  360. HighlightManager.selectedObjects.Remove(hb);
  361. }
  362. if (hb.isSelected) {
  363. if (hb.previousSettings == null) {
  364. hb.previousSettings = ScriptableObject.CreateInstance<HighlightProfile>();
  365. }
  366. hb.previousSettings.Save(hb);
  367. } else {
  368. hb.RestorePreviousHighlightEffectSettings();
  369. }
  370. Highlight(true);
  371. }
  372. public void OnTriggerEnter(Collider other) {
  373. if (triggerMode == TriggerMode.Volume) {
  374. if ((volumeLayerMask & (1 << other.gameObject.layer)) != 0) {
  375. Highlight(true);
  376. }
  377. }
  378. }
  379. public void OnTriggerExit(Collider other) {
  380. if (triggerMode == TriggerMode.Volume) {
  381. if ((volumeLayerMask & (1 << other.gameObject.layer)) != 0) {
  382. Highlight(false);
  383. }
  384. }
  385. }
  386. }
  387. }