Assignment for RMIT Mixed Reality in 2020
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

374 lines
16 KiB

  1. // Independent Radial Menu|Prefabs|0120
  2. namespace VRTK
  3. {
  4. using UnityEngine;
  5. using System.Collections.Generic;
  6. using System.Collections;
  7. /// <summary>
  8. /// Allows the RadialMenu to be anchored to any object, not just a controller.
  9. /// </summary>
  10. /// <remarks>
  11. /// **Prefab Usage:**
  12. /// * Place the `VRTK/Prefabs/RadialMenu/RadialMenu` prefab as a child of the GameObject to associate the Radial Menu with.
  13. /// * Position and scale the menu by adjusting the transform of the `RadialMenu` empty.
  14. /// * Replace `VRTK_RadialMenuController` with `VRTK_IndependentRadialMenuController` that is located on the `RadialMenu/RadialMenuUI/Panel` GameObject.
  15. /// * Ensure the parent object has the `VRTK_InteractableObject` script.
  16. /// * Verify that `Is Usable` and `Hold Button to Use` are both checked on the `VRTK_InteractableObject`.
  17. /// * Attach `VRTK_InteractTouch` and `VRTK_InteractUse` scripts to the objects that will activate the Radial Menu (e.g. the Controllers).
  18. /// </remarks>
  19. /// <example>
  20. /// `VRTK/Examples/030_Controls_RadialTouchpadMenu` displays a radial menu for each controller. The left controller uses the `Hide On Release` variable, so it will only be visible if the left touchpad is being touched. It also uses the `Execute On Unclick` variable to delay execution until the touchpad button is unclicked. The example scene also contains a demonstration of anchoring the RadialMenu to an interactable cube instead of a controller.
  21. /// </example>
  22. public class VRTK_IndependentRadialMenuController : VRTK_RadialMenuController
  23. {
  24. [Tooltip("If the RadialMenu is the child of an object with VRTK_InteractableObject attached, this will be automatically obtained. It can also be manually set.")]
  25. public VRTK_InteractableObject eventsManager;
  26. [Tooltip("Whether or not the script should dynamically add a SphereCollider to surround the menu.")]
  27. public bool addMenuCollider = true;
  28. [Tooltip("This times the size of the RadialMenu is the size of the collider.")]
  29. [Range(0, 10)]
  30. public float colliderRadiusMultiplier = 1.2f;
  31. [Tooltip("If true, after a button is clicked, the RadialMenu will hide.")]
  32. public bool hideAfterExecution = true;
  33. [Tooltip("How far away from the object the menu should be placed, relative to the size of the RadialMenu.")]
  34. [Range(-10, 10)]
  35. public float offsetMultiplier = 1.1f;
  36. [Tooltip("The object the RadialMenu should face towards. If left empty, it will automatically try to find the Headset Camera.")]
  37. public GameObject rotateTowards;
  38. protected List<GameObject> interactingObjects = new List<GameObject>(); // Objects (controllers) that are either colliding with the menu or clicking the menu
  39. protected HashSet<GameObject> collidingObjects = new HashSet<GameObject>(); // Just objects that are currently colliding with the menu or its parent
  40. protected SphereCollider menuCollider;
  41. protected Coroutine delayedSetColliderEnabledRoutine;
  42. protected Vector3 desiredColliderCenter;
  43. protected Quaternion initialRotation;
  44. protected bool isClicked = false;
  45. protected bool waitingToDisableCollider = false;
  46. protected int counter = 2;
  47. /// <summary>
  48. /// The UpdateEventsManager method is used to update the events within the menu controller.
  49. /// </summary>
  50. public virtual void UpdateEventsManager()
  51. {
  52. VRTK_InteractableObject newEventsManager = transform.GetComponentInParent<VRTK_InteractableObject>();
  53. if (newEventsManager == null)
  54. {
  55. VRTK_Logger.Error(VRTK_Logger.GetCommonMessage(VRTK_Logger.CommonMessageKeys.REQUIRED_COMPONENT_MISSING_NOT_INJECTED, "VRTK_IndependentRadialMenuController", "VRTK_InteractableObject", "eventsManager", "the parent"));
  56. return;
  57. }
  58. else if (newEventsManager != eventsManager) // Changed managers
  59. {
  60. if (eventsManager != null)
  61. { // Unsubscribe from the old events
  62. OnDisable();
  63. }
  64. eventsManager = newEventsManager;
  65. // Subscribe to new events
  66. OnEnable();
  67. Destroy(menuCollider);
  68. // Reset to initial state
  69. Initialize();
  70. }
  71. }
  72. protected override void Initialize()
  73. {
  74. if (eventsManager == null)
  75. {
  76. initialRotation = transform.localRotation;
  77. UpdateEventsManager();
  78. return; // If all goes well in updateEventsManager, it will then call Initialize again, skipping this if statement
  79. }
  80. // Reset variables
  81. interactingObjects.Clear();
  82. collidingObjects.Clear();
  83. if (delayedSetColliderEnabledRoutine != null)
  84. {
  85. StopCoroutine(delayedSetColliderEnabledRoutine);
  86. }
  87. isClicked = false;
  88. waitingToDisableCollider = false;
  89. counter = 2;
  90. if (transform.childCount == 0) // This means things haven't been properly initialized yet, will cause problems.
  91. {
  92. return;
  93. }
  94. float radius = (transform.GetChild(0).GetComponent<RectTransform>().rect.width / 2) * offsetMultiplier;
  95. transform.localPosition = new Vector3(0, 0, radius);
  96. if (addMenuCollider)
  97. {
  98. gameObject.SetActive(false); // Just be sure it doesn't briefly flash
  99. transform.localScale = Vector3.one; // If this were left at zero it would ruin the transformations below
  100. Quaternion startingRot = transform.rotation;
  101. transform.rotation = Quaternion.Euler(new Vector3(0, 0, 0)); // Rotation can mess up the calculations below
  102. SphereCollider collider = eventsManager.gameObject.AddComponent<SphereCollider>();
  103. // All of the transformVector's are to account for the scaling of the radial menu's 'panel' and the scaling of the eventsManager parent object
  104. collider.radius = (transform.GetChild(0).GetComponent<RectTransform>().rect.width / 2) * colliderRadiusMultiplier * eventsManager.transform.InverseTransformVector(transform.GetChild(0).TransformVector(Vector3.one)).x;
  105. collider.center = eventsManager.transform.InverseTransformVector(transform.position - eventsManager.transform.position);
  106. collider.isTrigger = true;
  107. collider.enabled = false; // Want this to only activate when the menu is showing
  108. menuCollider = collider;
  109. desiredColliderCenter = collider.center;
  110. transform.rotation = startingRot;
  111. }
  112. if (!menu.isShown)
  113. {
  114. transform.localScale = Vector3.zero;
  115. }
  116. gameObject.SetActive(true);
  117. }
  118. protected override void Awake()
  119. {
  120. menu = GetComponent<VRTK_RadialMenu>();
  121. VRTK_SDKManager.AttemptAddBehaviourToToggleOnLoadedSetupChange(this);
  122. }
  123. protected virtual void Start()
  124. {
  125. Initialize();
  126. }
  127. protected override void OnEnable()
  128. {
  129. if (eventsManager != null)
  130. {
  131. eventsManager.InteractableObjectUsed += ObjectClicked;
  132. eventsManager.InteractableObjectUnused += ObjectUnClicked;
  133. eventsManager.InteractableObjectTouched += ObjectTouched;
  134. eventsManager.InteractableObjectUntouched += ObjectUntouched;
  135. menu.FireHapticPulse += AttemptHapticPulse;
  136. }
  137. else
  138. {
  139. Initialize();
  140. }
  141. }
  142. protected override void OnDisable()
  143. {
  144. if (eventsManager != null)
  145. {
  146. eventsManager.InteractableObjectUsed -= ObjectClicked;
  147. eventsManager.InteractableObjectUnused -= ObjectUnClicked;
  148. eventsManager.InteractableObjectTouched -= ObjectTouched;
  149. eventsManager.InteractableObjectUntouched -= ObjectUntouched;
  150. menu.FireHapticPulse -= AttemptHapticPulse;
  151. }
  152. }
  153. protected virtual void OnDestroy()
  154. {
  155. VRTK_SDKManager.AttemptRemoveBehaviourToToggleOnLoadedSetupChange(this);
  156. }
  157. protected virtual void Update()
  158. {
  159. if (rotateTowards == null) // Backup
  160. {
  161. Transform headset = VRTK_DeviceFinder.HeadsetTransform();
  162. if (headset)
  163. {
  164. rotateTowards = headset.gameObject;
  165. }
  166. else
  167. {
  168. VRTK_Logger.Warn(VRTK_Logger.GetCommonMessage(VRTK_Logger.CommonMessageKeys.COULD_NOT_FIND_OBJECT_FOR_ACTION, "IndependentRadialMenu", "an object", "rotate towards"));
  169. }
  170. }
  171. if (menu.isShown)
  172. {
  173. if (interactingObjects.Count > 0) // There's not really an event for the controller moving, so just update the position every frame
  174. {
  175. DoChangeAngle(CalculateAngle(interactingObjects[0]), this);
  176. }
  177. if (rotateTowards != null)
  178. {
  179. transform.rotation = Quaternion.LookRotation((rotateTowards.transform.position - transform.position) * -1, Vector3.up) * initialRotation; // Face the target, but maintain initial rotation
  180. }
  181. }
  182. }
  183. protected virtual void FixedUpdate()
  184. {
  185. if (waitingToDisableCollider)
  186. {
  187. if (counter == 0)
  188. {
  189. menuCollider.enabled = false;
  190. waitingToDisableCollider = false;
  191. counter = 2;
  192. }
  193. else
  194. {
  195. counter--;
  196. }
  197. }
  198. }
  199. protected override void AttemptHapticPulse(float strength)
  200. {
  201. if (interactingObjects.Count > 0)
  202. {
  203. VRTK_ControllerHaptics.TriggerHapticPulse(VRTK_ControllerReference.GetControllerReference(interactingObjects[0]), strength);
  204. }
  205. }
  206. protected virtual void ObjectClicked(object sender, InteractableObjectEventArgs e)
  207. {
  208. DoClickButton(sender);
  209. isClicked = true;
  210. if (hideAfterExecution && !menu.executeOnUnclick)
  211. {
  212. ImmediatelyHideMenu(e);
  213. }
  214. }
  215. protected virtual void ObjectUnClicked(object sender, InteractableObjectEventArgs e)
  216. {
  217. DoUnClickButton(sender);
  218. isClicked = false;
  219. if ((hideAfterExecution || (collidingObjects.Count == 0 && menu.hideOnRelease)) && menu.executeOnUnclick)
  220. {
  221. ImmediatelyHideMenu(e);
  222. }
  223. }
  224. protected virtual void ObjectTouched(object sender, InteractableObjectEventArgs e)
  225. {
  226. DoShowMenu(CalculateAngle(e.interactingObject), sender);
  227. collidingObjects.Add(e.interactingObject);
  228. VRTK_SharedMethods.AddListValue(interactingObjects, e.interactingObject, true);
  229. if (addMenuCollider && menuCollider != null)
  230. {
  231. SetColliderState(true, e);
  232. if (delayedSetColliderEnabledRoutine != null)
  233. {
  234. StopCoroutine(delayedSetColliderEnabledRoutine);
  235. }
  236. }
  237. }
  238. protected virtual void ObjectUntouched(object sender, InteractableObjectEventArgs e)
  239. {
  240. collidingObjects.Remove(e.interactingObject);
  241. if (((!menu.executeOnUnclick || !isClicked) && menu.hideOnRelease) || (Object)sender == this)
  242. {
  243. DoHideMenu(hideAfterExecution, sender);
  244. interactingObjects.Remove(e.interactingObject);
  245. if (addMenuCollider && menuCollider != null)
  246. {
  247. // In case there's any gap between the normal collider and the menuCollider, delay a bit. Cancelled if collider is re-entered
  248. delayedSetColliderEnabledRoutine = StartCoroutine(DelayedSetColliderEnabled(false, 0.25f, e));
  249. }
  250. }
  251. }
  252. protected virtual TouchAngleDeflection CalculateAngle(GameObject interactingObject)
  253. {
  254. Vector3 controllerPosition = interactingObject.transform.position;
  255. Vector3 toController = controllerPosition - transform.position;
  256. Vector3 projection = transform.position + Vector3.ProjectOnPlane(toController, transform.forward);
  257. float angle = 0;
  258. angle = AngleSigned(transform.right * -1, projection - transform.position, transform.forward);
  259. // Ensure angle is positive
  260. if (angle < 0)
  261. {
  262. angle += 360.0f;
  263. }
  264. return new TouchAngleDeflection(angle, 1);
  265. }
  266. protected virtual float AngleSigned(Vector3 v1, Vector3 v2, Vector3 n)
  267. {
  268. return Mathf.Atan2(Vector3.Dot(n, Vector3.Cross(v1, v2)), Vector3.Dot(v1, v2)) * Mathf.Rad2Deg;
  269. }
  270. protected virtual void ImmediatelyHideMenu(InteractableObjectEventArgs e)
  271. {
  272. ObjectUntouched(this, e);
  273. if (delayedSetColliderEnabledRoutine != null)
  274. {
  275. StopCoroutine(delayedSetColliderEnabledRoutine);
  276. }
  277. SetColliderState(false, e); // Don't want to wait for this
  278. }
  279. protected virtual void SetColliderState(bool state, InteractableObjectEventArgs e)
  280. {
  281. if (addMenuCollider && menuCollider != null)
  282. {
  283. if (state)
  284. {
  285. menuCollider.enabled = true;
  286. menuCollider.center = desiredColliderCenter;
  287. }
  288. else
  289. {
  290. bool should = true;
  291. Collider[] colliders = eventsManager.GetComponents<Collider>();
  292. Collider[] controllerColliders = e.interactingObject.GetComponent<VRTK_InteractTouch>().ControllerColliders();
  293. for (int i = 0; i < colliders.Length; i++)
  294. {
  295. Collider collider = colliders[i];
  296. if (collider != menuCollider)
  297. {
  298. for (int j = 0; j < controllerColliders.Length; j++)
  299. {
  300. Collider controllerCollider = controllerColliders[j];
  301. if (controllerCollider.bounds.Intersects(collider.bounds))
  302. {
  303. should = false;
  304. }
  305. }
  306. }
  307. }
  308. if (should)
  309. {
  310. menuCollider.center = new Vector3(100000000.0f, 100000000.0f, 100000000.0f); // This needs to be done to get OnTriggerExit() to fire, unfortunately
  311. waitingToDisableCollider = true; // Need to give other things time to realize that they're not colliding with this anymore, so do it a couple FixedUpdates
  312. }
  313. else
  314. {
  315. menuCollider.enabled = false;
  316. }
  317. }
  318. }
  319. }
  320. protected virtual IEnumerator DelayedSetColliderEnabled(bool enabled, float delay, InteractableObjectEventArgs e)
  321. {
  322. yield return new WaitForSeconds(delay);
  323. SetColliderState(enabled, e);
  324. }
  325. }
  326. }