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 (#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
  • Loading branch information
AMollis authored May 8, 2023
1 parent 69e135b commit 77c9125
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 77c9125

Please sign in to comment.