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.

448 lines
19 KiB

  1. // Radial Menu|Prefabs|0110
  2. namespace VRTK
  3. {
  4. using UnityEngine;
  5. using System.Collections;
  6. using UnityEngine.Events;
  7. using System.Collections.Generic;
  8. using UnityEngine.UI;
  9. using UnityEngine.EventSystems;
  10. public struct TouchAngleDeflection
  11. {
  12. public float angle;
  13. public float deflection;
  14. /// <summary>
  15. /// Constructs an object to hold the angle and deflection of the user's touch on the touchpad
  16. /// </summary>
  17. /// <param name="angle">The angle of the touch on the radial menu.</param>
  18. /// <param name="deflection">Deflection of the touch, where 0 is the centre and 1 is the edge.</param>
  19. public TouchAngleDeflection(float angle, float deflection)
  20. {
  21. this.angle = angle;
  22. this.deflection = deflection;
  23. }
  24. }
  25. public delegate void HapticPulseEventHandler(float strength);
  26. /// <summary>
  27. /// Provides a UI element into the world space that can be dropped into a Controller GameObject and used to create and use Radial Menus from the touchpad.
  28. /// </summary>
  29. /// <remarks>
  30. /// **Prefab Usage:**
  31. /// * Place the `VRTK/Prefabs/RadialMenu/RadialMenu` prefab as a child of a Controller script alias GameObject.
  32. /// </remarks>
  33. /// <example>
  34. /// `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.
  35. /// </example>
  36. [ExecuteInEditMode]
  37. public class VRTK_RadialMenu : MonoBehaviour
  38. {
  39. [System.Serializable]
  40. public class RadialMenuButton
  41. {
  42. public Sprite ButtonIcon;
  43. public UnityEvent OnClick = new UnityEvent();
  44. public UnityEvent OnHold = new UnityEvent();
  45. public UnityEvent OnHoverEnter = new UnityEvent();
  46. public UnityEvent OnHoverExit = new UnityEvent();
  47. }
  48. public enum ButtonEvent
  49. {
  50. hoverOn,
  51. hoverOff,
  52. click,
  53. unclick
  54. }
  55. [Tooltip("An array of Buttons that define the interactive buttons required to be displayed as part of the radial menu.")]
  56. public List<RadialMenuButton> buttons = new List<RadialMenuButton>();
  57. [Tooltip("The base for each button in the menu, by default set to a dynamic circle arc that will fill up a portion of the menu.")]
  58. public GameObject buttonPrefab;
  59. [Tooltip("If checked, then the buttons will be auto generated on awake.")]
  60. public bool generateOnAwake = true;
  61. [Tooltip("Percentage of the menu the buttons should fill, 1.0 is a pie slice, 0.1 is a thin ring.")]
  62. [Range(0f, 1f)]
  63. public float buttonThickness = 0.5f;
  64. [Tooltip("The background colour of the buttons, default is white.")]
  65. public Color buttonColor = Color.white;
  66. [Tooltip("The distance the buttons should move away from the centre. This creates space between the individual buttons.")]
  67. public float offsetDistance = 1;
  68. [Tooltip("The additional rotation of the Radial Menu.")]
  69. [Range(0, 359)]
  70. public float offsetRotation;
  71. [Tooltip("Whether button icons should rotate according to their arc or be vertical compared to the controller.")]
  72. public bool rotateIcons;
  73. [Tooltip("The margin in pixels that the icon should keep within the button.")]
  74. public float iconMargin;
  75. [Tooltip("Whether the buttons are shown")]
  76. public bool isShown;
  77. [Tooltip("Whether the buttons should be visible when not in use.")]
  78. public bool hideOnRelease;
  79. [Tooltip("Whether the button action should happen when the button is released, as opposed to happening immediately when the button is pressed.")]
  80. public bool executeOnUnclick;
  81. [Tooltip("The base strength of the haptic pulses when the selected button is changed, or a button is pressed. Set to zero to disable.")]
  82. [Range(0, 1)]
  83. public float baseHapticStrength;
  84. [Tooltip("The dead zone in the middle of the dial where the menu does not consider a button is selected. Set to zero to disable.")]
  85. [Range(0, 1)]
  86. public float deadZone = 0;
  87. public event HapticPulseEventHandler FireHapticPulse;
  88. //Has to be public to keep state from editor -> play mode?
  89. [Tooltip("The actual GameObjects that make up the radial menu.")]
  90. public List<GameObject> menuButtons = new List<GameObject>();
  91. protected int currentHover = -1;
  92. protected int currentPress = -1;
  93. protected Coroutine tweenMenuScaleRoutine;
  94. /// <summary>
  95. /// The HoverButton method is used to set the button hover at a given angle.
  96. /// </summary>
  97. /// <param name="angle">The angle on the radial menu.</param>
  98. [System.Obsolete("`VRTK_RadialMenu.HoverButton(float)` has been replaced with `VRTK_RadialMenu.HoverButton(TouchAngleDeflection)`. This method will be removed in a future version of VRTK.")]
  99. public virtual void HoverButton(float angle)
  100. {
  101. HoverButton(new TouchAngleDeflection(angle, 1));
  102. }
  103. /// <summary>
  104. /// The HoverButton method is used to set the button hover at a given angle and deflection.
  105. /// </summary>
  106. /// <param name="givenTouchAngleDeflection">The angle and deflection on the radial menu.</param>
  107. public virtual void HoverButton(TouchAngleDeflection givenTouchAngleDeflection)
  108. {
  109. InteractButton(givenTouchAngleDeflection, ButtonEvent.hoverOn);
  110. }
  111. /// <summary>
  112. /// The ClickButton method is used to set the button click at a given angle.
  113. /// </summary>
  114. /// <param name="angle">The angle on the radial menu.</param>
  115. [System.Obsolete("`VRTK_RadialMenu.ClickButton(float)` has been replaced with `VRTK_RadialMenu.ClickButton(TouchAngleDeflection)`. This method will be removed in a future version of VRTK.")]
  116. public virtual void ClickButton(float angle)
  117. {
  118. ClickButton(new TouchAngleDeflection(angle, 1));
  119. }
  120. /// <summary>
  121. /// The ClickButton method is used to set the button click at a given angle and deflection.
  122. /// </summary>
  123. /// <param name="givenTouchAngleDeflection">The angle and deflection on the radial menu.</param>
  124. public virtual void ClickButton(TouchAngleDeflection givenTouchAngleDeflection)
  125. {
  126. InteractButton(givenTouchAngleDeflection, ButtonEvent.click);
  127. }
  128. /// <summary>
  129. /// The UnClickButton method is used to set the button unclick at a given angle.
  130. /// </summary>
  131. /// <param name="angle">The angle on the radial menu.</param>
  132. [System.Obsolete("`VRTK_RadialMenu.UnClickButton(float)` has been replaced with `VRTK_RadialMenu.UnClickButton(TouchAngleDeflection)`. This method will be removed in a future version of VRTK.")]
  133. public virtual void UnClickButton(float angle)
  134. {
  135. UnClickButton(new TouchAngleDeflection(angle, 1));
  136. }
  137. /// <summary>
  138. /// The UnClickButton method is used to set the button unclick at a given angle and deflection.
  139. /// </summary>
  140. /// <param name="givenTouchAngleDeflection">The angle and deflection on the radial menu.</param>
  141. public virtual void UnClickButton(TouchAngleDeflection givenTouchAngleDeflection)
  142. {
  143. InteractButton(givenTouchAngleDeflection, ButtonEvent.unclick);
  144. }
  145. /// <summary>
  146. /// The ToggleMenu method is used to show or hide the radial menu.
  147. /// </summary>
  148. public virtual void ToggleMenu()
  149. {
  150. if (isShown)
  151. {
  152. HideMenu(true);
  153. }
  154. else
  155. {
  156. ShowMenu();
  157. }
  158. }
  159. /// <summary>
  160. /// The StopTouching method is used to stop touching the menu.
  161. /// </summary>
  162. public virtual void StopTouching()
  163. {
  164. if (currentHover != -1)
  165. {
  166. PointerEventData pointer = new PointerEventData(EventSystem.current);
  167. ExecuteEvents.Execute(menuButtons[currentHover], pointer, ExecuteEvents.pointerExitHandler);
  168. buttons[currentHover].OnHoverExit.Invoke();
  169. currentHover = -1;
  170. }
  171. }
  172. /// <summary>
  173. /// The ShowMenu method is used to show the menu.
  174. /// </summary>
  175. public virtual void ShowMenu()
  176. {
  177. if (!isShown)
  178. {
  179. isShown = true;
  180. InitTweenMenuScale(isShown);
  181. }
  182. }
  183. /// <summary>
  184. /// The GetButton method is used to get a button from the menu.
  185. /// </summary>
  186. /// <param name="id">The id of the button to retrieve.</param>
  187. /// <returns>The found radial menu button.</returns>
  188. public virtual RadialMenuButton GetButton(int id)
  189. {
  190. if (id < buttons.Count)
  191. {
  192. return buttons[id];
  193. }
  194. return null;
  195. }
  196. /// <summary>
  197. /// The HideMenu method is used to hide the menu.
  198. /// </summary>
  199. /// <param name="force">If true then the menu is always hidden.</param>
  200. public virtual void HideMenu(bool force)
  201. {
  202. if (isShown && (hideOnRelease || force))
  203. {
  204. isShown = false;
  205. InitTweenMenuScale(isShown);
  206. }
  207. }
  208. /// <summary>
  209. /// The RegenerateButtons method creates all the button arcs and populates them with desired icons.
  210. /// </summary>
  211. public void RegenerateButtons()
  212. {
  213. RemoveAllButtons();
  214. for (int i = 0; i < buttons.Count; i++)
  215. {
  216. // Initial placement/instantiation
  217. GameObject newButton = Instantiate(buttonPrefab);
  218. newButton.transform.SetParent(transform);
  219. newButton.transform.localScale = Vector3.one;
  220. newButton.GetComponent<RectTransform>().offsetMax = Vector2.zero;
  221. newButton.GetComponent<RectTransform>().offsetMin = Vector2.zero;
  222. //Setup button arc
  223. UICircle circle = newButton.GetComponent<UICircle>();
  224. if (buttonThickness == 1f)
  225. {
  226. circle.fill = true;
  227. }
  228. else
  229. {
  230. circle.thickness = (int)(buttonThickness * (GetComponent<RectTransform>().rect.width / 2f));
  231. }
  232. int fillPerc = (int)(100f / buttons.Count);
  233. circle.fillPercent = fillPerc;
  234. circle.color = buttonColor;
  235. //Final placement/rotation
  236. float angle = ((360f / buttons.Count) * i) + offsetRotation;
  237. newButton.transform.localEulerAngles = new Vector3(0, 0, angle);
  238. newButton.layer = 4; //UI Layer
  239. newButton.transform.localPosition = Vector3.zero;
  240. if (circle.fillPercent < 55)
  241. {
  242. float angleRad = (angle * Mathf.PI) / 180f;
  243. Vector2 angleVector = new Vector2(-Mathf.Cos(angleRad), -Mathf.Sin(angleRad));
  244. newButton.transform.localPosition += (Vector3)angleVector * offsetDistance;
  245. }
  246. //Place and populate Button Icon
  247. GameObject buttonIcon = newButton.GetComponentInChildren<RadialButtonIcon>().gameObject;
  248. if (buttons[i].ButtonIcon == null)
  249. {
  250. buttonIcon.SetActive(false);
  251. }
  252. else
  253. {
  254. buttonIcon.GetComponent<Image>().sprite = buttons[i].ButtonIcon;
  255. buttonIcon.transform.localPosition = new Vector2(-1 * ((newButton.GetComponent<RectTransform>().rect.width / 2f) - (circle.thickness / 2f)), 0);
  256. //Min icon size from thickness and arc
  257. float scale1 = Mathf.Abs(circle.thickness);
  258. float absButtonIconXPos = Mathf.Abs(buttonIcon.transform.localPosition.x);
  259. float bAngle = (359f * circle.fillPercent * 0.01f * Mathf.PI) / 180f;
  260. float scale2 = (absButtonIconXPos * 2f * Mathf.Sin(bAngle / 2f));
  261. if (circle.fillPercent > 24) //Scale calc doesn't work for > 90 degrees
  262. {
  263. scale2 = float.MaxValue;
  264. }
  265. float iconScale = Mathf.Min(scale1, scale2) - iconMargin;
  266. buttonIcon.GetComponent<RectTransform>().sizeDelta = new Vector2(iconScale, iconScale);
  267. //Rotate icons all vertically if desired
  268. if (!rotateIcons)
  269. {
  270. buttonIcon.transform.eulerAngles = GetComponentInParent<Canvas>().transform.eulerAngles;
  271. }
  272. }
  273. VRTK_SharedMethods.AddListValue(menuButtons, newButton, true);
  274. }
  275. }
  276. /// <summary>
  277. /// The AddButton method is used to add a new button to the menu.
  278. /// </summary>
  279. /// <param name="newButton">The button to add.</param>
  280. public void AddButton(RadialMenuButton newButton)
  281. {
  282. VRTK_SharedMethods.AddListValue(buttons, newButton, true);
  283. RegenerateButtons();
  284. }
  285. protected virtual void Awake()
  286. {
  287. if (Application.isPlaying)
  288. {
  289. if (!isShown)
  290. {
  291. transform.localScale = Vector3.zero;
  292. }
  293. if (generateOnAwake)
  294. {
  295. RegenerateButtons();
  296. }
  297. }
  298. }
  299. protected virtual void Update()
  300. {
  301. //Keep track of pressed button and constantly invoke Hold event
  302. if (currentPress != -1)
  303. {
  304. buttons[currentPress].OnHold.Invoke();
  305. }
  306. }
  307. //Turns and Angle and Event type into a button action
  308. protected virtual void InteractButton(TouchAngleDeflection givenTouchAngleDeflection, ButtonEvent evt) //Can't pass ExecuteEvents as parameter? Unity gives error
  309. {
  310. //Get button ID from angle
  311. float buttonAngle = 360f / buttons.Count; //Each button is an arc with this angle
  312. givenTouchAngleDeflection.angle = VRTK_SharedMethods.Mod((givenTouchAngleDeflection.angle + -offsetRotation), 360f); //Offset the touch coordinate with our offset
  313. int buttonID = (int)VRTK_SharedMethods.Mod(((givenTouchAngleDeflection.angle + (buttonAngle / 2f)) / buttonAngle), buttons.Count); //Convert angle into ButtonID (This is the magic)
  314. PointerEventData pointer = new PointerEventData(EventSystem.current); //Create a new EventSystem (UI) Event
  315. if (givenTouchAngleDeflection.deflection <= deadZone)
  316. {
  317. //No button selected. Use -1 to represent this
  318. buttonID = -1;
  319. }
  320. //If we changed buttons while moving, un-hover and un-click the last button we were on
  321. if (currentHover != buttonID && currentHover != -1)
  322. {
  323. ExecuteEvents.Execute(menuButtons[currentHover], pointer, ExecuteEvents.pointerUpHandler);
  324. ExecuteEvents.Execute(menuButtons[currentHover], pointer, ExecuteEvents.pointerExitHandler);
  325. buttons[currentHover].OnHoverExit.Invoke();
  326. if (executeOnUnclick && currentPress != -1 && buttonID != -1)
  327. {
  328. ExecuteEvents.Execute(menuButtons[buttonID], pointer, ExecuteEvents.pointerDownHandler);
  329. AttempHapticPulse(baseHapticStrength * 1.666f);
  330. }
  331. }
  332. if (evt == ButtonEvent.click) //Click button if click, and keep track of current press (executes button action)
  333. {
  334. if (buttonID != -1)
  335. {
  336. ExecuteEvents.Execute(menuButtons[buttonID], pointer, ExecuteEvents.pointerDownHandler);
  337. }
  338. currentPress = buttonID;
  339. if (!executeOnUnclick && buttonID != -1)
  340. {
  341. buttons[buttonID].OnClick.Invoke();
  342. AttempHapticPulse(baseHapticStrength * 2.5f);
  343. }
  344. }
  345. else if (evt == ButtonEvent.unclick) //Clear press id to stop invoking OnHold method (hide menu)
  346. {
  347. if (buttonID != -1)
  348. {
  349. ExecuteEvents.Execute(menuButtons[buttonID], pointer, ExecuteEvents.pointerUpHandler);
  350. }
  351. currentPress = -1;
  352. if (executeOnUnclick && buttonID != -1)
  353. {
  354. AttempHapticPulse(baseHapticStrength * 2.5f);
  355. buttons[buttonID].OnClick.Invoke();
  356. }
  357. }
  358. else if (evt == ButtonEvent.hoverOn && currentHover != buttonID && buttonID != -1) // Show hover UI event (darken button etc). Show menu
  359. {
  360. ExecuteEvents.Execute(menuButtons[buttonID], pointer, ExecuteEvents.pointerEnterHandler);
  361. buttons[buttonID].OnHoverEnter.Invoke();
  362. AttempHapticPulse(baseHapticStrength);
  363. }
  364. currentHover = buttonID; //Set current hover ID, need this to un-hover if selected button changes
  365. }
  366. protected virtual void InitTweenMenuScale(bool isShown)
  367. {
  368. if (tweenMenuScaleRoutine != null)
  369. {
  370. StopCoroutine(tweenMenuScaleRoutine);
  371. }
  372. tweenMenuScaleRoutine = StartCoroutine(TweenMenuScale(isShown));
  373. }
  374. //Simple tweening for menu, scales linearly from 0 to 1 and 1 to 0
  375. protected virtual IEnumerator TweenMenuScale(bool show)
  376. {
  377. float targetScale = 0f;
  378. Vector3 Dir = -1 * Vector3.one;
  379. if (show)
  380. {
  381. targetScale = 1;
  382. Dir = Vector3.one;
  383. }
  384. int i = 0; //Sanity check for infinite loops
  385. while (i < 250 && ((show && transform.localScale.x < targetScale) || (!show && transform.localScale.x > targetScale)))
  386. {
  387. transform.localScale += Dir * Time.deltaTime * 4f; //Tweening function - currently 0.25 second linear
  388. yield return true;
  389. i++;
  390. }
  391. transform.localScale = Dir * targetScale;
  392. }
  393. protected virtual void AttempHapticPulse(float strength)
  394. {
  395. if (strength > 0f && FireHapticPulse != null)
  396. {
  397. FireHapticPulse(strength);
  398. }
  399. }
  400. protected virtual void RemoveAllButtons()
  401. {
  402. if (menuButtons != null)
  403. {
  404. for (int i = 0; i < menuButtons.Count; i++)
  405. {
  406. DestroyImmediate(menuButtons[i]);
  407. }
  408. menuButtons.Clear();
  409. }
  410. }
  411. }
  412. }