// Copyright (c) Microsoft Corporation. // Licensed under the MIT License. using Microsoft.MixedReality.Toolkit.Physics; using System; using System.Collections; using Unity.Profiling; using UnityEngine; namespace Microsoft.MixedReality.Toolkit.Input { /// /// Base Pointer class for pointers that exist in the scene as GameObjects. /// [DisallowMultipleComponent] [HelpURL("https://docs.microsoft.com/windows/mixed-reality/mrtk-unity/features/input/pointers")] public abstract class BaseControllerPointer : ControllerPoseSynchronizer, IMixedRealityQueryablePointer { [SerializeField] private GameObject cursorPrefab = null; [SerializeField] private bool disableCursorOnStart = false; protected bool DisableCursorOnStart => disableCursorOnStart; [SerializeField] private bool setCursorVisibilityOnSourceDetected = false; private GameObject cursorInstance = null; [SerializeField] [Tooltip("Source transform for raycast origin - leave null to use default transform")] protected Transform raycastOrigin = null; [SerializeField] [Tooltip("The hold action that will enable the raise the input event for this pointer.")] private MixedRealityInputAction activeHoldAction = MixedRealityInputAction.None; [SerializeField] [Tooltip("The action that will enable the raise the input event for this pointer.")] protected MixedRealityInputAction pointerAction = MixedRealityInputAction.None; [SerializeField] [Tooltip("The action that will enable the raise the input grab event for this pointer.")] protected MixedRealityInputAction grabAction = MixedRealityInputAction.None; /// /// True if grab is pressed right now /// protected bool IsGrabPressed = false; [SerializeField] [Tooltip("Does the interaction require hold?")] private bool requiresHoldAction = false; [SerializeField] [Tooltip("Does the interaction require the action to occur at least once first?")] private bool requiresActionBeforeEnabling = true; /// /// True if select is pressed right now /// protected bool IsSelectPressed = false; /// /// True if select has been pressed once since this component was enabled /// protected bool HasSelectPressedOnce = false; protected bool IsHoldPressed = false; private bool isCursorInstantiatedFromPrefab = false; private static readonly ProfilerMarker SetCursorPerfMarker = new ProfilerMarker("[MRTK] BaseControllerPointer.SetCursor"); /// /// Set a new cursor for this /// /// This GameObject must have a attached to it. /// The new cursor public virtual void SetCursor(GameObject newCursor = null) { using (SetCursorPerfMarker.Auto()) { // Destroy the old cursor and replace it with the new one if a new cursor was provided if (cursorInstance != null && newCursor != null) { DestroyCursorInstance(); cursorInstance = newCursor; } if (cursorInstance == null && cursorPrefab != null) { // We spawn the cursor at the same level as this pointer by setting its parent to be the same as the pointer's // In the future, the pointer will not be responsible for instantiating the cursor, so we'll avoid making this assumption about the hierarchy cursorInstance = Instantiate(cursorPrefab, transform.parent); isCursorInstantiatedFromPrefab = true; } if (cursorInstance != null) { cursorInstance.name = $"{name}_Cursor"; BaseCursor oldC = BaseCursor as BaseCursor; if (oldC != null && enabled) { oldC.VisibleSourcesCount--; } BaseCursor = cursorInstance.GetComponent(); BaseCursor newC = BaseCursor as BaseCursor; if (newC != null && enabled) { newC.VisibleSourcesCount++; } if (BaseCursor != null) { BaseCursor.DefaultCursorDistance = DefaultPointerExtent; BaseCursor.Pointer = this; BaseCursor.SetVisibilityOnSourceDetected = setCursorVisibilityOnSourceDetected; if (disableCursorOnStart) { BaseCursor.SetVisibility(false); } } else { Debug.LogError($"No IMixedRealityCursor component found on {cursorInstance.name}"); } } } } private void DestroyCursorInstance() { if (cursorInstance != null) { // Destroy correctly depending on if in play mode or edit mode GameObjectExtensions.DestroyGameObject(cursorInstance); } } #region MonoBehaviour Implementation protected override void OnEnable() { base.OnEnable(); // Disable renderers so that they don't display before having been processed (which manifests as a flash at the origin). var renderers = GetComponentsInChildren(); if (renderers != null) { foreach (var renderer in renderers) { renderer.enabled = false; } } SetCursor(); } protected override async void Start() { base.Start(); await EnsureInputSystemValid(); // We've been destroyed during the await. if (this.IsNull()) { return; } // The pointer's input source was lost during the await. if (Controller == null) { GameObjectExtensions.DestroyGameObject(gameObject); return; } } protected override void OnDisable() { if (IsSelectPressed || IsGrabPressed) { CoreServices.InputSystem?.RaisePointerUp(this, pointerAction, Handedness); } base.OnDisable(); IsHoldPressed = false; IsSelectPressed = false; IsGrabPressed = false; HasSelectPressedOnce = false; BaseCursor?.SetVisibility(false); BaseCursor c = BaseCursor as BaseCursor; if (c != null) { c.VisibleSourcesCount--; } // Need to destroy instantiated cursor prefab if it was added by the controller itself in 'OnEnable' if (isCursorInstantiatedFromPrefab) { // Manually reset base cursor before destroying it BaseCursor?.Destroy(); DestroyCursorInstance(); isCursorInstantiatedFromPrefab = false; } } #endregion MonoBehaviour Implementation #region IMixedRealityPointer Implementation /// public override IMixedRealityController Controller { get => base.Controller; set { base.Controller = value; if (base.Controller != null && this.IsNotNull()) { // Ensures that the basePointerName is only initialized once if (basePointerName == string.Empty) { basePointerName = gameObject.name; } PointerName = $"{Handedness}_{basePointerName}"; SetCursor(); } } } private uint pointerId; /// public uint PointerId { get { if (pointerId == 0) { pointerId = CoreServices.InputSystem.FocusProvider.GenerateNewPointerId(); } return pointerId; } } private string basePointerName = string.Empty; private string pointerName = string.Empty; /// public string PointerName { get => pointerName; set { pointerName = value; if (this.IsNotNull()) { gameObject.name = value; } } } /// public IMixedRealityInputSource InputSourceParent { get { return base.Controller?.InputSource; } #if UNITY_2020_3_OR_NEWER [Obsolete("Setting the Input Source Parent directly is no longer supported")] #endif protected set { Debug.LogWarning("Setting the Input Source Parent directly is no longer supported"); } } /// public IMixedRealityCursor BaseCursor { get; set; } /// public ICursorModifier CursorModifier { get; set; } /// public virtual bool IsInteractionEnabled { get { if (IsFocusLocked) { return true; } if (!IsActive) { return false; } if (requiresHoldAction && IsHoldPressed) { return true; } if (IsSelectPressed || IsGrabPressed) { return true; } return HasSelectPressedOnce || !requiresActionBeforeEnabling; } } /// public virtual bool IsActive { get; set; } /// public bool IsFocusLocked { get; set; } /// /// Specifies whether the pointer's target position (cursor) is locked to the target object when focus is locked. /// Most pointers want the cursor to "stick" to the object when manipulating, so set this to true by default. /// public virtual bool IsTargetPositionLockedOnFocusLock { get; set; } = true; [SerializeField] private bool overrideGlobalPointerExtent = false; [SerializeField] [Tooltip("Maximum distance at which all pointers can collide with a GameObject, unless it has an override extent.")] private float pointerExtent = 10f; /// /// Maximum distance at which all pointers can collide with a GameObject, unless it has an override extent. /// public float PointerExtent { get { if (overrideGlobalPointerExtent) { if (CoreServices.InputSystem?.FocusProvider != null) { return CoreServices.InputSystem.FocusProvider.GlobalPointingExtent; } } return pointerExtent; } set { pointerExtent = value; overrideGlobalPointerExtent = false; } } [SerializeField] [Tooltip("The length of the pointer when nothing is hit")] private float defaultPointerExtent = 10f; /// /// The length of the pointer when nothing is hit. /// public float DefaultPointerExtent { get => Mathf.Min(defaultPointerExtent, PointerExtent); set => defaultPointerExtent = value; } /// public RayStep[] Rays { get; protected set; } = { new RayStep(Vector3.zero, Vector3.forward) }; /// public virtual LayerMask[] PrioritizedLayerMasksOverride { get; set; } = null; /// public IMixedRealityFocusHandler FocusTarget { get; set; } /// public IPointerResult Result { get; set; } /// /// Ray stabilizer used when calculating position of pointer end point. /// public IBaseRayStabilizer RayStabilizer { get; set; } /// public virtual SceneQueryType SceneQueryType { get; set; } = SceneQueryType.SimpleRaycast; [SerializeField] [Tooltip("How far controller needs to be from object before object can be grabbed / focused.")] private float sphereCastRadius = 0.1f; /// public float SphereCastRadius { get => sphereCastRadius; set => sphereCastRadius = value; } /// public virtual Vector3 Position => raycastOrigin != null ? raycastOrigin.position : transform.position; /// public virtual Quaternion Rotation => raycastOrigin != null ? raycastOrigin.rotation : transform.rotation; /// public virtual void OnPreSceneQuery() { } /// public virtual bool OnSceneQuery(LayerMask[] prioritizedLayerMasks, bool focusIndividualCompoundCollider, out MixedRealityRaycastHit hitInfo, out RayStep Ray, out int rayStepIndex) { float rayStartDistance = 0; var raycastProvider = CoreServices.InputSystem.RaycastProvider; switch (SceneQueryType) { case SceneQueryType.SimpleRaycast: for (int i = 0; i < Rays.Length; i++) { if (raycastProvider.Raycast(Rays[i], prioritizedLayerMasks, focusIndividualCompoundCollider, out hitInfo)) { // Ensure that our distance is the sum of the rays we've traversed so far hitInfo.distance += rayStartDistance; Ray = Rays[i]; rayStepIndex = i; return true; } rayStartDistance += Rays[i].Length; } break; case SceneQueryType.SphereCast: for (int i = 0; i < Rays.Length; i++) { if (raycastProvider.SphereCast(Rays[i], SphereCastRadius, prioritizedLayerMasks, focusIndividualCompoundCollider, out hitInfo)) { // Ensure that our distance is the sum of the rays we've traversed so far hitInfo.distance += rayStartDistance; Ray = Rays[i]; rayStepIndex = i; return true; } rayStartDistance += Rays[i].Length; } break; default: throw new System.Exception("The Base Controller Pointer does not handle non-raycast scene queries"); } hitInfo = new MixedRealityRaycastHit(); Ray = Rays[0]; rayStepIndex = 0; return false; } /// public virtual bool OnSceneQuery(LayerMask[] prioritizedLayerMasks, bool focusIndividualCompoundCollider, out GameObject hitObject, out Vector3 hitPoint, out float hitDistance) { MixedRealityRaycastHit hitInfo = new MixedRealityRaycastHit(); bool querySuccessful = OnSceneQuery(prioritizedLayerMasks, focusIndividualCompoundCollider, out hitInfo, out _, out _); hitObject = focusIndividualCompoundCollider ? hitInfo.collider.gameObject : hitInfo.transform.gameObject; hitPoint = hitInfo.point; hitDistance = hitInfo.distance; return querySuccessful; } private static readonly ProfilerMarker OnPostSceneQueryPerfMarker = new ProfilerMarker("[MRTK] BaseControllerPointer.OnPostSceneQuery"); /// public virtual void OnPostSceneQuery() { using (OnPostSceneQueryPerfMarker.Auto()) { if (grabAction != MixedRealityInputAction.None && InputSourceParent.SourceType == InputSourceType.Controller) { if (IsGrabPressed) { CoreServices.InputSystem.RaisePointerDragged(this, grabAction, Handedness); } } else { if (IsSelectPressed) { CoreServices.InputSystem.RaisePointerDragged(this, MixedRealityInputAction.None, Handedness); } } } } /// public virtual void OnPreCurrentPointerTargetChange() { } /// public virtual void Reset() { Controller = null; IsActive = false; IsFocusLocked = false; } #endregion IMixedRealityPointer Implementation #region IEquality Implementation private static bool Equals(IMixedRealityPointer left, IMixedRealityPointer right) { return left.Equals(right); } /// bool IEqualityComparer.Equals(object left, object right) { return left != null && left.Equals(right); } /// public override bool Equals(object obj) { if (ReferenceEquals(null, obj)) { return false; } if (ReferenceEquals(this, obj)) { return true; } if (obj.GetType() != GetType()) { return false; } return Equals((IMixedRealityPointer)obj); } private bool Equals(IMixedRealityPointer other) { return other != null && PointerId == other.PointerId && string.Equals(PointerName, other.PointerName); } /// int IEqualityComparer.GetHashCode(object obj) { return obj.GetHashCode(); } /// public override int GetHashCode() { unchecked { int hashCode = 0; hashCode = (hashCode * 397) ^ (int)PointerId; hashCode = (hashCode * 397) ^ (PointerName != null ? PointerName.GetHashCode() : 0); return hashCode; } } #endregion IEquality Implementation #region IMixedRealitySourcePoseHandler Implementation private static readonly ProfilerMarker OnSourceLostPerfMarker = new ProfilerMarker("[MRTK] BaseControllerPointer.OnSourceLost"); /// public override void OnSourceLost(SourceStateEventData eventData) { using (OnSourceLostPerfMarker.Auto()) { base.OnSourceLost(eventData); if (eventData.SourceId == InputSourceParent.SourceId) { if (requiresHoldAction) { IsHoldPressed = false; } if (IsSelectPressed) { CoreServices.InputSystem.RaisePointerUp(this, pointerAction, Handedness); } if (IsGrabPressed) { CoreServices.InputSystem.RaisePointerUp(this, grabAction, Handedness); } IsSelectPressed = false; IsGrabPressed = false; } } } #endregion IMixedRealitySourcePoseHandler Implementation #region IMixedRealityInputHandler Implementation private static readonly ProfilerMarker OnInputUpPerfMarker = new ProfilerMarker("[MRTK] BaseControllerPointer.OnInputUp"); /// public override void OnInputUp(InputEventData eventData) { if (!IsInteractionEnabled) { return; } using (OnInputUpPerfMarker.Auto()) { base.OnInputUp(eventData); if (eventData.SourceId == InputSourceParent.SourceId) { if (requiresHoldAction && eventData.MixedRealityInputAction == activeHoldAction) { IsHoldPressed = false; } if (grabAction != MixedRealityInputAction.None && eventData.InputSource.SourceType == InputSourceType.Controller && eventData.MixedRealityInputAction == grabAction) { IsGrabPressed = false; CoreServices.InputSystem.RaisePointerClicked(this, grabAction, 0, Handedness); CoreServices.InputSystem.RaisePointerUp(this, grabAction, Handedness); } if (eventData.MixedRealityInputAction == pointerAction) { IsSelectPressed = false; CoreServices.InputSystem.RaisePointerClicked(this, pointerAction, 0, Handedness); CoreServices.InputSystem.RaisePointerUp(this, pointerAction, Handedness); } } } } private static readonly ProfilerMarker OnInputDownPerfMarker = new ProfilerMarker("[MRTK] BaseControllerPointer.OnInputDown"); /// public override void OnInputDown(InputEventData eventData) { if (!IsInteractionEnabled) { return; } using (OnInputDownPerfMarker.Auto()) { base.OnInputDown(eventData); if (eventData.SourceId == InputSourceParent.SourceId) { if (requiresHoldAction && eventData.MixedRealityInputAction == activeHoldAction) { IsHoldPressed = true; } if (grabAction != MixedRealityInputAction.None && eventData.InputSource.SourceType == InputSourceType.Controller && eventData.MixedRealityInputAction == grabAction) { IsGrabPressed = true; if (IsInteractionEnabled) { CoreServices.InputSystem.RaisePointerDown(this, grabAction, Handedness); } } if (eventData.MixedRealityInputAction == pointerAction) { IsSelectPressed = true; HasSelectPressedOnce = true; if (IsInteractionEnabled) { CoreServices.InputSystem.RaisePointerDown(this, pointerAction, Handedness); } } } } } #endregion IMixedRealityInputHandler Implementation } }