mixedreality/com.microsoft.mixedreality..../SDK/Features/UX/Scripts/Pointers/BaseControllerPointer.cs

685 lines
24 KiB
C#

// 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
{
/// <summary>
/// Base Pointer class for pointers that exist in the scene as GameObjects.
/// </summary>
[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;
/// <summary>
/// True if grab is pressed right now
/// </summary>
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;
/// <summary>
/// True if select is pressed right now
/// </summary>
protected bool IsSelectPressed = false;
/// <summary>
/// True if select has been pressed once since this component was enabled
/// </summary>
protected bool HasSelectPressedOnce = false;
protected bool IsHoldPressed = false;
private bool isCursorInstantiatedFromPrefab = false;
private static readonly ProfilerMarker SetCursorPerfMarker = new ProfilerMarker("[MRTK] BaseControllerPointer.SetCursor");
/// <summary>
/// Set a new cursor for this <see cref="Microsoft.MixedReality.Toolkit.Input.IMixedRealityPointer"/>
/// </summary>
/// <remarks>This <see href="https://docs.unity3d.com/ScriptReference/GameObject.html">GameObject</see> must have a <see cref="Microsoft.MixedReality.Toolkit.Input.IMixedRealityCursor"/> attached to it.</remarks>
/// <param name="newCursor">The new cursor</param>
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<IMixedRealityCursor>();
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<Renderer>();
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
/// <inheritdoc />
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;
/// <inheritdoc />
public uint PointerId
{
get
{
if (pointerId == 0)
{
pointerId = CoreServices.InputSystem.FocusProvider.GenerateNewPointerId();
}
return pointerId;
}
}
private string basePointerName = string.Empty;
private string pointerName = string.Empty;
/// <inheritdoc />
public string PointerName
{
get => pointerName;
set
{
pointerName = value;
if (this.IsNotNull())
{
gameObject.name = value;
}
}
}
/// <inheritdoc />
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"); }
}
/// <inheritdoc />
public IMixedRealityCursor BaseCursor { get; set; }
/// <inheritdoc />
public ICursorModifier CursorModifier { get; set; }
/// <inheritdoc />
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;
}
}
/// <inheritdoc />
public virtual bool IsActive { get; set; }
/// <inheritdoc />
public bool IsFocusLocked { get; set; }
/// <summary>
/// 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.
/// </summary>
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;
/// <summary>
/// Maximum distance at which all pointers can collide with a <see href="https://docs.unity3d.com/ScriptReference/GameObject.html">GameObject</see>, unless it has an override extent.
/// </summary>
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;
/// <summary>
/// The length of the pointer when nothing is hit.
/// </summary>
public float DefaultPointerExtent
{
get => Mathf.Min(defaultPointerExtent, PointerExtent);
set => defaultPointerExtent = value;
}
/// <inheritdoc />
public RayStep[] Rays { get; protected set; } = { new RayStep(Vector3.zero, Vector3.forward) };
/// <inheritdoc />
public virtual LayerMask[] PrioritizedLayerMasksOverride { get; set; } = null;
/// <inheritdoc />
public IMixedRealityFocusHandler FocusTarget { get; set; }
/// <inheritdoc />
public IPointerResult Result { get; set; }
/// <summary>
/// Ray stabilizer used when calculating position of pointer end point.
/// </summary>
public IBaseRayStabilizer RayStabilizer { get; set; }
/// <inheritdoc />
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;
/// <inheritdoc />
public float SphereCastRadius
{
get => sphereCastRadius;
set => sphereCastRadius = value;
}
/// <inheritdoc />
public virtual Vector3 Position => raycastOrigin != null ? raycastOrigin.position : transform.position;
/// <inheritdoc />
public virtual Quaternion Rotation => raycastOrigin != null ? raycastOrigin.rotation : transform.rotation;
/// <inheritdoc />
public virtual void OnPreSceneQuery() { }
/// <inheritdoc />
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;
}
/// <inheritdoc />
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");
/// <inheritdoc />
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);
}
}
}
}
/// <inheritdoc />
public virtual void OnPreCurrentPointerTargetChange() { }
/// <inheritdoc />
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);
}
/// <inheritdoc />
bool IEqualityComparer.Equals(object left, object right)
{
return left != null && left.Equals(right);
}
/// <inheritdoc />
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);
}
/// <inheritdoc />
int IEqualityComparer.GetHashCode(object obj)
{
return obj.GetHashCode();
}
/// <inheritdoc />
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");
/// <inheritdoc />
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");
/// <inheritdoc />
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");
/// <inheritdoc />
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
}
}