using System; using System.Collections; using System.Collections.Generic; using UnityEngine; using UnityEngine.InputSystem; using UnityEngine.XR.Interaction.Toolkit; namespace _PROJECT.NewHandPresence { public class TutorialController : MonoBehaviour { public enum TutorialState { Initializing, Turn, Teleport, WaitForGrip, Grip, Done, Move } public ActionBasedController leftHand; public ActionBasedController rightHand; public ActionBasedContinuousMoveProvider moveProvider; public ActionBasedSnapTurnProvider turnProvider; public TeleportationProvider teleportProvider; public GameObject billboardTemplate; private XRControllerHintController _leftHintController; private XRControllerHintController _rightHintController; private SmartHandPresence _leftSmartHandPresence; private SmartHandPresence _rightSmartHandPresence; public TutorialState _state = TutorialState.Initializing; private Camera _camera; private List _grabInteractables = new List(); private XRGrabInteractable _grabInteractable; public event Action OnGrab; private GameObject _billboard; public enum TutorialInfoKey { Initialized, LeftHintController, RightHintController, LeftSmartHandPresence, RightSmartHandPresence } protected Dictionary initializationInfoStatus = new Dictionary(); protected Coroutine initializationInfoCoroutine; private void Awake() { initializationInfoStatus.Add(TutorialInfoKey.Initialized, false); initializationInfoStatus.Add(TutorialInfoKey.LeftHintController, false); initializationInfoStatus.Add(TutorialInfoKey.RightHintController, false); initializationInfoStatus.Add(TutorialInfoKey.LeftSmartHandPresence, false); initializationInfoStatus.Add(TutorialInfoKey.RightSmartHandPresence, false); } private void Update() { if (_state == TutorialState.Initializing) { TryInitialize(); } if (_state == TutorialState.WaitForGrip) { TryFindXRGrabInteractable(); } } private void TryFindXRGrabInteractable() { // Find closest interactable in list XRGrabInteractable closestInteractable = null; var closestDistance = float.MaxValue; foreach (var grabInteractable in _grabInteractables) { var distance = Vector3.Distance(grabInteractable.transform.position, _camera.transform.position); if (!(distance < closestDistance)) continue; if (grabInteractable.CompareTag("Door")) continue; closestDistance = distance; closestInteractable = grabInteractable; } if (closestInteractable == null) return; _grabInteractable = closestInteractable; UpdateState(_state.Next()); } private void UpdateState(TutorialState newState) { _state = newState; Debug.Log($"Tutorial state: {_state}"); if (_state == TutorialState.Initializing) return; SetHandsVisibility(true); _rightHintController.HideHint(); _leftHintController.HideHint(); switch (_state) { case TutorialState.Initializing: break; case TutorialState.Turn: ShowTurnHint(); SetControllerVisibility(true); break; case TutorialState.Move: ShowLocomotionHint(); SetControllerVisibility(true); break; case TutorialState.Teleport: ShowTeleportHint(); SetControllerVisibility(true); break; case TutorialState.WaitForGrip: SetControllerVisibility(false); break; case TutorialState.Grip: //SetHandsVisibility(false); CreateBillboard(_grabInteractable.gameObject, "Grab me!"); ShowGripHint(); SetControllerVisibility(true); break; case TutorialState.Done: SetControllerVisibility(false); DestroyBillboard(); break; default: throw new ArgumentOutOfRangeException(); } } private void CreateBillboard(GameObject gmeObject, string gripMe) { var number = 1 / gmeObject.transform.localScale.x; // Create billboard as a child on grab interactable _billboard = Instantiate(billboardTemplate, gmeObject.transform); // Set local position above grab interactable _billboard.transform.localPosition = new Vector3(0, 1, 0); // Unscale billboard relative to parent _billboard.transform.localScale = Vector3.one * number; // Get ControllerTextHint on billboard var controllerTextHint = _billboard.GetComponentInChildren(); // Set text controllerTextHint.defaultText = gripMe; controllerTextHint.ShowHint(gripMe); // Set interactable to outlined layer //gmeObject.layer = LayerMask.NameToLayer("Outlined Objects"); } private void DestroyBillboard() { Destroy(_billboard); } private bool CanInitialize() { if (leftHand == null || rightHand == null) { Debug.Log("Left or right hand is null"); return false; } if (turnProvider.rightHandSnapTurnAction.action == null) { Debug.Log("Right hand snap turn action is null"); return false; } if (moveProvider.leftHandMoveAction.action == null) { Debug.Log("Left hand move action is null"); return false; } if (teleportProvider == null) { Debug.Log("Teleport provider is null"); return false; } return true; } private void TryInitialize() { if (!CanInitialize()) return; if (null == initializationInfoCoroutine) { initializationInfoCoroutine = StartCoroutine(InitializationInfoCoroutine()); } _camera = Camera.main; //Debug.Log("Initializing tutorial"); _leftHintController = leftHand.GetComponentInChildren(); _rightHintController = rightHand.GetComponentInChildren(); initializationInfoStatus[TutorialInfoKey.LeftHintController] = null != _leftHintController; initializationInfoStatus[TutorialInfoKey.RightHintController] = null != _rightHintController; _leftSmartHandPresence = leftHand.GetComponentInChildren(); _rightSmartHandPresence = rightHand.GetComponentInChildren(); initializationInfoStatus[TutorialInfoKey.LeftSmartHandPresence] = null != _leftSmartHandPresence; initializationInfoStatus[TutorialInfoKey.RightSmartHandPresence] = null != _rightSmartHandPresence; if (_leftHintController == null || _rightHintController == null || _leftSmartHandPresence == null || _rightSmartHandPresence == null) { //Debug.Log("Hint controller or smart hand presence is null"); return; } _leftHintController.xrBaseController = leftHand; _rightHintController.xrBaseController = rightHand; // Setup action listeners turnProvider.rightHandSnapTurnAction.action.performed += OnTurnPerformed; moveProvider.leftHandMoveAction.action.performed += OnMovePerformed; teleportProvider.endLocomotion += OnTeleportPerformed; leftHand.GetComponent().selectEntered.AddListener(OnGripPerformed); rightHand.GetComponent().selectEntered.AddListener(OnGripPerformed); UpdateState(_state.Next()); Debug.Log("Tutorial initialized"); StopCoroutine(initializationInfoCoroutine); } private void OnGripPerformed(SelectEnterEventArgs arg0) { var grab = arg0.interactableObject as XRGrabInteractable; // Notify any listeners OnGrab?.Invoke(grab); if (_state != TutorialState.Grip) return; Debug.Log("Grip performed"); UpdateState(_state.Next()); } private void OnTeleportPerformed(LocomotionSystem obj) { if (_state != TutorialState.Teleport) return; Debug.Log("Teleport performed"); UpdateState(_state.Next()); } private void OnMovePerformed(InputAction.CallbackContext obj) { if (_state != TutorialState.Move) return; Debug.Log("Move performed"); UpdateState(_state.Next()); } private void OnTurnPerformed(InputAction.CallbackContext obj) { if (_state != TutorialState.Turn) return; Debug.Log("Turn performed"); UpdateState(_state.Next()); } private void ShowTeleportHint() { _rightHintController.ShowTeleportHint(); _leftHintController.ShowTeleportHint(); } private void ShowTurnHint() { _rightHintController.ShowTurnHint(); _leftHintController.ShowTurnHint(); } private void ShowLocomotionHint() { _rightHintController.ShowSmoothLocomotionHint(); _leftHintController.ShowSmoothLocomotionHint(); } private void ShowGripHint() { _rightHintController.ShowGripHint(); _leftHintController.ShowGripHint(); } private void SetControllerVisibility(bool visible) { _leftSmartHandPresence.showController = visible; _rightSmartHandPresence.showController = visible; } private void SetHandsVisibility(bool visible) { _leftSmartHandPresence.showHand = visible; _rightSmartHandPresence.showHand = visible; } private void OnTriggerEnter(Collider other) { // If has component add to list var grabInteractable = other.GetComponent(); if (grabInteractable != null) { _grabInteractables.Add(grabInteractable); } } private void OnTriggerExit(Collider other) { // If has component remove from list var grabInteractable = other.GetComponent(); if (grabInteractable != null) { _grabInteractables.Remove(grabInteractable); } } IEnumerator InitializationInfoCoroutine() { string CombineMessage(TutorialInfoKey key, object value, string prefix) { bool isAvailable = initializationInfoStatus.GetValueOrDefault(key); return prefix + ": " + (isAvailable ? value.ToString() : "NULL") + "\r\n"; } bool isInitialized = initializationInfoStatus.GetValueOrDefault(TutorialInfoKey.Initialized); while (!isInitialized) { isInitialized = initializationInfoStatus.GetValueOrDefault(TutorialInfoKey.Initialized); string infoMessage = "Tutorial not yet initialized!" + "\r\n"; infoMessage += "(click me for more info)" + "\r\n"; infoMessage += "Hint controllers" + "\r\n"; infoMessage += CombineMessage(TutorialInfoKey.LeftHintController, _leftHintController, "Left"); infoMessage += CombineMessage(TutorialInfoKey.RightHintController, _rightHintController, "Right"); infoMessage += "Smart hand presence" + "\r\n"; infoMessage += CombineMessage(TutorialInfoKey.LeftSmartHandPresence, _leftSmartHandPresence, "Left"); infoMessage += CombineMessage(TutorialInfoKey.RightSmartHandPresence, _rightSmartHandPresence, "Right"); Debug.Log(infoMessage); yield return new WaitForSeconds(7); } } } }