using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
#if ZED_STEAM_VR
using Valve.VR;
#endif
///
/// Lets the user choose the tracked object that the ZED is anchored to, for the MR calibration scene.
/// Works by spawning a prefab button for each tracked object, complete with graphics, and arranging them
/// into a grid. Each is assigned a callback that will enable the scene's CameraAnchor object and set its
/// tracked object accordingly.
///
public class ChooseTrackedObjectMenu : MonoBehaviour
{
///
/// Prefab object for the buttons you press to choose the anchor.
/// By default, it's Prefabs/ChooseTrackedObjectButton.
///
[Tooltip("Prefab object for the buttons you press to choose the anchor. " +
"By default, it's Prefabs/ChooseTrackedObjectButton.")]
public GameObject chooseObjectButtonPrefab;
public bool updateTrackedObjects = true;
public float updateIntervalSeconds = 1f;
///
/// How wide the grid of tracked object buttons can be. Objects are stacked upwards.
///
[Tooltip("How wide the grid of tracked object buttons can be. Objects are stacked upwards. ")]
[Space(5)]
[Header("Grid")]
public int maxColumns = 3;
///
/// Width of the button prefab object. Used for spacing them in the grid horizontally.
///
[Tooltip("Width of the button prefab object. Used for spacing them in the grid horizontally. ")]
public float objectWidth = 0.3f;
///
/// Width of the button prefab object. Used for spacing them in the grid vertically.
///
[Tooltip("Width of the button prefab object. Used for spacing them in the grid vertically. ")]
public float objectHeight = 0.4f;
///
/// All button objects instantiated at runtime.
///
private List allButtons = new List();
///
/// Temporary left controller object that's created at spawn so that both left and right controllers
/// can be used to choose an anchor. Destroyed once an anchor is chosen.
///
[Space(5)]
[Header("Anchors")]
[Tooltip("Temporary left controller object that's created at spawn so that both left and right controllers " +
"can be used to choose an anchor. Destroyed once an anchor is chosen. ")]
public ZEDControllerTracker tempLeftController;
///
/// The scene's CameraAnchor object, which holds the ZED camera. Should be disabled at start.
///
[Tooltip("The scene's CameraAnchor object, which holds the ZED camera. Should be disabled at start. ")]
public CameraAnchor zedAnchor;
///
/// The scene's left controller. Should be disabled at start, since the left controller has the menu by default,
/// which doesn't do anything until you've chosen an anchor.
///
[Tooltip("The scene's left controller. Should be disabled at start, since the left controller has the menu by default, " +
"which doesn't do anything until you've chosen an anchor. ")]
public ZEDControllerTracker leftController;
///
/// The scene's right controller. By default, this can interact with buttons/grabbables, so it's always enabled
/// unless the right controller is set to be the ZED's anchor.
///
[Tooltip("The scene's right controller. By default, this can interact with buttons/grabbables, so it's always enabled " +
"unless the right controller is set to be the ZED's anchor. ")]
public ZEDControllerTracker rightController;
///
/// The "belly" menu controller, which provides the menu normally on the left (or right) hands when a Tracker is
/// the ZED's anchor. Disabled by default.
///
[Tooltip("The 'belly' menu controller, which provides the menu normally on the left (or right) hands when a Tracker is " +
"the ZED's anchor. Disabled by default. ")]
public BellyMenu bellyMenu;
///
/// The transform holding the 2D floating view screen object. Disabled at start, enabled once the user chooses an anchor.
///
[Tooltip("The transform holding the 2D floating view screen object. Disabled at start, enabled once the user chooses an anchor. ")]
public GameObject viewScreen;
///
/// Transform holding the 3D textual instructions for choosing an anchor. Used to move it above the procedural grid of buttons.
///
[Space(5)]
[Tooltip("Transform holding the 3D textual instructions for choosing an anchor. Used to move it above the procedural grid of buttons. ")]
public Transform instructionsText;
public float textBottomMargin = 0.2f;
#if ZED_STEAM_VR
///
/// Action we'll invoke when a controller is connected/disconnected. Cached as it's a lambda function, and we need to
/// remove that listener in OnDestroy().
///
private UnityAction deviceConnectedAction;
#endif
#if ZED_OCULUS
public const int TOUCH_INDEX_LEFT = 0;
public const int TOUCH_INDEX_RIGHT = 1;
#endif
// Use this for initialization
void Start()
{
FindTrackedObjects(); //TODO: Make this happen in Update() at regular intervals, and clean up existing devices.
MessageDisplay.DisplayMessageAll("Which object is holding the ZED?");
#if ZED_STEAM_VR
//Subscribe to controllers being connected/disconnected so we can update the available devices.
deviceConnectedAction = (x, y) => FindTrackedObjects();
SteamVR_Events.DeviceConnected.AddListener(deviceConnectedAction);
#endif
}
///
/// Sets the chosen device as the anchor object, and enables/disables scene objects appropriately.
/// Called by ChooseTrackedObjectMenu when the user clicks it.
///
/// Index of the tracked object.
private void OnTrackedObjectSelected(int deviceindex)
{
//Make sure it's valid.
ZEDControllerTracker.Devices trackeddevice = ZEDControllerTracker.Devices.Hmd; //Can't be HMD at end - will catch.
#if ZED_STEAM_VR
ETrackedDeviceClass dclass = OpenVR.System.GetTrackedDeviceClass((uint)deviceindex);
if (dclass == ETrackedDeviceClass.GenericTracker) trackeddevice = ZEDControllerTracker.Devices.ViveTracker;
else if (dclass == ETrackedDeviceClass.Controller)
{
ETrackedControllerRole role = OpenVR.System.GetControllerRoleForTrackedDeviceIndex((uint)deviceindex);
if (role == ETrackedControllerRole.LeftHand) trackeddevice = ZEDControllerTracker.Devices.LeftController;
else if (role == ETrackedControllerRole.RightHand) trackeddevice = ZEDControllerTracker.Devices.RightController;
else
{
Debug.Log("Couldn't assign to device of index " + deviceindex + " as it's a controller with an unknown role.");
}
//TODO: Make sure this works for a third controller.
}
#endif
#if ZED_OCULUS
if(deviceindex == TOUCH_INDEX_LEFT)
{
trackeddevice = ZEDControllerTracker.Devices.LeftController;
}
else if (deviceindex == TOUCH_INDEX_RIGHT)
{
trackeddevice = ZEDControllerTracker.Devices.RightController;
}
#endif
//Set up the anchor object, and readjust controllers if needed.
zedAnchor.gameObject.SetActive(true);
zedAnchor.controllerTracker.deviceToTrack = trackeddevice;
#if ZED_STEAM_VR
leftController.index = tempLeftController.index;
#endif
switch (trackeddevice)
{
#if ZED_STEAM_VR
case ZEDControllerTracker.Devices.ViveTracker:
leftController.index = tempLeftController.index;
leftController.gameObject.SetActive(true);
break;
#endif
case ZEDControllerTracker.Devices.LeftController:
bellyMenu.gameObject.SetActive(true);
break;
case ZEDControllerTracker.Devices.RightController:
PrimaryHandSwitcher switcher = leftController.GetComponentInChildren(true);
if (!switcher) break;
ToggleGroup3D handtoggle = switcher.transform.GetComponentInChildren(true);
handtoggle.toggleAtStart = false;
leftController.gameObject.SetActive(true);
switcher.SetPrimaryHand(false); //Switch to being left-handed.
Destroy(rightController.gameObject);
rightController = null;
bellyMenu.gameObject.SetActive(true);
break;
case ZEDControllerTracker.Devices.Hmd:
default:
Debug.LogError("trackeddevice value somehow not set to valid value - must be a controller or tracker. ");
break;
}
if (viewScreen) viewScreen.SetActive(true);
Destroy(tempLeftController.gameObject);
tempLeftController = null;
//We're all done with this menu, so destroy it all.
Destroy(gameObject);
}
private void OnDestroy()
{
#if ZED_STEAM_VR
SteamVR_Events.DeviceConnected.RemoveListener(deviceConnectedAction);
#endif
}
///
/// Detects all tracked objects that could potentially be the ZED's anchor, and makes a button for each.
///
public void FindTrackedObjects()
{
//If there are any existing buttons, destroy them all.
foreach(ChooseTrackedObjectButton oldbutton in allButtons)
{
Destroy(oldbutton.gameObject);
}
allButtons.Clear();
List newbuttons = new List();
#if ZED_STEAM_VR
for (uint i = 0; i < 14; i++)
{
var error = ETrackedPropertyError.TrackedProp_Success;
var chsnresult = new System.Text.StringBuilder((int)64);
OpenVR.System.GetStringTrackedDeviceProperty(i, ETrackedDeviceProperty.Prop_ControllerType_String, chsnresult, 64, ref error);
if (error == ETrackedPropertyError.TrackedProp_Success && chsnresult.ToString() != "")
{
//Create tracked object button.
ChooseTrackedObjectButton button;
CreateTrackedObjectPrefab(i, out button);
if (button != null)
{
newbuttons.Add(button);
}
}
}
//allButtons.Clear();
foreach (ChooseTrackedObjectButton button in newbuttons)
{
allButtons.Add(button);
}
#elif ZED_OCULUS
/*//Warn the user if they don't have both controllers connected, because they need one to hold the ZED
//and another to calibrate.
OVRInput.Controller controllers = OVRInput.GetConnectedControllers();
if (controllers != OVRInput.Controller.Touch)
{
Debug.Log("Warning: You need at least two controllers connected to use the calibration app: One to hold " +
"the ZED and another to interact with the app.");
}*/
//Make a left controller button.
ChooseTrackedObjectButton leftButton;
CreateTrackedObjectPrefab(TOUCH_INDEX_LEFT, out leftButton);
if (leftButton != null) allButtons.Add(leftButton);
//Make a right controller button.
ChooseTrackedObjectButton rightButton;
CreateTrackedObjectPrefab(TOUCH_INDEX_RIGHT, out rightButton);
if (rightButton != null) allButtons.Add(rightButton);
#endif
ArrangeIntoGrid(allButtons, objectWidth, objectHeight, maxColumns);
}
///
/// Creates a button for a tracked object detected in FindTrackedObjects() and sets it up.
///
/// Index of the tracked object.
/// Required ChooseTrackedObjectButton script that must be on the prefab.
private GameObject CreateTrackedObjectPrefab(uint deviceindex, out ChooseTrackedObjectButton scriptref)
{
string label = "ERROR";
#if ZED_STEAM_VR
ETrackedDeviceClass dclass = OpenVR.System.GetTrackedDeviceClass(deviceindex);
ETrackedControllerRole role = OpenVR.System.GetControllerRoleForTrackedDeviceIndex(deviceindex);
if (!(dclass == ETrackedDeviceClass.Controller || dclass == ETrackedDeviceClass.GenericTracker))
{
//Debug.LogError("Tried to create button for tracked device of index " + deviceindex + ", but it's role is " + dclass + ".");
scriptref = null;
return null;
}
if (role == ETrackedControllerRole.LeftHand)
{
label = "Left\r\nController";
}
else if (role == ETrackedControllerRole.RightHand)
{
label = "Right\r\nController";
}
else if (dclass == ETrackedDeviceClass.GenericTracker)
{
label = "Tracker";
}
else
{
//Debug.LogError("Tried to create button for tracked device of index " + deviceindex + " with an invalid role/device class combo: " +
// role + " / " + dclass);
scriptref = null;
return null;
}
#elif ZED_OCULUS
if (deviceindex == TOUCH_INDEX_LEFT)
{
label = "Left\r\nController";
}
else if (deviceindex == TOUCH_INDEX_RIGHT)
{
label = "Right\r\nController";
}
#endif
GameObject buttongo = Instantiate(chooseObjectButtonPrefab, transform, false);
scriptref = buttongo.GetComponentInChildren();
scriptref.SetDeviceIndex((int)deviceindex);
scriptref.SetLabel(label);
buttongo.name = "Select " + label + " Button";
scriptref.OnTrackedObjectSelected += OnTrackedObjectSelected;
return buttongo;
}
///
/// Creates a button for a tracked object detected in FindTrackedObjects() and sets it up.
/// Overload that doesn't require you to output a ChooseTrackedObjectButton reference.
///
/// Index of the tracked object.
private GameObject CreateTrackedObjectPrefab(uint deviceindex)
{
ChooseTrackedObjectButton throwaway;
return CreateTrackedObjectPrefab(deviceindex, out throwaway);
}
///
/// Arranges the anchor buttons into a grid, spaced based on the total number and user preferences.
/// Also adjusts the text object (instructionsText) to be above the grid.
///
/// List of all instantiated buttons
/// Width of each prefab, used for spacing.
/// Height of each prefab, used for spacing.
/// How wide the grid can be before it stacks to a new row.
private void ArrangeIntoGrid(List buttonlist, float objwidth, float objheight, int maxcolumns)
{
int numrows = Mathf.CeilToInt(buttonlist.Count / (float)maxcolumns);
int remainingobjects = buttonlist.Count;
float ycenterpoint = numrows / 2f + 0.5f;
ycenterpoint -= 1;
for (int v = 0; v < numrows; v++)
{
//Row height info.
float yoffset = v - ycenterpoint;
yoffset *= objheight;
int thisrowwidth = (maxcolumns < remainingobjects) ? maxcolumns : remainingobjects;
float xcenterpoint = thisrowwidth / 2f + 0.5f; //If you drew a line in the middle of a row of objects, where would it fall?
xcenterpoint -= 1; //To account for index starting at 0. (yes we could just subtract 0.5 instead of adding but that's less clear.)
for (int u = 0; u < thisrowwidth; u++)
{
int goindex = v * maxcolumns + u;
float xoffset = u - xcenterpoint;
xoffset *= objwidth;
//TODO: Y offset
Vector3 oldpos = buttonlist[goindex].transform.localPosition;
buttonlist[goindex].transform.localPosition = new Vector3(xoffset, yoffset, oldpos.z);
}
remainingobjects -= thisrowwidth; //To simplify process of deciding how long the rows should be.
}
//Reposition the instruction text to be just above the grid.
instructionsText.localPosition = new Vector3(instructionsText.localPosition.x, numrows / 2f * objheight + textBottomMargin, instructionsText.localPosition.z);
}
}