HighlightManager.cs 17 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455
  1. using System.Collections.Generic;
  2. using UnityEngine;
  3. using UnityEngine.EventSystems;
  4. namespace HighlightPlus {
  5. public delegate bool OnObjectSelectionEvent(GameObject obj);
  6. [RequireComponent(typeof(HighlightEffect))]
  7. [DefaultExecutionOrder(100)]
  8. [HelpURL("https://kronnect.com/guides/highlight-plus-introduction/")]
  9. public class HighlightManager : MonoBehaviour {
  10. [Tooltip("Enables highlight when pointer is over this object.")]
  11. [SerializeField]
  12. bool _highlightOnHover = true;
  13. public bool highlightOnHover {
  14. get { return _highlightOnHover; }
  15. set {
  16. if (_highlightOnHover != value) {
  17. _highlightOnHover = value;
  18. if (!_highlightOnHover) {
  19. if (currentEffect != null) {
  20. Highlight(false);
  21. }
  22. }
  23. }
  24. }
  25. }
  26. public LayerMask layerMask = -1;
  27. public Camera raycastCamera;
  28. public RayCastSource raycastSource = RayCastSource.MousePosition;
  29. [Tooltip("Minimum distance for target.")]
  30. public float minDistance;
  31. [Tooltip("Maximum distance for target. 0 = infinity")]
  32. public float maxDistance;
  33. [Tooltip("Blocks interaction if pointer is over an UI element")]
  34. public bool respectUI = true;
  35. [Tooltip("If the object will be selected by clicking with mouse or tapping on it.")]
  36. public bool selectOnClick;
  37. [Tooltip("Optional profile for objects selected by clicking on them")]
  38. public HighlightProfile selectedProfile;
  39. [Tooltip("Profile to use whtn object is selected and highlighted.")]
  40. public HighlightProfile selectedAndHighlightedProfile;
  41. [Tooltip("Automatically deselects other previously selected objects")]
  42. public bool singleSelection;
  43. [Tooltip("Toggles selection on/off when clicking object")]
  44. public bool toggle;
  45. [Tooltip("Keeps current selection when clicking outside of any selectable object")]
  46. public bool keepSelection = true;
  47. HighlightEffect baseEffect, currentEffect;
  48. Transform currentObject;
  49. RaycastHit2D[] hitInfo2D;
  50. public readonly static List<HighlightEffect> selectedObjects = new List<HighlightEffect>();
  51. public event OnObjectSelectionEvent OnObjectSelected;
  52. public event OnObjectSelectionEvent OnObjectUnSelected;
  53. public event OnObjectHighlightEvent OnObjectHighlightStart;
  54. public event OnObjectHighlightEvent OnObjectHighlightEnd;
  55. public static int lastTriggerFrame;
  56. static HighlightManager _instance;
  57. public static HighlightManager instance {
  58. get {
  59. if (_instance == null) {
  60. _instance = FindObjectOfType<HighlightManager>();
  61. }
  62. return _instance;
  63. }
  64. }
  65. [RuntimeInitializeOnLoadMethod]
  66. static void DomainReloadDisabledSupport() {
  67. selectedObjects.Clear();
  68. lastTriggerFrame = 0;
  69. _instance = null;
  70. }
  71. void OnEnable() {
  72. currentObject = null;
  73. currentEffect = null;
  74. if (baseEffect == null) {
  75. baseEffect = GetComponent<HighlightEffect> ();
  76. if (baseEffect == null) {
  77. baseEffect = gameObject.AddComponent<HighlightEffect> ();
  78. }
  79. }
  80. raycastCamera = GetComponent<Camera> ();
  81. if (raycastCamera == null) {
  82. raycastCamera = GetCamera ();
  83. if (raycastCamera == null) {
  84. Debug.LogError ("Highlight Manager: no camera found!");
  85. }
  86. }
  87. hitInfo2D = new RaycastHit2D[1];
  88. InputProxy.Init();
  89. }
  90. void OnDisable () {
  91. SwitchesCollider (null);
  92. internal_DeselectAll();
  93. }
  94. void Update () {
  95. if (raycastCamera == null)
  96. return;
  97. #if ENABLE_INPUT_SYSTEM
  98. if (respectUI) {
  99. EventSystem es = EventSystem.current;
  100. if (es == null) {
  101. es = CreateEventSystem();
  102. }
  103. List<RaycastResult> raycastResults = new List<RaycastResult>();
  104. PointerEventData eventData = new PointerEventData(es);
  105. Vector3 cameraPos = raycastCamera.transform.position;
  106. if (raycastSource == RayCastSource.MousePosition) {
  107. eventData.position = InputProxy.mousePosition;
  108. } else {
  109. eventData.position = new Vector2(raycastCamera.pixelWidth * 0.5f, raycastCamera.pixelHeight * 0.5f);
  110. }
  111. es.RaycastAll(eventData, raycastResults);
  112. int hitCount = raycastResults.Count;
  113. // check UI blocker
  114. bool blocked = false;
  115. for (int k = 0; k < hitCount; k++) {
  116. RaycastResult rr = raycastResults[k];
  117. if (rr.module is UnityEngine.UI.GraphicRaycaster) {
  118. blocked = true;
  119. break;
  120. }
  121. }
  122. if (blocked) return;
  123. // look for our gameobject
  124. for (int k = 0; k < hitCount; k++) {
  125. RaycastResult rr = raycastResults[k];
  126. float distance = Vector3.Distance(rr.worldPosition, cameraPos);
  127. if (distance < minDistance || (maxDistance > 0 && distance > maxDistance)) continue;
  128. GameObject theGameObject = rr.gameObject;
  129. if ((layerMask & (1 << rr.gameObject.layer)) == 0) continue;
  130. // is this object state controller by Highlight Trigger?
  131. HighlightTrigger trigger = theGameObject.GetComponent<HighlightTrigger>();
  132. if (trigger != null) return;
  133. // Toggles selection
  134. Transform t = theGameObject.transform;
  135. if (InputProxy.GetMouseButtonDown(0)) {
  136. if (selectOnClick) {
  137. ToggleSelection(t, !toggle);
  138. }
  139. } else {
  140. // Check if the object has a Highlight Effect
  141. if (t != currentObject) {
  142. SwitchesCollider(t);
  143. }
  144. }
  145. return;
  146. }
  147. }
  148. // if not blocked by UI and no hit found, fallback to raycast (required if no PhysicsRaycaster is present on the camera)
  149. #endif
  150. Ray ray;
  151. if (raycastSource == RayCastSource.MousePosition) {
  152. #if !ENABLE_INPUT_SYSTEM
  153. if (!CanInteract()) {
  154. return;
  155. }
  156. #endif
  157. ray = raycastCamera.ScreenPointToRay(InputProxy.mousePosition);
  158. } else {
  159. ray = new Ray(raycastCamera.transform.position, raycastCamera.transform.forward);
  160. }
  161. RaycastHit hitInfo;
  162. if (Physics.Raycast(ray, out hitInfo, maxDistance > 0 ? maxDistance : raycastCamera.farClipPlane, layerMask) && Vector3.Distance(hitInfo.point, ray.origin) >= minDistance) {
  163. Transform t = hitInfo.collider.transform;
  164. // is this object state controller by Highlight Trigger?
  165. HighlightTrigger trigger = t.GetComponent<HighlightTrigger>();
  166. if (trigger != null) return;
  167. // Toggles selection
  168. if (InputProxy.GetMouseButtonDown(0)) {
  169. if (selectOnClick) {
  170. ToggleSelection(t, !toggle);
  171. }
  172. } else {
  173. // Check if the object has a Highlight Effect
  174. if (t != currentObject) {
  175. SwitchesCollider(t);
  176. }
  177. }
  178. return;
  179. } else // check sprites
  180. if (Physics2D.GetRayIntersectionNonAlloc(ray, hitInfo2D, maxDistance > 0 ? maxDistance : raycastCamera.farClipPlane, layerMask) > 0 && Vector3.Distance(hitInfo2D[0].point, ray.origin) >= minDistance) {
  181. Transform t = hitInfo2D[0].collider.transform;
  182. // is this object state controller by Highlight Trigger?
  183. HighlightTrigger trigger = t.GetComponent<HighlightTrigger>();
  184. if (trigger != null) return;
  185. // Toggles selection
  186. if (InputProxy.GetMouseButtonDown(0)) {
  187. if (selectOnClick) {
  188. ToggleSelection(t, !toggle);
  189. }
  190. } else {
  191. // Check if the object has a Highlight Effect
  192. if (t != currentObject) {
  193. SwitchesCollider(t);
  194. }
  195. }
  196. return;
  197. }
  198. // no hit
  199. if (selectOnClick && !keepSelection && InputProxy.GetMouseButtonDown(0) && lastTriggerFrame < Time.frameCount) {
  200. internal_DeselectAll();
  201. }
  202. SwitchesCollider (null);
  203. }
  204. #if ENABLE_INPUT_SYSTEM
  205. EventSystem CreateEventSystem() {
  206. GameObject eo = new GameObject("Event System created by Highlight Plus", typeof(EventSystem), typeof(UnityEngine.InputSystem.UI.InputSystemUIInputModule));
  207. return eo.GetComponent<EventSystem>();
  208. }
  209. #endif
  210. void SwitchesCollider (Transform newObject) {
  211. if (currentEffect != null) {
  212. if (highlightOnHover) {
  213. Highlight(false);
  214. }
  215. currentEffect = null;
  216. }
  217. currentObject = newObject;
  218. if (newObject == null) return;
  219. HighlightTrigger ht = newObject.GetComponent<HighlightTrigger>();
  220. if (ht != null && ht.enabled)
  221. return;
  222. HighlightEffect otherEffect = newObject.GetComponent<HighlightEffect> ();
  223. if (otherEffect == null) {
  224. // Check if there's a parent highlight effect that includes this object
  225. HighlightEffect parentEffect = newObject.GetComponentInParent<HighlightEffect>();
  226. if (parentEffect != null && parentEffect.Includes(newObject)) {
  227. currentEffect = parentEffect;
  228. if (highlightOnHover) {
  229. Highlight(true);
  230. }
  231. return;
  232. }
  233. }
  234. currentEffect = otherEffect != null ? otherEffect : baseEffect;
  235. baseEffect.enabled = currentEffect == baseEffect;
  236. currentEffect.SetTarget(currentObject);
  237. if (highlightOnHover) {
  238. Highlight(true);
  239. }
  240. }
  241. bool CanInteract() {
  242. if (!respectUI) return true;
  243. EventSystem es = EventSystem.current;
  244. if (es == null) return true;
  245. if (Application.isMobilePlatform && InputProxy.touchCount > 0 && es.IsPointerOverGameObject(InputProxy.GetFingerIdFromTouch(0))) {
  246. return false;
  247. } else if (es.IsPointerOverGameObject(-1))
  248. return false;
  249. return true;
  250. }
  251. void ToggleSelection(Transform t, bool forceSelection) {
  252. // We need a highlight effect on each selected object
  253. HighlightEffect hb = t.GetComponent<HighlightEffect>();
  254. if (hb == null) {
  255. HighlightEffect parentEffect = t.GetComponentInParent<HighlightEffect>();
  256. if (parentEffect != null && parentEffect.Includes(t)) {
  257. hb = parentEffect;
  258. if (hb.previousSettings == null) {
  259. hb.previousSettings = ScriptableObject.CreateInstance<HighlightProfile>();
  260. }
  261. hb.previousSettings.Save(hb);
  262. } else {
  263. hb = t.gameObject.AddComponent<HighlightEffect>();
  264. hb.previousSettings = ScriptableObject.CreateInstance<HighlightProfile>();
  265. // copy default highlight effect settings from this manager into this highlight plus component
  266. hb.previousSettings.Save(baseEffect);
  267. hb.previousSettings.Load(hb);
  268. }
  269. }
  270. bool currentState = hb.isSelected;
  271. bool newState = forceSelection ? true : !currentState;
  272. if (newState == currentState) return;
  273. if (newState) {
  274. if (OnObjectSelected != null && !OnObjectSelected(t.gameObject)) return;
  275. } else {
  276. if (OnObjectUnSelected != null && !OnObjectUnSelected(t.gameObject)) return;
  277. }
  278. if (singleSelection) {
  279. internal_DeselectAll();
  280. }
  281. currentEffect = hb;
  282. currentEffect.isSelected = newState;
  283. baseEffect.enabled = false;
  284. if (currentEffect.isSelected) {
  285. if (currentEffect.previousSettings == null) {
  286. currentEffect.previousSettings = ScriptableObject.CreateInstance<HighlightProfile>();
  287. }
  288. hb.previousSettings.Save(hb);
  289. if (!selectedObjects.Contains(currentEffect)) {
  290. selectedObjects.Add(currentEffect);
  291. }
  292. } else {
  293. if (currentEffect.previousSettings != null) {
  294. currentEffect.previousSettings.Load(hb);
  295. }
  296. if (selectedObjects.Contains(currentEffect)) {
  297. selectedObjects.Remove(currentEffect);
  298. }
  299. }
  300. Highlight(true);
  301. }
  302. void Highlight(bool state) {
  303. if (state) {
  304. if (!currentEffect.highlighted) {
  305. if (OnObjectHighlightStart != null && currentEffect.target != null) {
  306. if (!OnObjectHighlightStart(currentEffect.target.gameObject)) return;
  307. }
  308. }
  309. } else {
  310. if (currentEffect.highlighted) {
  311. if (OnObjectHighlightEnd != null && currentEffect.target != null) {
  312. OnObjectHighlightEnd(currentEffect.target.gameObject);
  313. }
  314. }
  315. }
  316. if (selectOnClick || currentEffect.isSelected) {
  317. if (currentEffect.isSelected) {
  318. if (state && selectedAndHighlightedProfile != null) {
  319. selectedAndHighlightedProfile.Load(currentEffect);
  320. } else if (selectedProfile != null) {
  321. selectedProfile.Load(currentEffect);
  322. } else {
  323. currentEffect.previousSettings.Load(currentEffect);
  324. }
  325. if (currentEffect.highlighted) {
  326. currentEffect.UpdateMaterialProperties();
  327. } else {
  328. currentEffect.SetHighlighted(true);
  329. }
  330. return;
  331. } else if (!highlightOnHover) {
  332. currentEffect.SetHighlighted(false);
  333. return;
  334. }
  335. }
  336. currentEffect.SetHighlighted(state);
  337. }
  338. public static Camera GetCamera() {
  339. Camera raycastCamera = Camera.main;
  340. if (raycastCamera == null) {
  341. raycastCamera = FindObjectOfType<Camera>();
  342. }
  343. return raycastCamera;
  344. }
  345. void internal_DeselectAll() {
  346. foreach (HighlightEffect hb in selectedObjects) {
  347. if (hb != null && hb.gameObject != null) {
  348. if (OnObjectUnSelected != null) {
  349. if (!OnObjectUnSelected(hb.gameObject)) continue;
  350. }
  351. hb.RestorePreviousHighlightEffectSettings();
  352. hb.isSelected = false;
  353. hb.SetHighlighted(false);
  354. }
  355. }
  356. selectedObjects.Clear();
  357. }
  358. public static void DeselectAll() {
  359. foreach (HighlightEffect hb in selectedObjects) {
  360. if (hb != null && hb.gameObject != null) {
  361. hb.isSelected = false;
  362. if (hb.highlighted && _instance != null) {
  363. _instance.Highlight(false);
  364. } else {
  365. hb.SetHighlighted(false);
  366. }
  367. }
  368. }
  369. selectedObjects.Clear();
  370. }
  371. /// <summary>
  372. /// Manually causes highlight manager to select an object
  373. /// </summary>
  374. public void SelectObject(Transform t) {
  375. ToggleSelection(t, true);
  376. }
  377. /// <summary>
  378. /// Manually causes highlight manager to toggle selection on an object
  379. /// </summary>
  380. public void ToggleObject(Transform t) {
  381. ToggleSelection(t, false);
  382. }
  383. /// <summary>
  384. /// Manually causes highlight manager to unselect an object
  385. /// </summary>
  386. public void UnselectObject(Transform t) {
  387. if (t == null) return;
  388. HighlightEffect hb = t.GetComponent<HighlightEffect>();
  389. if (hb == null) return;
  390. if (selectedObjects.Contains(hb)) {
  391. if (OnObjectUnSelected != null) {
  392. if (!OnObjectUnSelected(hb.gameObject)) return;
  393. }
  394. hb.isSelected = false;
  395. hb.SetHighlighted(false);
  396. selectedObjects.Remove(hb);
  397. }
  398. }
  399. }
  400. }