// Radial Menu|Prefabs|0110
namespace VRTK
{
using UnityEngine;
using System.Collections;
using UnityEngine.Events;
using System.Collections.Generic;
using UnityEngine.UI;
using UnityEngine.EventSystems;
public struct TouchAngleDeflection
{
public float angle;
public float deflection;
///
/// Constructs an object to hold the angle and deflection of the user's touch on the touchpad
///
/// The angle of the touch on the radial menu.
/// Deflection of the touch, where 0 is the centre and 1 is the edge.
public TouchAngleDeflection(float angle, float deflection)
{
this.angle = angle;
this.deflection = deflection;
}
}
public delegate void HapticPulseEventHandler(float strength);
///
/// 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.
///
///
/// **Prefab Usage:**
/// * Place the `VRTK/Prefabs/RadialMenu/RadialMenu` prefab as a child of a Controller script alias GameObject.
///
///
/// `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.
///
[ExecuteInEditMode]
public class VRTK_RadialMenu : MonoBehaviour
{
[System.Serializable]
public class RadialMenuButton
{
public Sprite ButtonIcon;
public UnityEvent OnClick = new UnityEvent();
public UnityEvent OnHold = new UnityEvent();
public UnityEvent OnHoverEnter = new UnityEvent();
public UnityEvent OnHoverExit = new UnityEvent();
}
public enum ButtonEvent
{
hoverOn,
hoverOff,
click,
unclick
}
[Tooltip("An array of Buttons that define the interactive buttons required to be displayed as part of the radial menu.")]
public List buttons = new List();
[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.")]
public GameObject buttonPrefab;
[Tooltip("If checked, then the buttons will be auto generated on awake.")]
public bool generateOnAwake = true;
[Tooltip("Percentage of the menu the buttons should fill, 1.0 is a pie slice, 0.1 is a thin ring.")]
[Range(0f, 1f)]
public float buttonThickness = 0.5f;
[Tooltip("The background colour of the buttons, default is white.")]
public Color buttonColor = Color.white;
[Tooltip("The distance the buttons should move away from the centre. This creates space between the individual buttons.")]
public float offsetDistance = 1;
[Tooltip("The additional rotation of the Radial Menu.")]
[Range(0, 359)]
public float offsetRotation;
[Tooltip("Whether button icons should rotate according to their arc or be vertical compared to the controller.")]
public bool rotateIcons;
[Tooltip("The margin in pixels that the icon should keep within the button.")]
public float iconMargin;
[Tooltip("Whether the buttons are shown")]
public bool isShown;
[Tooltip("Whether the buttons should be visible when not in use.")]
public bool hideOnRelease;
[Tooltip("Whether the button action should happen when the button is released, as opposed to happening immediately when the button is pressed.")]
public bool executeOnUnclick;
[Tooltip("The base strength of the haptic pulses when the selected button is changed, or a button is pressed. Set to zero to disable.")]
[Range(0, 1)]
public float baseHapticStrength;
[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.")]
[Range(0, 1)]
public float deadZone = 0;
public event HapticPulseEventHandler FireHapticPulse;
//Has to be public to keep state from editor -> play mode?
[Tooltip("The actual GameObjects that make up the radial menu.")]
public List menuButtons = new List();
protected int currentHover = -1;
protected int currentPress = -1;
protected Coroutine tweenMenuScaleRoutine;
///
/// The HoverButton method is used to set the button hover at a given angle.
///
/// The angle on the radial menu.
[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.")]
public virtual void HoverButton(float angle)
{
HoverButton(new TouchAngleDeflection(angle, 1));
}
///
/// The HoverButton method is used to set the button hover at a given angle and deflection.
///
/// The angle and deflection on the radial menu.
public virtual void HoverButton(TouchAngleDeflection givenTouchAngleDeflection)
{
InteractButton(givenTouchAngleDeflection, ButtonEvent.hoverOn);
}
///
/// The ClickButton method is used to set the button click at a given angle.
///
/// The angle on the radial menu.
[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.")]
public virtual void ClickButton(float angle)
{
ClickButton(new TouchAngleDeflection(angle, 1));
}
///
/// The ClickButton method is used to set the button click at a given angle and deflection.
///
/// The angle and deflection on the radial menu.
public virtual void ClickButton(TouchAngleDeflection givenTouchAngleDeflection)
{
InteractButton(givenTouchAngleDeflection, ButtonEvent.click);
}
///
/// The UnClickButton method is used to set the button unclick at a given angle.
///
/// The angle on the radial menu.
[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.")]
public virtual void UnClickButton(float angle)
{
UnClickButton(new TouchAngleDeflection(angle, 1));
}
///
/// The UnClickButton method is used to set the button unclick at a given angle and deflection.
///
/// The angle and deflection on the radial menu.
public virtual void UnClickButton(TouchAngleDeflection givenTouchAngleDeflection)
{
InteractButton(givenTouchAngleDeflection, ButtonEvent.unclick);
}
///
/// The ToggleMenu method is used to show or hide the radial menu.
///
public virtual void ToggleMenu()
{
if (isShown)
{
HideMenu(true);
}
else
{
ShowMenu();
}
}
///
/// The StopTouching method is used to stop touching the menu.
///
public virtual void StopTouching()
{
if (currentHover != -1)
{
PointerEventData pointer = new PointerEventData(EventSystem.current);
ExecuteEvents.Execute(menuButtons[currentHover], pointer, ExecuteEvents.pointerExitHandler);
buttons[currentHover].OnHoverExit.Invoke();
currentHover = -1;
}
}
///
/// The ShowMenu method is used to show the menu.
///
public virtual void ShowMenu()
{
if (!isShown)
{
isShown = true;
InitTweenMenuScale(isShown);
}
}
///
/// The GetButton method is used to get a button from the menu.
///
/// The id of the button to retrieve.
/// The found radial menu button.
public virtual RadialMenuButton GetButton(int id)
{
if (id < buttons.Count)
{
return buttons[id];
}
return null;
}
///
/// The HideMenu method is used to hide the menu.
///
/// If true then the menu is always hidden.
public virtual void HideMenu(bool force)
{
if (isShown && (hideOnRelease || force))
{
isShown = false;
InitTweenMenuScale(isShown);
}
}
///
/// The RegenerateButtons method creates all the button arcs and populates them with desired icons.
///
public void RegenerateButtons()
{
RemoveAllButtons();
for (int i = 0; i < buttons.Count; i++)
{
// Initial placement/instantiation
GameObject newButton = Instantiate(buttonPrefab);
newButton.transform.SetParent(transform);
newButton.transform.localScale = Vector3.one;
newButton.GetComponent().offsetMax = Vector2.zero;
newButton.GetComponent().offsetMin = Vector2.zero;
//Setup button arc
UICircle circle = newButton.GetComponent();
if (buttonThickness == 1f)
{
circle.fill = true;
}
else
{
circle.thickness = (int)(buttonThickness * (GetComponent().rect.width / 2f));
}
int fillPerc = (int)(100f / buttons.Count);
circle.fillPercent = fillPerc;
circle.color = buttonColor;
//Final placement/rotation
float angle = ((360f / buttons.Count) * i) + offsetRotation;
newButton.transform.localEulerAngles = new Vector3(0, 0, angle);
newButton.layer = 4; //UI Layer
newButton.transform.localPosition = Vector3.zero;
if (circle.fillPercent < 55)
{
float angleRad = (angle * Mathf.PI) / 180f;
Vector2 angleVector = new Vector2(-Mathf.Cos(angleRad), -Mathf.Sin(angleRad));
newButton.transform.localPosition += (Vector3)angleVector * offsetDistance;
}
//Place and populate Button Icon
GameObject buttonIcon = newButton.GetComponentInChildren().gameObject;
if (buttons[i].ButtonIcon == null)
{
buttonIcon.SetActive(false);
}
else
{
buttonIcon.GetComponent().sprite = buttons[i].ButtonIcon;
buttonIcon.transform.localPosition = new Vector2(-1 * ((newButton.GetComponent().rect.width / 2f) - (circle.thickness / 2f)), 0);
//Min icon size from thickness and arc
float scale1 = Mathf.Abs(circle.thickness);
float absButtonIconXPos = Mathf.Abs(buttonIcon.transform.localPosition.x);
float bAngle = (359f * circle.fillPercent * 0.01f * Mathf.PI) / 180f;
float scale2 = (absButtonIconXPos * 2f * Mathf.Sin(bAngle / 2f));
if (circle.fillPercent > 24) //Scale calc doesn't work for > 90 degrees
{
scale2 = float.MaxValue;
}
float iconScale = Mathf.Min(scale1, scale2) - iconMargin;
buttonIcon.GetComponent().sizeDelta = new Vector2(iconScale, iconScale);
//Rotate icons all vertically if desired
if (!rotateIcons)
{
buttonIcon.transform.eulerAngles = GetComponentInParent