Skip to content

Commit

Permalink
Fixes MRTK3's Tap to Place solver to work with any hand and interac…
Browse files Browse the repository at this point in the history
…tor (microsoft#11545)

## Overview
Fixes MRTK3's `Tap to Place` solver to work with any hand and any interactor (even speech). This change, on `StartPlacement`, queries the `XRInteractionManager` for all registered interactors, and then registers for the interactors' select events.  This is an alternative to requiring the developer to specify particular actions. 

Note, this change also removes the `StatefulInteractable` requirement.  The consumer of `TapToPlace` is now required to determine when `StartPlacement` is invoked. This change is meant to provide extensibility for future scenarios which may not require or use `StatefulInteractables`.

This also adds Unit Tests to validate `TapToPlace`.

## Changes
- Fixes: microsoft#11527
  • Loading branch information
AMollis committed May 8, 2023
1 parent 10a858a commit 57c4de1
Show file tree
Hide file tree
Showing 5 changed files with 312 additions and 68 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -257,8 +257,8 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 3986155c7a728454f8bbbabd2e274601, type: 3}
m_Name:
m_EditorClassIdentifier:
leftInteractor: {fileID: 1127029052}
rightInteractor: {fileID: 0}
leftInteractor: {fileID: 1127029054}
rightInteractor: {fileID: 1127029052}
trackedTargetType: 1
trackedHandedness: 3
trackedHandJoint: 2
Expand Down Expand Up @@ -572,7 +572,20 @@ MonoBehaviour:
m_Calls: []
<OnClicked>k__BackingField:
m_PersistentCalls:
m_Calls: []
m_Calls:
- m_Target: {fileID: 396224581}
m_TargetAssemblyTypeName: Microsoft.MixedReality.Toolkit.SpatialManipulation.TapToPlace,
Microsoft.MixedReality.Toolkit.SpatialManipulation
m_MethodName: StartPlacement
m_Mode: 1
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
<OnEnabled>k__BackingField:
m_PersistentCalls:
m_Calls: []
Expand Down Expand Up @@ -1210,6 +1223,10 @@ PrefabInstance:
propertyPath: m_RootOrder
value: 3
objectReference: {fileID: 0}
- target: {fileID: 8479077998186684813, guid: 4d7e2f87fefe0ba468719b15288b46e7, type: 3}
propertyPath: m_IsActive
value: 1
objectReference: {fileID: 0}
m_RemovedComponents: []
m_SourcePrefab: {fileID: 100100000, guid: 4d7e2f87fefe0ba468719b15288b46e7, type: 3}
--- !u!4 &1127029051 stripped
Expand Down Expand Up @@ -1239,6 +1256,17 @@ MonoBehaviour:
m_Script: {fileID: 11500000, guid: 83e4e6cca11330d4088d729ab4fc9d9f, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!114 &1127029054 stripped
MonoBehaviour:
m_CorrespondingSourceObject: {fileID: 7115113329451106245, guid: 4d7e2f87fefe0ba468719b15288b46e7, type: 3}
m_PrefabInstance: {fileID: 1127029050}
m_PrefabAsset: {fileID: 0}
m_GameObject: {fileID: 0}
m_Enabled: 1
m_EditorHideFlags: 0
m_Script: {fileID: 11500000, guid: e85416945309f8244a5715a2ec5c254f, type: 3}
m_Name:
m_EditorClassIdentifier:
--- !u!1 &1167916486
GameObject:
m_ObjectHideFlags: 0
Expand Down Expand Up @@ -2650,7 +2678,20 @@ MonoBehaviour:
m_Calls: []
<OnClicked>k__BackingField:
m_PersistentCalls:
m_Calls: []
m_Calls:
- m_Target: {fileID: 4125495309857526231}
m_TargetAssemblyTypeName: Microsoft.MixedReality.Toolkit.SpatialManipulation.TapToPlace,
Microsoft.MixedReality.Toolkit.SpatialManipulation
m_MethodName: StartPlacement
m_Mode: 1
m_Arguments:
m_ObjectArgument: {fileID: 0}
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
m_IntArgument: 0
m_FloatArgument: 0
m_StringArgument:
m_BoolArgument: 0
m_CallState: 2
<OnEnabled>k__BackingField:
m_PersistentCalls:
m_Calls: []
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -16,8 +16,6 @@ public class TapToPlaceEditor : UnityEditor.Editor
{
private TapToPlace instance;

private GUIContent placementActionContent = new GUIContent("Placement action");

// Tap to Place properties
private SerializedProperty placementAction;
private SerializedProperty autoStart;
Expand Down Expand Up @@ -81,7 +79,6 @@ private void RenderCustomInspector()
{
serializedObject.Update();

EditorGUILayout.PropertyField(placementAction, placementActionContent);
EditorGUILayout.PropertyField(autoStart);
EditorGUILayout.PropertyField(defaultPlacementDistance);
EditorGUILayout.PropertyField(maxRaycastDistance);
Expand Down Expand Up @@ -128,4 +125,4 @@ private void RenderAdvancedProperties()
}
}
}
}
}
133 changes: 73 additions & 60 deletions com.microsoft.mrtk.spatialmanipulation/Solvers/TapToPlace.cs
Original file line number Diff line number Diff line change
@@ -1,33 +1,22 @@
// Copyright (c) Microsoft Corporation.
// Licensed under the MIT License.

using System.Collections.Generic;
using Unity.Profiling;
using UnityEngine;
using UnityEngine.Events;
using UnityEngine.InputSystem;
using UnityEngine.XR.Interaction.Toolkit;
using UnityPhysics = UnityEngine.Physics;

namespace Microsoft.MixedReality.Toolkit.SpatialManipulation
{
/// <summary>
/// Tap to place is a far interaction component used to place objects on a surface.
/// </summary>
[RequireComponent(typeof(StatefulInteractable))]
[AddComponentMenu("MRTK/Spatial Manipulation/Solvers/Tap To Place")]
public class TapToPlace : Solver
{
[SerializeField]
[Tooltip("The input action which is to control placement.")]
private InputActionReference placementActionReference = null;

/// <summary>
/// The input action which is to control placement.
/// </summary>
public InputActionReference PlacementActionReference
{
get => placementActionReference;
set => placementActionReference = value;
}

// todo: needed? [Space(10)]
[SerializeField]
Expand Down Expand Up @@ -237,8 +226,11 @@ public UnityEvent OnPlacingStopped
// Used to mark whether StartPlacement() is called before Start() is called.
private bool placementRequested;

// The interactable used to pick up the obect.
private StatefulInteractable interactable;
// Used to obtain list of known interactors
private XRInteractionManager interactionManager;

// Used to cache a known set of interactor
private List<IXRInteractor> interactorsCache;

#region MonoBehaviour Implementation

Expand All @@ -257,9 +249,6 @@ protected override void Start()

startCalled = true;

interactable = gameObject.GetComponent<StatefulInteractable>();
RegisterPickupAction();

if (AutoStart || placementRequested)
{
StartPlacement();
Expand All @@ -272,8 +261,7 @@ protected override void Start()

protected override void OnDisable()
{
UnregisterPlacementAction();
UnregisterPickupAction();
StopPlacement();
base.OnDisable();
}

Expand All @@ -284,13 +272,13 @@ protected override void OnDisable()

/// <summary>
/// Start the placement of a game object without the need of the OnPointerClicked event. The game object will begin to follow the
/// TrackedTargetType (Head by default) at a default distance. StopPlacement() must be called after StartPlacement() to stop the
/// TrackedTargetType (Head by default) at a default distance. StopPlacementViaPerformedAction() must be called after StartPlacement() to stop the
/// game object from following the TrackedTargetType. The game object layer is changed to IgnoreRaycast temporarily and then
/// restored to its original layer in StopPlacement().
/// restored to its original layer in StopPlacementViaPerformedAction().
/// </summary>
public void StartPlacement()
{
// Checking the amount of time passed between when StartPlacement or StopPlacement is called twice in
// Checking the amount of time passed between when StartPlacement or StopPlacementViaPerformedAction is called twice in
// succession. If these methods are called twice very rapidly, the object will be
// selected and then immediately unselected. If two calls occur within the
// double click timeout, then return to prevent an immediate object state switch.
Expand All @@ -299,6 +287,7 @@ public void StartPlacement()
{
return;
}

// Get the time of this click action
LastTimeClicked = Time.time;

Expand Down Expand Up @@ -333,14 +322,30 @@ public void StartPlacement()
}

private static readonly ProfilerMarker StopPlacementPerfMarker =
new ProfilerMarker("[MRTK] TapToPlace.StopPlacement");
new ProfilerMarker("[MRTK] TapToPlace.StopPlacementViaPerformedAction");

/// <summary>
/// Stop the placement of a game object without the need of the OnPointerClicked event.
/// Stop the placement of a game object via an action's performance.
/// </summary>
public void StopPlacement(InputAction.CallbackContext context)
private void StopPlacementViaPerformedAction(InputAction.CallbackContext context)
{
// Checking the amount of time passed between when StartPlacement or StopPlacement is called twice in
StopPlacement();
}

/// <summary>
/// Stop the placement of a game object via an interactor's select event.
/// </summary>
private void StopPlacementViaSelect(SelectEnterEventArgs args)
{
StopPlacement();
}

/// <summary>
/// Stop the placement of a game object.
/// </summary>
public void StopPlacement()
{
// Checking the amount of time passed between when StartPlacement or StopPlacementViaPerformedAction is called twice in
// succession. If these methods are called twice very rapidly, the object will be
// selected and then immediately unselected. If two calls occur within the
// double click timeout, then return to prevent an immediate object state switch.
Expand All @@ -353,7 +358,7 @@ public void StopPlacement(InputAction.CallbackContext context)

using (StopPlacementPerfMarker.Auto())
{
// Added for code configurability to avoid multiple calls to StopPlacement in a row
// Added for code configurability to avoid multiple calls to StopPlacementViaPerformedAction in a row
if (IsBeingPlaced)
{
// Change the game object layer back to the game object's layer on start
Expand Down Expand Up @@ -467,53 +472,61 @@ protected virtual void SetRotation()
/// </summary>
private void RegisterPlacementAction()
{
InputAction placementAction = GetInputActionFromReference(placementActionReference);
if (placementAction == null)
// Refresh the registeration if they already exist
UnregisterPlacementAction();

if (interactionManager == null)
{
Debug.Log("Failed to register the placement action, the action reference was null or contained no action.");
return;
interactionManager = FindObjectOfType<XRInteractionManager>();
if (interactionManager == null)
{
Debug.LogError("No interaction manager found in scene. Please add an interaction manager to the scene.");
}
}
placementAction.performed += StopPlacement;
}

/// <summary>
/// Registers the event which performs pickup.
/// </summary>
private void RegisterPickupAction()
{
if (interactable == null)
if (interactorsCache == null)
{
Debug.Log("Failed to register the pick up event. There is no StatefulInteractable set.");
return;
interactorsCache = new List<IXRInteractor>();
}
interactable.OnClicked.AddListener(StartPlacement);
}

/// <summary>
/// Unregisters the input action which performs placement.
/// </summary>
private void UnregisterPlacementAction()
{
InputAction placementAction = GetInputActionFromReference(placementActionReference);
if (placementAction == null)
// Try registering for the controller's "action" so object selection isn't required for placement.
// If no controller, then fallback to using object selections for placement.
interactionManager.GetRegisteredInteractors(interactorsCache);
foreach (IXRInteractor interactor in interactorsCache)
{
Debug.Log("Failed to unregister the placement action, the action reference was null or contained no action.");
return;
if (interactor is XRBaseControllerInteractor controllerInteractor &&
controllerInteractor.xrController is ActionBasedController actionController)
{
actionController.selectAction.action.performed += StopPlacementViaPerformedAction;
}
else if (interactor is IXRSelectInteractor selectInteractor)
{
selectInteractor.selectEntered.AddListener(StopPlacementViaSelect);
}
}
placementAction.performed -= StopPlacement;
}

/// <summary>
/// Unregisters the event which performs pickup.
/// Unregisters the input action which performs placement.
/// </summary>
private void UnregisterPickupAction()
private void UnregisterPlacementAction()
{
if (interactable == null)
if (interactorsCache != null)
{
Debug.Log("Failed to unregister the pick up event. There is no StatefulInteractable set.");
return;
foreach (IXRInteractor interactor in interactorsCache)
{
if (interactor is XRBaseControllerInteractor controllerInteractor &&
controllerInteractor.xrController is ActionBasedController actionController)
{
actionController.selectAction.action.performed -= StopPlacementViaPerformedAction;
}
else if (interactor is IXRSelectInteractor selectInteractor)
{
selectInteractor.selectEntered.RemoveListener(StopPlacementViaSelect);
}
}
interactorsCache.Clear();
}
interactable.OnClicked.RemoveListener(StartPlacement);
}

/// <summary>
Expand Down
Loading

0 comments on commit 57c4de1

Please sign in to comment.