//======= Copyright (c) Valve Corporation, All rights reserved. =============== // // Purpose: Displays text and button hints on the controllers // //============================================================================= using UnityEngine; using System.Collections; using System.Collections.Generic; using UnityEngine.UI; namespace Valve.VR.InteractionSystem { //------------------------------------------------------------------------- public class ControllerButtonHints : MonoBehaviour { public Material controllerMaterial; public Color flashColor = new Color( 1.0f, 0.557f, 0.0f ); public GameObject textHintPrefab; [Header( "Debug" )] public bool debugHints = false; private SteamVR_RenderModel renderModel; private Player player; private List renderers = new List(); private List flashingRenderers = new List(); private float startTime; private float tickCount; private enum OffsetType { Up, Right, Forward, Back } //Info for each of the buttons private class ButtonHintInfo { public string componentName; public List renderers; public Transform localTransform; //Text hint public GameObject textHintObject; public Transform textStartAnchor; public Transform textEndAnchor; public Vector3 textEndOffsetDir; public Transform canvasOffset; public Text text; public TextMesh textMesh; public Canvas textCanvas; public LineRenderer line; public float distanceFromCenter; public bool textHintActive = false; } private Dictionary buttonHintInfos; private Transform textHintParent; private List> componentButtonMasks = new List>(); private int colorID; public bool initialized { get; private set; } private Vector3 centerPosition = Vector3.zero; SteamVR_Events.Action renderModelLoadedAction; //------------------------------------------------- void Awake() { renderModelLoadedAction = SteamVR_Events.RenderModelLoadedAction( OnRenderModelLoaded ); colorID = Shader.PropertyToID( "_Color" ); } //------------------------------------------------- void Start() { player = Player.instance; } //------------------------------------------------- private void HintDebugLog( string msg ) { if ( debugHints ) { Debug.Log( "Hints: " + msg ); } } //------------------------------------------------- void OnEnable() { renderModelLoadedAction.enabled = true; } //------------------------------------------------- void OnDisable() { renderModelLoadedAction.enabled = false; Clear(); } //------------------------------------------------- private void OnParentHandInputFocusLost() { //Hide all the hints when the controller is no longer the primary attached object HideAllButtonHints(); HideAllText(); } //------------------------------------------------- // Gets called when the hand has been initialized and a render model has been set //------------------------------------------------- private void OnHandInitialized( int deviceIndex ) { //Create a new render model for the controller hints renderModel = new GameObject( "SteamVR_RenderModel" ).AddComponent(); renderModel.transform.parent = transform; renderModel.transform.localPosition = Vector3.zero; renderModel.transform.localRotation = Quaternion.identity; renderModel.transform.localScale = Vector3.one; renderModel.SetDeviceIndex( deviceIndex ); if ( !initialized ) { //The controller hint render model needs to be active to get accurate transforms for all the individual components renderModel.gameObject.SetActive( true ); } } //------------------------------------------------- void OnRenderModelLoaded( SteamVR_RenderModel renderModel, bool succeess ) { //Only initialize when the render model for the controller hints has been loaded if ( renderModel == this.renderModel ) { textHintParent = new GameObject( "Text Hints" ).transform; textHintParent.SetParent( this.transform ); textHintParent.localPosition = Vector3.zero; textHintParent.localRotation = Quaternion.identity; textHintParent.localScale = Vector3.one; //Get the button mask for each component of the render model using ( var holder = new SteamVR_RenderModel.RenderModelInterfaceHolder() ) { var renderModels = holder.instance; if ( renderModels != null ) { string renderModelDebug = "Components for render model " + renderModel.index; foreach ( Transform child in renderModel.transform ) { ulong buttonMask = renderModels.GetComponentButtonMask( renderModel.renderModelName, child.name ); componentButtonMasks.Add( new KeyValuePair( child.name, buttonMask ) ); renderModelDebug += "\n\t" + child.name + ": " + buttonMask; } //Uncomment to show the button mask for each component of the render model HintDebugLog( renderModelDebug ); } } buttonHintInfos = new Dictionary(); CreateAndAddButtonInfo( EVRButtonId.k_EButton_SteamVR_Trigger ); CreateAndAddButtonInfo( EVRButtonId.k_EButton_ApplicationMenu ); CreateAndAddButtonInfo( EVRButtonId.k_EButton_System ); CreateAndAddButtonInfo( EVRButtonId.k_EButton_Grip ); CreateAndAddButtonInfo( EVRButtonId.k_EButton_SteamVR_Touchpad ); CreateAndAddButtonInfo( EVRButtonId.k_EButton_A ); ComputeTextEndTransforms(); initialized = true; //Set the controller hints render model to not active renderModel.gameObject.SetActive( false ); } } //------------------------------------------------- private void CreateAndAddButtonInfo( EVRButtonId buttonID ) { Transform buttonTransform = null; List buttonRenderers = new List(); string buttonDebug = "Looking for button: " + buttonID; EVRButtonId searchButtonID = buttonID; if ( buttonID == EVRButtonId.k_EButton_Grip && SteamVR.instance.hmd_TrackingSystemName.ToLowerInvariant().Contains( "oculus" ) ) { searchButtonID = EVRButtonId.k_EButton_Axis2; } ulong buttonMaskForID = ( 1ul << (int)searchButtonID ); foreach ( KeyValuePair componentButtonMask in componentButtonMasks ) { if ( ( componentButtonMask.Value & buttonMaskForID ) == buttonMaskForID ) { buttonDebug += "\nFound component: " + componentButtonMask.Key + " " + componentButtonMask.Value; Transform componentTransform = renderModel.FindComponent( componentButtonMask.Key ); buttonTransform = componentTransform; buttonDebug += "\nFound componentTransform: " + componentTransform + " buttonTransform: " + buttonTransform; buttonRenderers.AddRange( componentTransform.GetComponentsInChildren() ); } } buttonDebug += "\nFound " + buttonRenderers.Count + " renderers for " + buttonID; foreach ( MeshRenderer renderer in buttonRenderers ) { buttonDebug += "\n\t" + renderer.name; } HintDebugLog( buttonDebug ); if ( buttonTransform == null ) { HintDebugLog( "Couldn't find buttonTransform for " + buttonID ); return; } ButtonHintInfo hintInfo = new ButtonHintInfo(); buttonHintInfos.Add( buttonID, hintInfo ); hintInfo.componentName = buttonTransform.name; hintInfo.renderers = buttonRenderers; //Get the local transform for the button hintInfo.localTransform = buttonTransform.Find( SteamVR_RenderModel.k_localTransformName ); OffsetType offsetType = OffsetType.Right; switch ( buttonID ) { case EVRButtonId.k_EButton_SteamVR_Trigger: { offsetType = OffsetType.Right; } break; case EVRButtonId.k_EButton_ApplicationMenu: { offsetType = OffsetType.Right; } break; case EVRButtonId.k_EButton_System: { offsetType = OffsetType.Right; } break; case Valve.VR.EVRButtonId.k_EButton_Grip: { offsetType = OffsetType.Forward; } break; case Valve.VR.EVRButtonId.k_EButton_SteamVR_Touchpad: { offsetType = OffsetType.Up; } break; } //Offset for the text end transform switch ( offsetType ) { case OffsetType.Forward: hintInfo.textEndOffsetDir = hintInfo.localTransform.forward; break; case OffsetType.Back: hintInfo.textEndOffsetDir = -hintInfo.localTransform.forward; break; case OffsetType.Right: hintInfo.textEndOffsetDir = hintInfo.localTransform.right; break; case OffsetType.Up: hintInfo.textEndOffsetDir = hintInfo.localTransform.up; break; } //Create the text hint object Vector3 hintStartPos = hintInfo.localTransform.position + ( hintInfo.localTransform.forward * 0.01f ); hintInfo.textHintObject = GameObject.Instantiate( textHintPrefab, hintStartPos, Quaternion.identity ) as GameObject; hintInfo.textHintObject.name = "Hint_" + hintInfo.componentName + "_Start"; hintInfo.textHintObject.transform.SetParent( textHintParent ); hintInfo.textHintObject.layer = gameObject.layer; hintInfo.textHintObject.tag = gameObject.tag; //Get all the relevant child objects hintInfo.textStartAnchor = hintInfo.textHintObject.transform.Find( "Start" ); hintInfo.textEndAnchor = hintInfo.textHintObject.transform.Find( "End" ); hintInfo.canvasOffset = hintInfo.textHintObject.transform.Find( "CanvasOffset" ); hintInfo.line = hintInfo.textHintObject.transform.Find( "Line" ).GetComponent(); hintInfo.textCanvas = hintInfo.textHintObject.GetComponentInChildren(); hintInfo.text = hintInfo.textCanvas.GetComponentInChildren(); hintInfo.textMesh = hintInfo.textCanvas.GetComponentInChildren(); hintInfo.textHintObject.SetActive( false ); hintInfo.textStartAnchor.position = hintStartPos; if ( hintInfo.text != null ) { hintInfo.text.text = hintInfo.componentName; } if ( hintInfo.textMesh != null ) { hintInfo.textMesh.text = hintInfo.componentName; } centerPosition += hintInfo.textStartAnchor.position; // Scale hint components to match player size hintInfo.textCanvas.transform.localScale = Vector3.Scale( hintInfo.textCanvas.transform.localScale, player.transform.localScale ); hintInfo.textStartAnchor.transform.localScale = Vector3.Scale( hintInfo.textStartAnchor.transform.localScale, player.transform.localScale ); hintInfo.textEndAnchor.transform.localScale = Vector3.Scale( hintInfo.textEndAnchor.transform.localScale, player.transform.localScale ); hintInfo.line.transform.localScale = Vector3.Scale( hintInfo.line.transform.localScale, player.transform.localScale ); } //------------------------------------------------- private void ComputeTextEndTransforms() { //This is done as a separate step after all the ButtonHintInfos have been initialized //to make the text hints fan out appropriately based on the button's position on the controller. centerPosition /= buttonHintInfos.Count; float maxDistanceFromCenter = 0.0f; foreach ( var hintInfo in buttonHintInfos ) { hintInfo.Value.distanceFromCenter = Vector3.Distance( hintInfo.Value.textStartAnchor.position, centerPosition ); if ( hintInfo.Value.distanceFromCenter > maxDistanceFromCenter ) { maxDistanceFromCenter = hintInfo.Value.distanceFromCenter; } } foreach ( var hintInfo in buttonHintInfos ) { Vector3 centerToButton = hintInfo.Value.textStartAnchor.position - centerPosition; centerToButton.Normalize(); centerToButton = Vector3.Project( centerToButton, renderModel.transform.forward ); //Spread out the text end positions based on the distance from the center float t = hintInfo.Value.distanceFromCenter / maxDistanceFromCenter; float scale = hintInfo.Value.distanceFromCenter * Mathf.Pow( 2, 10 * ( t - 1.0f ) ) * 20.0f; //Flip the direction of the end pos based on which hand this is float endPosOffset = 0.1f; Vector3 hintEndPos = hintInfo.Value.textStartAnchor.position + ( hintInfo.Value.textEndOffsetDir * endPosOffset ) + ( centerToButton * scale * 0.1f ); hintInfo.Value.textEndAnchor.position = hintEndPos; hintInfo.Value.canvasOffset.position = hintEndPos; hintInfo.Value.canvasOffset.localRotation = Quaternion.identity; } } //------------------------------------------------- private void ShowButtonHint( params EVRButtonId[] buttons ) { renderModel.gameObject.SetActive( true ); renderModel.GetComponentsInChildren( renderers ); for ( int i = 0; i < renderers.Count; i++ ) { Texture mainTexture = renderers[i].material.mainTexture; renderers[i].sharedMaterial = controllerMaterial; renderers[i].material.mainTexture = mainTexture; // This is to poke unity into setting the correct render queue for the model renderers[i].material.renderQueue = controllerMaterial.shader.renderQueue; } for ( int i = 0; i < buttons.Length; i++ ) { if ( buttonHintInfos.ContainsKey( buttons[i] ) ) { ButtonHintInfo hintInfo = buttonHintInfos[buttons[i]]; foreach ( MeshRenderer renderer in hintInfo.renderers ) { if ( !flashingRenderers.Contains( renderer ) ) { flashingRenderers.Add( renderer ); } } } } startTime = Time.realtimeSinceStartup; tickCount = 0.0f; } //------------------------------------------------- private void HideAllButtonHints() { Clear(); renderModel.gameObject.SetActive( false ); } //------------------------------------------------- private void HideButtonHint( params EVRButtonId[] buttons ) { Color baseColor = controllerMaterial.GetColor( colorID ); for ( int i = 0; i < buttons.Length; i++ ) { if ( buttonHintInfos.ContainsKey( buttons[i] ) ) { ButtonHintInfo hintInfo = buttonHintInfos[buttons[i]]; foreach ( MeshRenderer renderer in hintInfo.renderers ) { renderer.material.color = baseColor; flashingRenderers.Remove( renderer ); } } } if ( flashingRenderers.Count == 0 ) { renderModel.gameObject.SetActive( false ); } } //------------------------------------------------- private bool IsButtonHintActive( EVRButtonId button ) { if ( buttonHintInfos.ContainsKey( button ) ) { ButtonHintInfo hintInfo = buttonHintInfos[button]; foreach ( MeshRenderer buttonRenderer in hintInfo.renderers ) { if ( flashingRenderers.Contains( buttonRenderer ) ) { return true; } } } return false; } //------------------------------------------------- private IEnumerator TestButtonHints() { while ( true ) { ShowButtonHint( EVRButtonId.k_EButton_SteamVR_Trigger ); yield return new WaitForSeconds( 1.0f ); ShowButtonHint( EVRButtonId.k_EButton_ApplicationMenu ); yield return new WaitForSeconds( 1.0f ); ShowButtonHint( EVRButtonId.k_EButton_System ); yield return new WaitForSeconds( 1.0f ); ShowButtonHint( EVRButtonId.k_EButton_Grip ); yield return new WaitForSeconds( 1.0f ); ShowButtonHint( EVRButtonId.k_EButton_SteamVR_Touchpad ); yield return new WaitForSeconds( 1.0f ); } } //------------------------------------------------- private IEnumerator TestTextHints() { while ( true ) { ShowText( EVRButtonId.k_EButton_SteamVR_Trigger, "Trigger" ); yield return new WaitForSeconds( 3.0f ); ShowText( EVRButtonId.k_EButton_ApplicationMenu, "Application" ); yield return new WaitForSeconds( 3.0f ); ShowText( EVRButtonId.k_EButton_System, "System" ); yield return new WaitForSeconds( 3.0f ); ShowText( EVRButtonId.k_EButton_Grip, "Grip" ); yield return new WaitForSeconds( 3.0f ); ShowText( EVRButtonId.k_EButton_SteamVR_Touchpad, "Touchpad" ); yield return new WaitForSeconds( 3.0f ); HideAllText(); yield return new WaitForSeconds( 3.0f ); } } //------------------------------------------------- void Update() { if ( renderModel != null && renderModel.gameObject.activeInHierarchy && flashingRenderers.Count > 0 ) { Color baseColor = controllerMaterial.GetColor( colorID ); float flash = ( Time.realtimeSinceStartup - startTime ) * Mathf.PI * 2.0f; flash = Mathf.Cos( flash ); flash = Util.RemapNumberClamped( flash, -1.0f, 1.0f, 0.0f, 1.0f ); float ticks = ( Time.realtimeSinceStartup - startTime ); if ( ticks - tickCount > 1.0f ) { tickCount += 1.0f; SteamVR_Controller.Device device = SteamVR_Controller.Input( (int)renderModel.index ); if ( device != null ) { device.TriggerHapticPulse(); } } for ( int i = 0; i < flashingRenderers.Count; i++ ) { Renderer r = flashingRenderers[i]; r.material.SetColor( colorID, Color.Lerp( baseColor, flashColor, flash ) ); } if ( initialized ) { foreach ( var hintInfo in buttonHintInfos ) { if ( hintInfo.Value.textHintActive ) { UpdateTextHint( hintInfo.Value ); } } } } } //------------------------------------------------- private void UpdateTextHint( ButtonHintInfo hintInfo ) { Transform playerTransform = player.hmdTransform; Vector3 vDir = playerTransform.position - hintInfo.canvasOffset.position; Quaternion standardLookat = Quaternion.LookRotation( vDir, Vector3.up ); Quaternion upsideDownLookat = Quaternion.LookRotation( vDir, playerTransform.up ); float flInterp; if ( playerTransform.forward.y > 0.0f ) { flInterp = Util.RemapNumberClamped( playerTransform.forward.y, 0.6f, 0.4f, 1.0f, 0.0f ); } else { flInterp = Util.RemapNumberClamped( playerTransform.forward.y, -0.8f, -0.6f, 1.0f, 0.0f ); } hintInfo.canvasOffset.rotation = Quaternion.Slerp( standardLookat, upsideDownLookat, flInterp ); Transform lineTransform = hintInfo.line.transform; hintInfo.line.useWorldSpace = false; hintInfo.line.SetPosition( 0, lineTransform.InverseTransformPoint( hintInfo.textStartAnchor.position ) ); hintInfo.line.SetPosition( 1, lineTransform.InverseTransformPoint( hintInfo.textEndAnchor.position ) ); } //------------------------------------------------- private void Clear() { renderers.Clear(); flashingRenderers.Clear(); } //------------------------------------------------- private void ShowText( EVRButtonId button, string text, bool highlightButton = true ) { if ( buttonHintInfos.ContainsKey( button ) ) { ButtonHintInfo hintInfo = buttonHintInfos[button]; hintInfo.textHintObject.SetActive( true ); hintInfo.textHintActive = true; if ( hintInfo.text != null ) { hintInfo.text.text = text; } if ( hintInfo.textMesh != null ) { hintInfo.textMesh.text = text; } UpdateTextHint( hintInfo ); if ( highlightButton ) { ShowButtonHint( button ); } renderModel.gameObject.SetActive( true ); } } //------------------------------------------------- private void HideText( EVRButtonId button ) { if ( buttonHintInfos.ContainsKey( button ) ) { ButtonHintInfo hintInfo = buttonHintInfos[button]; hintInfo.textHintObject.SetActive( false ); hintInfo.textHintActive = false; HideButtonHint( button ); } } //------------------------------------------------- private void HideAllText() { foreach ( var hintInfo in buttonHintInfos ) { hintInfo.Value.textHintObject.SetActive( false ); hintInfo.Value.textHintActive = false; } HideAllButtonHints(); } //------------------------------------------------- private string GetActiveHintText( EVRButtonId button ) { if ( buttonHintInfos.ContainsKey( button ) ) { ButtonHintInfo hintInfo = buttonHintInfos[button]; if ( hintInfo.textHintActive ) { return hintInfo.text.text; } } return string.Empty; } //------------------------------------------------- // These are the static functions which are used to show/hide the hints //------------------------------------------------- //------------------------------------------------- private static ControllerButtonHints GetControllerButtonHints( Hand hand ) { if ( hand != null ) { ControllerButtonHints hints = hand.GetComponentInChildren(); if ( hints != null && hints.initialized ) { return hints; } } return null; } //------------------------------------------------- public static void ShowButtonHint( Hand hand, params EVRButtonId[] buttons ) { ControllerButtonHints hints = GetControllerButtonHints( hand ); if ( hints != null ) { hints.ShowButtonHint( buttons ); } } //------------------------------------------------- public static void HideButtonHint( Hand hand, params EVRButtonId[] buttons ) { ControllerButtonHints hints = GetControllerButtonHints( hand ); if ( hints != null ) { hints.HideButtonHint( buttons ); } } //------------------------------------------------- public static void HideAllButtonHints( Hand hand ) { ControllerButtonHints hints = GetControllerButtonHints( hand ); if ( hints != null ) { hints.HideAllButtonHints(); } } //------------------------------------------------- public static bool IsButtonHintActive( Hand hand, EVRButtonId button ) { ControllerButtonHints hints = GetControllerButtonHints( hand ); if ( hints != null ) { return hints.IsButtonHintActive( button ); } return false; } //------------------------------------------------- public static void ShowTextHint( Hand hand, EVRButtonId button, string text, bool highlightButton = true ) { ControllerButtonHints hints = GetControllerButtonHints( hand ); if ( hints != null ) { hints.ShowText( button, text, highlightButton ); } } //------------------------------------------------- public static void HideTextHint( Hand hand, EVRButtonId button ) { ControllerButtonHints hints = GetControllerButtonHints( hand ); if ( hints != null ) { hints.HideText( button ); } } //------------------------------------------------- public static void HideAllTextHints( Hand hand ) { ControllerButtonHints hints = GetControllerButtonHints( hand ); if ( hints != null ) { hints.HideAllText(); } } //------------------------------------------------- public static string GetActiveHintText( Hand hand, EVRButtonId button ) { ControllerButtonHints hints = GetControllerButtonHints( hand ); if ( hints != null ) { return hints.GetActiveHintText( button ); } return string.Empty; } } }