Skip to content

Commit 97c48a3

Browse files
authored
Fix lock visualizer when moving and rename component (#148)
* Use hint transform instead of pose input * Optimize locking code * Rename behaviour * Simplify script nesting * Log erros when component misconfigured * Fix hold onto smoothing transition when rig is moving * Update README and deps
1 parent f170f15 commit 97c48a3

8 files changed

+296
-214
lines changed

Editor/Inspectors/BaseInteractionBehaviourInspector.cs

+3
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
using UnityEditor.UIElements;
22
using UnityEngine.UIElements;
3+
using RealityToolkit.Editor.Utilities;
34

45
namespace RealityToolkit.Editor.Inspectors
56
{
@@ -32,6 +33,8 @@ public override VisualElement CreateInspectorGUI()
3233
inspector.Add(new PropertyField(serializedObject.FindProperty(targetHandednessBindingPath)));
3334
}
3435

36+
inspector.Add(UIElementsUtilities.VerticalSpace());
37+
3538
return inspector;
3639
}
3740
}

Editor/Inspectors/LockControllerVisualizerBehaviourInspector.cs Editor/Inspectors/HoldOntoBehaviourInspector.cs

+19-19
Original file line numberDiff line numberDiff line change
@@ -9,16 +9,16 @@
99

1010
namespace RealityToolkit.Editor.Inspectors
1111
{
12-
[CustomEditor(typeof(LockControllerVisualizerBehaviour), true)]
13-
public class LockControllerVisualizerBehaviourInspector : BaseInteractionBehaviourInspector
12+
[CustomEditor(typeof(HoldOntoBehaviour), true)]
13+
public class HoldOntoBehaviourInspector : BaseInteractionBehaviourInspector
1414
{
1515
private VisualElement inspector;
16-
private PropertyField smoothSyncPose;
17-
private PropertyField syncDuration;
16+
private PropertyField smooth;
17+
private PropertyField smoothingDuration;
1818

19-
private const string localOffsetPoseBindingPath = "localOffsetPose";
20-
private const string smoothSyncPoseBindingPath = nameof(smoothSyncPose);
21-
private const string syncPositionSpeedBindingPath = nameof(syncDuration);
19+
private const string hintBindingPath = "hint";
20+
private const string smoothSyncPoseBindingPath = nameof(smooth);
21+
private const string syncPositionSpeedBindingPath = nameof(smoothingDuration);
2222

2323
/// <summary>
2424
/// <inheritdoc/>
@@ -27,14 +27,14 @@ public override VisualElement CreateInspectorGUI()
2727
{
2828
inspector = base.CreateInspectorGUI();
2929

30-
inspector.Add(new PropertyField(serializedObject.FindProperty(localOffsetPoseBindingPath)));
30+
inspector.Add(new PropertyField(serializedObject.FindProperty(hintBindingPath)));
3131

32-
smoothSyncPose = new PropertyField(serializedObject.FindProperty(smoothSyncPoseBindingPath));
33-
smoothSyncPose.RegisterCallback<ChangeEvent<bool>>(SmoothSyncPose_ValueChanged);
34-
inspector.Add(smoothSyncPose);
32+
smooth = new PropertyField(serializedObject.FindProperty(smoothSyncPoseBindingPath));
33+
smooth.RegisterCallback<ChangeEvent<bool>>(SmoothSyncPose_ValueChanged);
34+
inspector.Add(smooth);
3535

36-
syncDuration = new PropertyField(serializedObject.FindProperty(syncPositionSpeedBindingPath));
37-
syncDuration.style.paddingLeft = UIElementsUtilities.DefaultInset;
36+
smoothingDuration = new PropertyField(serializedObject.FindProperty(syncPositionSpeedBindingPath));
37+
smoothingDuration.style.paddingLeft = UIElementsUtilities.DefaultInset;
3838

3939
UpdateSmoothSyncPoseFields(serializedObject.FindProperty(smoothSyncPoseBindingPath).boolValue);
4040

@@ -46,9 +46,9 @@ public override VisualElement CreateInspectorGUI()
4646
/// </summary>
4747
private void OnDestroy()
4848
{
49-
if (smoothSyncPose != null)
49+
if (smooth != null)
5050
{
51-
smoothSyncPose.UnregisterCallback<ChangeEvent<bool>>(SmoothSyncPose_ValueChanged);
51+
smooth.UnregisterCallback<ChangeEvent<bool>>(SmoothSyncPose_ValueChanged);
5252
}
5353
}
5454

@@ -58,14 +58,14 @@ private void UpdateSmoothSyncPoseFields(bool showFields)
5858
{
5959
if (showFields)
6060
{
61-
inspector.Add(syncDuration);
62-
syncDuration.PlaceInFront(smoothSyncPose);
61+
inspector.Add(smoothingDuration);
62+
smoothingDuration.PlaceInFront(smooth);
6363
return;
6464
}
6565

66-
if (inspector.Contains(syncDuration))
66+
if (inspector.Contains(smoothingDuration))
6767
{
68-
inspector.Remove(syncDuration);
68+
inspector.Remove(smoothingDuration);
6969
}
7070
}
7171
}

README.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
![com.realitytoolkit.core](https://github.com/realitycollective/realitycollective.logo/blob/main/RealityToolkit/RepoBanners/com.realitytoolkit.core.png?raw=true)
44

5-
The core module of the [Reality Toolkit](https://realitytoolkit.realitycollective.net/) contains base implementations used across toolkit modules and is mandatory in any project using the toolkit.
5+
The foundational module of the [Reality Toolkit](https://realitytoolkit.realitycollective.net/), providing essential code and assets required to develop XR applications. This package serves as the core dependency for all other [Reality Toolkit](https://realitytoolkit.realitycollective.net/) modules, ensuring seamless integration and functionality.
66

77
[![openupm](https://img.shields.io/npm/v/com.realitytoolkit.core?label=openupm&registry_uri=https://package.openupm.com)](https://openupm.com/packages/com.realitytoolkit.core/) [![Discord](https://img.shields.io/discord/597064584980987924.svg?label=&logo=discord&logoColor=ffffff&color=7389D8&labelColor=6A7EC2)](https://discord.gg/hF7TtRCFmB)
88
[![Publish main branch and increment version](https://github.com/realitycollective/com.realitytoolkit.core/actions/workflows/main-publish.yml/badge.svg)](https://github.com/realitycollective/com.realitytoolkit.core/actions/workflows/main-publish.yml)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,266 @@
1+
// Copyright (c) Reality Collective. All rights reserved.
2+
// Licensed under the MIT License. See LICENSE in the project root for license information.
3+
4+
using RealityCollective.Utilities.Extensions;
5+
using RealityToolkit.Input.Controllers;
6+
using RealityToolkit.Input.Events;
7+
using RealityToolkit.Input.Interactors;
8+
using System.Collections.Generic;
9+
using UnityEngine;
10+
11+
namespace RealityToolkit.Input.InteractionBehaviours
12+
{
13+
/// <summary>
14+
/// Syncs the <see cref="IControllerInteractor"/>'s <see cref="IControllerVisualizer"/>
15+
/// to the <see cref="Interactables.IInteractable"/> using <see cref="IControllerVisualizer.OverrideSourcePose"/>.
16+
/// </summary>
17+
/// <remarks>
18+
/// Only supports <see cref="IControllerInteractor"/>s.
19+
/// Does not support <see cref="IPokeInteractor"/>s and will ignore them.
20+
/// </remarks>
21+
[HelpURL(RealityToolkitRuntimePreferences.Toolkit_Docs_BaseUrl + "docs/interactions/interaction-behaviours/default-behaviours/hold-onto-behaviour")]
22+
[AddComponentMenu(RealityToolkitRuntimePreferences.Toolkit_InteractionsAddComponentMenu + "/" + nameof(HoldOntoBehaviour))]
23+
public class HoldOntoBehaviour : BaseInteractionBehaviour
24+
{
25+
/// <summary>
26+
/// Internal structure keeping track of locked visualizers and their state.
27+
/// </summary>
28+
private class VisualizerLockData
29+
{
30+
public bool IsLocked { get; set; }
31+
32+
public bool IsPendingUnlock { get; set; }
33+
34+
public float SmoothingStartTime { get; set; }
35+
36+
public float SmoothingProgress { get; set; }
37+
38+
public float SmoothingDuration { get; set; }
39+
}
40+
41+
[SerializeField, Tooltip("Optional: The pose to hold / grab onto when locking onto this interactable. Must be a child " +
42+
"transform of the component transform. If not set, the component transform is used.")]
43+
private Transform hint = null;
44+
45+
[SerializeField, Tooltip("If set, the controller visualizer will smoothly lock to the interactable instead of instantly.")]
46+
private bool smooth = true;
47+
48+
[SerializeField, Min(.01f), Tooltip("Duration in seconds to sync the visualizer pose with the interactable. The value is in seconds per unit and will " +
49+
"interpolate based on distance. So if the visualizer is 5 units away, the total sync duration is five times the configured duration.")]
50+
private float smoothingDuration = 1f;
51+
52+
private readonly Dictionary<IControllerVisualizer, VisualizerLockData> visualizers = new();
53+
private readonly List<IControllerVisualizer> visualizersForClearing = new();
54+
55+
/// <inheritdoc/>
56+
protected override void Awake()
57+
{
58+
base.Awake();
59+
60+
if (hint.IsNull())
61+
{
62+
hint = transform;
63+
}
64+
else if (!hint.IsChildOf(transform))
65+
{
66+
Debug.LogError($"{hint.name} must be a child transform of {transform.name}.", this);
67+
}
68+
69+
if (smooth && smoothingDuration < 0f)
70+
{
71+
Debug.LogError("Sync duration must be non-negative and above 0.", this);
72+
}
73+
}
74+
75+
/// <inheritdoc/>
76+
protected override void Update()
77+
{
78+
if (visualizers.Count == 0)
79+
{
80+
return;
81+
}
82+
83+
foreach (var item in visualizers)
84+
{
85+
var visualizer = item.Key;
86+
var data = item.Value;
87+
88+
var t = CalculateSmoothingTransition(visualizer, data);
89+
if (data.IsPendingUnlock && t >= 1f)
90+
{
91+
visualizersForClearing.Add(visualizer);
92+
continue;
93+
}
94+
95+
var lockPose = GetInteractableLockPose(visualizer);
96+
var targetPose = lockPose;
97+
98+
if (data.IsPendingUnlock)
99+
{
100+
// Smoothly return to actual controller pose.
101+
var startPose = GetInteractableLockPose(visualizer);
102+
var unlockPose = GetVisualizerUnlockPose(visualizer);
103+
104+
targetPose.position = Vector3.Slerp(startPose.position, unlockPose.position, data.SmoothingProgress);
105+
targetPose.rotation = Quaternion.Slerp(startPose.rotation, unlockPose.rotation, data.SmoothingProgress);
106+
}
107+
else if (!data.IsPendingUnlock && t < 1f)
108+
{
109+
// Smoothly lock onto the interactable.
110+
var startPose = GetVisualizerUnlockPose(visualizer);
111+
targetPose.position = Vector3.Slerp(startPose.position, lockPose.position, data.SmoothingProgress);
112+
targetPose.rotation = Quaternion.Slerp(startPose.rotation, lockPose.rotation, data.SmoothingProgress);
113+
}
114+
115+
// The position we store is in local space while the rotation is a world rotation.
116+
visualizer.PoseDriver.localPosition = targetPose.position;
117+
visualizer.PoseDriver.rotation = targetPose.rotation;
118+
}
119+
120+
for (var i = 0; i < visualizersForClearing.Count; i++)
121+
{
122+
CleanUpVisualizer(visualizersForClearing[i]);
123+
}
124+
125+
visualizersForClearing.Clear();
126+
}
127+
128+
/// <inheritdoc/>
129+
protected override void OnSelectEntered(InteractionEventArgs eventArgs)
130+
{
131+
if (eventArgs.Interactor is not IControllerInteractor controllerInteractor ||
132+
eventArgs.Interactor is IPokeInteractor)
133+
{
134+
return;
135+
}
136+
137+
LockVisualizer(controllerInteractor.Controller.Visualizer);
138+
}
139+
140+
/// <inheritdoc/>
141+
protected override void OnSelectExited(InteractionExitEventArgs eventArgs)
142+
{
143+
if (eventArgs.Interactor is not IControllerInteractor controllerInteractor ||
144+
eventArgs.Interactor is IPokeInteractor)
145+
{
146+
return;
147+
}
148+
149+
UnlockVisualizer(controllerInteractor.Controller.Visualizer);
150+
}
151+
152+
/// <inheritdoc/>
153+
protected override void OnGrabEntered(InteractionEventArgs eventArgs)
154+
{
155+
if (eventArgs.Interactor is not IControllerInteractor controllerInteractor ||
156+
eventArgs.Interactor is IPokeInteractor)
157+
{
158+
return;
159+
}
160+
161+
LockVisualizer(controllerInteractor.Controller.Visualizer);
162+
}
163+
164+
/// <inheritdoc/>
165+
protected override void OnGrabExited(InteractionExitEventArgs eventArgs)
166+
{
167+
if (eventArgs.Interactor is not IControllerInteractor controllerInteractor ||
168+
eventArgs.Interactor is IPokeInteractor)
169+
{
170+
return;
171+
}
172+
173+
UnlockVisualizer(controllerInteractor.Controller.Visualizer);
174+
}
175+
176+
private void LockVisualizer(IControllerVisualizer visualizer)
177+
{
178+
visualizers.EnsureDictionaryItem(visualizer, new VisualizerLockData
179+
{
180+
IsPendingUnlock = false,
181+
IsLocked = !smooth,
182+
SmoothingStartTime = Time.time,
183+
SmoothingProgress = 0f,
184+
SmoothingDuration = smooth ? CalculateSmoothingDuration(visualizer) : 0f
185+
});
186+
187+
visualizer.OverrideSourcePose = true;
188+
}
189+
190+
private void UnlockVisualizer(IControllerVisualizer visualizer)
191+
{
192+
if (!smooth)
193+
{
194+
CleanUpVisualizer(visualizer);
195+
return;
196+
}
197+
198+
visualizers.EnsureDictionaryItem(visualizer, new VisualizerLockData
199+
{
200+
IsPendingUnlock = true,
201+
IsLocked = false,
202+
SmoothingStartTime = Time.time,
203+
SmoothingProgress = 0f,
204+
SmoothingDuration = CalculateSmoothingDuration(visualizer)
205+
}, true);
206+
}
207+
208+
private void CleanUpVisualizer(IControllerVisualizer visualizer)
209+
{
210+
visualizers.SafeRemoveDictionaryItem(visualizer);
211+
visualizer.OverrideSourcePose = false;
212+
}
213+
214+
private Pose GetInteractableLockPose(IControllerVisualizer visualizer)
215+
{
216+
// If the visualizer is not parented, that means we'll be positioning it in world space.
217+
// So we can use the hint transform world pose directly.
218+
if (visualizer.PoseDriver.parent.IsNull())
219+
{
220+
return new Pose(hint.position, hint.rotation);
221+
}
222+
223+
// If the visualzer is parented, we'll be positioning it within the local space of its parent,
224+
// so we must first figure out where the hint transform is located within that space.
225+
return new Pose(visualizer.PoseDriver.parent.InverseTransformPoint(hint.position), hint.rotation);
226+
}
227+
228+
private Pose GetVisualizerUnlockPose(IControllerVisualizer visualizer)
229+
{
230+
visualizer.Controller.TryGetPose(Space.World, out var controllerPose);
231+
232+
// If the visualizer is not parented, we can use the controller's actual world space
233+
// pose to determine where it currently is, since we'll be positioning in world space.
234+
if (visualizer.PoseDriver.parent.IsNull())
235+
{
236+
return controllerPose;
237+
}
238+
239+
// If the visualizer is parented, we'll be positioning it within the local space of its parent,
240+
// so we need to know where the actual controller currently is within that space.
241+
return new Pose(visualizer.PoseDriver.parent.InverseTransformPoint(controllerPose.position), controllerPose.rotation);
242+
}
243+
244+
private float CalculateSmoothingDuration(IControllerVisualizer visualizer)
245+
{
246+
var start = hint.position;
247+
var end = visualizer.Controller.TryGetPose(Space.World, out var controllerPose) ? controllerPose.position : visualizer.PoseDriver.position;
248+
var distance = Vector3.Distance(start, end);
249+
250+
return distance * smoothingDuration;
251+
}
252+
253+
private float CalculateSmoothingTransition(IControllerVisualizer visualizer, VisualizerLockData data)
254+
{
255+
if (!smooth || data.IsLocked || data.SmoothingProgress >= 1f)
256+
{
257+
return 1f;
258+
}
259+
260+
var t = (Time.time - data.SmoothingStartTime) / data.SmoothingDuration;
261+
data.SmoothingProgress = t;
262+
263+
return t;
264+
}
265+
}
266+
}

0 commit comments

Comments
 (0)