Skip to content

Commit 77c9125

Browse files
authored
Fixes MRTK3's Tap to Place solver to work with any hand and interactor (#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: #11527
1 parent 69e135b commit 77c9125

File tree

5 files changed

+312
-68
lines changed

5 files changed

+312
-68
lines changed

UnityProjects/MRTKDevTemplate/Assets/Scenes/TapToPlaceExample.unity

Lines changed: 45 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -257,8 +257,8 @@ MonoBehaviour:
257257
m_Script: {fileID: 11500000, guid: 3986155c7a728454f8bbbabd2e274601, type: 3}
258258
m_Name:
259259
m_EditorClassIdentifier:
260-
leftInteractor: {fileID: 1127029052}
261-
rightInteractor: {fileID: 0}
260+
leftInteractor: {fileID: 1127029054}
261+
rightInteractor: {fileID: 1127029052}
262262
trackedTargetType: 1
263263
trackedHandedness: 3
264264
trackedHandJoint: 2
@@ -572,7 +572,20 @@ MonoBehaviour:
572572
m_Calls: []
573573
<OnClicked>k__BackingField:
574574
m_PersistentCalls:
575-
m_Calls: []
575+
m_Calls:
576+
- m_Target: {fileID: 396224581}
577+
m_TargetAssemblyTypeName: Microsoft.MixedReality.Toolkit.SpatialManipulation.TapToPlace,
578+
Microsoft.MixedReality.Toolkit.SpatialManipulation
579+
m_MethodName: StartPlacement
580+
m_Mode: 1
581+
m_Arguments:
582+
m_ObjectArgument: {fileID: 0}
583+
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
584+
m_IntArgument: 0
585+
m_FloatArgument: 0
586+
m_StringArgument:
587+
m_BoolArgument: 0
588+
m_CallState: 2
576589
<OnEnabled>k__BackingField:
577590
m_PersistentCalls:
578591
m_Calls: []
@@ -1210,6 +1223,10 @@ PrefabInstance:
12101223
propertyPath: m_RootOrder
12111224
value: 3
12121225
objectReference: {fileID: 0}
1226+
- target: {fileID: 8479077998186684813, guid: 4d7e2f87fefe0ba468719b15288b46e7, type: 3}
1227+
propertyPath: m_IsActive
1228+
value: 1
1229+
objectReference: {fileID: 0}
12131230
m_RemovedComponents: []
12141231
m_SourcePrefab: {fileID: 100100000, guid: 4d7e2f87fefe0ba468719b15288b46e7, type: 3}
12151232
--- !u!4 &1127029051 stripped
@@ -1239,6 +1256,17 @@ MonoBehaviour:
12391256
m_Script: {fileID: 11500000, guid: 83e4e6cca11330d4088d729ab4fc9d9f, type: 3}
12401257
m_Name:
12411258
m_EditorClassIdentifier:
1259+
--- !u!114 &1127029054 stripped
1260+
MonoBehaviour:
1261+
m_CorrespondingSourceObject: {fileID: 7115113329451106245, guid: 4d7e2f87fefe0ba468719b15288b46e7, type: 3}
1262+
m_PrefabInstance: {fileID: 1127029050}
1263+
m_PrefabAsset: {fileID: 0}
1264+
m_GameObject: {fileID: 0}
1265+
m_Enabled: 1
1266+
m_EditorHideFlags: 0
1267+
m_Script: {fileID: 11500000, guid: e85416945309f8244a5715a2ec5c254f, type: 3}
1268+
m_Name:
1269+
m_EditorClassIdentifier:
12421270
--- !u!1 &1167916486
12431271
GameObject:
12441272
m_ObjectHideFlags: 0
@@ -2650,7 +2678,20 @@ MonoBehaviour:
26502678
m_Calls: []
26512679
<OnClicked>k__BackingField:
26522680
m_PersistentCalls:
2653-
m_Calls: []
2681+
m_Calls:
2682+
- m_Target: {fileID: 4125495309857526231}
2683+
m_TargetAssemblyTypeName: Microsoft.MixedReality.Toolkit.SpatialManipulation.TapToPlace,
2684+
Microsoft.MixedReality.Toolkit.SpatialManipulation
2685+
m_MethodName: StartPlacement
2686+
m_Mode: 1
2687+
m_Arguments:
2688+
m_ObjectArgument: {fileID: 0}
2689+
m_ObjectArgumentAssemblyTypeName: UnityEngine.Object, UnityEngine
2690+
m_IntArgument: 0
2691+
m_FloatArgument: 0
2692+
m_StringArgument:
2693+
m_BoolArgument: 0
2694+
m_CallState: 2
26542695
<OnEnabled>k__BackingField:
26552696
m_PersistentCalls:
26562697
m_Calls: []

com.microsoft.mrtk.spatialmanipulation/Editor/Solvers/TapToPlaceEditor.cs

Lines changed: 1 addition & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -16,8 +16,6 @@ public class TapToPlaceEditor : UnityEditor.Editor
1616
{
1717
private TapToPlace instance;
1818

19-
private GUIContent placementActionContent = new GUIContent("Placement action");
20-
2119
// Tap to Place properties
2220
private SerializedProperty placementAction;
2321
private SerializedProperty autoStart;
@@ -81,7 +79,6 @@ private void RenderCustomInspector()
8179
{
8280
serializedObject.Update();
8381

84-
EditorGUILayout.PropertyField(placementAction, placementActionContent);
8582
EditorGUILayout.PropertyField(autoStart);
8683
EditorGUILayout.PropertyField(defaultPlacementDistance);
8784
EditorGUILayout.PropertyField(maxRaycastDistance);
@@ -128,4 +125,4 @@ private void RenderAdvancedProperties()
128125
}
129126
}
130127
}
131-
}
128+
}

com.microsoft.mrtk.spatialmanipulation/Solvers/TapToPlace.cs

Lines changed: 73 additions & 60 deletions
Original file line numberDiff line numberDiff line change
@@ -1,33 +1,22 @@
11
// Copyright (c) Microsoft Corporation.
22
// Licensed under the MIT License.
33

4+
using System.Collections.Generic;
45
using Unity.Profiling;
56
using UnityEngine;
67
using UnityEngine.Events;
78
using UnityEngine.InputSystem;
9+
using UnityEngine.XR.Interaction.Toolkit;
810
using UnityPhysics = UnityEngine.Physics;
911

1012
namespace Microsoft.MixedReality.Toolkit.SpatialManipulation
1113
{
1214
/// <summary>
1315
/// Tap to place is a far interaction component used to place objects on a surface.
1416
/// </summary>
15-
[RequireComponent(typeof(StatefulInteractable))]
1617
[AddComponentMenu("MRTK/Spatial Manipulation/Solvers/Tap To Place")]
1718
public class TapToPlace : Solver
1819
{
19-
[SerializeField]
20-
[Tooltip("The input action which is to control placement.")]
21-
private InputActionReference placementActionReference = null;
22-
23-
/// <summary>
24-
/// The input action which is to control placement.
25-
/// </summary>
26-
public InputActionReference PlacementActionReference
27-
{
28-
get => placementActionReference;
29-
set => placementActionReference = value;
30-
}
3120

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

240-
// The interactable used to pick up the obect.
241-
private StatefulInteractable interactable;
229+
// Used to obtain list of known interactors
230+
private XRInteractionManager interactionManager;
231+
232+
// Used to cache a known set of interactor
233+
private List<IXRInteractor> interactorsCache;
242234

243235
#region MonoBehaviour Implementation
244236

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

258250
startCalled = true;
259251

260-
interactable = gameObject.GetComponent<StatefulInteractable>();
261-
RegisterPickupAction();
262-
263252
if (AutoStart || placementRequested)
264253
{
265254
StartPlacement();
@@ -272,8 +261,7 @@ protected override void Start()
272261

273262
protected override void OnDisable()
274263
{
275-
UnregisterPlacementAction();
276-
UnregisterPickupAction();
264+
StopPlacement();
277265
base.OnDisable();
278266
}
279267

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

285273
/// <summary>
286274
/// Start the placement of a game object without the need of the OnPointerClicked event. The game object will begin to follow the
287-
/// TrackedTargetType (Head by default) at a default distance. StopPlacement() must be called after StartPlacement() to stop the
275+
/// TrackedTargetType (Head by default) at a default distance. StopPlacementViaPerformedAction() must be called after StartPlacement() to stop the
288276
/// game object from following the TrackedTargetType. The game object layer is changed to IgnoreRaycast temporarily and then
289-
/// restored to its original layer in StopPlacement().
277+
/// restored to its original layer in StopPlacementViaPerformedAction().
290278
/// </summary>
291279
public void StartPlacement()
292280
{
293-
// Checking the amount of time passed between when StartPlacement or StopPlacement is called twice in
281+
// Checking the amount of time passed between when StartPlacement or StopPlacementViaPerformedAction is called twice in
294282
// succession. If these methods are called twice very rapidly, the object will be
295283
// selected and then immediately unselected. If two calls occur within the
296284
// double click timeout, then return to prevent an immediate object state switch.
@@ -299,6 +287,7 @@ public void StartPlacement()
299287
{
300288
return;
301289
}
290+
302291
// Get the time of this click action
303292
LastTimeClicked = Time.time;
304293

@@ -333,14 +322,30 @@ public void StartPlacement()
333322
}
334323

335324
private static readonly ProfilerMarker StopPlacementPerfMarker =
336-
new ProfilerMarker("[MRTK] TapToPlace.StopPlacement");
325+
new ProfilerMarker("[MRTK] TapToPlace.StopPlacementViaPerformedAction");
337326

338327
/// <summary>
339-
/// Stop the placement of a game object without the need of the OnPointerClicked event.
328+
/// Stop the placement of a game object via an action's performance.
340329
/// </summary>
341-
public void StopPlacement(InputAction.CallbackContext context)
330+
private void StopPlacementViaPerformedAction(InputAction.CallbackContext context)
342331
{
343-
// Checking the amount of time passed between when StartPlacement or StopPlacement is called twice in
332+
StopPlacement();
333+
}
334+
335+
/// <summary>
336+
/// Stop the placement of a game object via an interactor's select event.
337+
/// </summary>
338+
private void StopPlacementViaSelect(SelectEnterEventArgs args)
339+
{
340+
StopPlacement();
341+
}
342+
343+
/// <summary>
344+
/// Stop the placement of a game object.
345+
/// </summary>
346+
public void StopPlacement()
347+
{
348+
// Checking the amount of time passed between when StartPlacement or StopPlacementViaPerformedAction is called twice in
344349
// succession. If these methods are called twice very rapidly, the object will be
345350
// selected and then immediately unselected. If two calls occur within the
346351
// double click timeout, then return to prevent an immediate object state switch.
@@ -353,7 +358,7 @@ public void StopPlacement(InputAction.CallbackContext context)
353358

354359
using (StopPlacementPerfMarker.Auto())
355360
{
356-
// Added for code configurability to avoid multiple calls to StopPlacement in a row
361+
// Added for code configurability to avoid multiple calls to StopPlacementViaPerformedAction in a row
357362
if (IsBeingPlaced)
358363
{
359364
// Change the game object layer back to the game object's layer on start
@@ -467,53 +472,61 @@ protected virtual void SetRotation()
467472
/// </summary>
468473
private void RegisterPlacementAction()
469474
{
470-
InputAction placementAction = GetInputActionFromReference(placementActionReference);
471-
if (placementAction == null)
475+
// Refresh the registeration if they already exist
476+
UnregisterPlacementAction();
477+
478+
if (interactionManager == null)
472479
{
473-
Debug.Log("Failed to register the placement action, the action reference was null or contained no action.");
474-
return;
480+
interactionManager = FindObjectOfType<XRInteractionManager>();
481+
if (interactionManager == null)
482+
{
483+
Debug.LogError("No interaction manager found in scene. Please add an interaction manager to the scene.");
484+
}
475485
}
476-
placementAction.performed += StopPlacement;
477-
}
478486

479-
/// <summary>
480-
/// Registers the event which performs pickup.
481-
/// </summary>
482-
private void RegisterPickupAction()
483-
{
484-
if (interactable == null)
487+
if (interactorsCache == null)
485488
{
486-
Debug.Log("Failed to register the pick up event. There is no StatefulInteractable set.");
487-
return;
489+
interactorsCache = new List<IXRInteractor>();
488490
}
489-
interactable.OnClicked.AddListener(StartPlacement);
490-
}
491491

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

506509
/// <summary>
507-
/// Unregisters the event which performs pickup.
510+
/// Unregisters the input action which performs placement.
508511
/// </summary>
509-
private void UnregisterPickupAction()
512+
private void UnregisterPlacementAction()
510513
{
511-
if (interactable == null)
514+
if (interactorsCache != null)
512515
{
513-
Debug.Log("Failed to unregister the pick up event. There is no StatefulInteractable set.");
514-
return;
516+
foreach (IXRInteractor interactor in interactorsCache)
517+
{
518+
if (interactor is XRBaseControllerInteractor controllerInteractor &&
519+
controllerInteractor.xrController is ActionBasedController actionController)
520+
{
521+
actionController.selectAction.action.performed -= StopPlacementViaPerformedAction;
522+
}
523+
else if (interactor is IXRSelectInteractor selectInteractor)
524+
{
525+
selectInteractor.selectEntered.RemoveListener(StopPlacementViaSelect);
526+
}
527+
}
528+
interactorsCache.Clear();
515529
}
516-
interactable.OnClicked.RemoveListener(StartPlacement);
517530
}
518531

519532
/// <summary>

0 commit comments

Comments
 (0)