8 using UnityEngine.Events;
9 using System.Collections;
11 namespace Valve.VR.InteractionSystem
15 [RequireComponent( typeof( Interactable ) )]
25 [Tooltip(
"The axis around which the circular drive will rotate in local space" )]
26 public Axis_t axisOfRotation = Axis_t.XAxis;
28 [Tooltip(
"Child GameObject which has the Collider component to initiate interaction, only needs to be set if there is more than one Collider child" )]
29 public Collider childCollider = null;
31 [Tooltip(
"A LinearMapping component to drive, if not specified one will be dynamically added to this GameObject" )]
34 [Tooltip(
"If true, the drive will stay manipulating as long as the button is held down, if false, it will stop if the controller moves out of the collider" )]
35 public bool hoverLock =
false;
37 [HeaderAttribute(
"Limited Rotation" )]
38 [Tooltip(
"If true, the rotation will be limited to [minAngle, maxAngle], if false, the rotation is unlimited" )]
39 public bool limited =
false;
40 public Vector2 frozenDistanceMinMaxThreshold =
new Vector2( 0.1f, 0.2f );
41 public UnityEvent onFrozenDistanceThreshold;
43 [HeaderAttribute(
"Limited Rotation Min" )]
44 [Tooltip(
"If limited is true, the specifies the lower limit, otherwise value is unused" )]
45 public float minAngle = -45.0f;
46 [Tooltip(
"If limited, set whether drive will freeze its angle when the min angle is reached" )]
47 public bool freezeOnMin =
false;
48 [Tooltip(
"If limited, event invoked when minAngle is reached" )]
49 public UnityEvent onMinAngle;
51 [HeaderAttribute(
"Limited Rotation Max" )]
52 [Tooltip(
"If limited is true, the specifies the upper limit, otherwise value is unused" )]
53 public float maxAngle = 45.0f;
54 [Tooltip(
"If limited, set whether drive will freeze its angle when the max angle is reached" )]
55 public bool freezeOnMax =
false;
56 [Tooltip(
"If limited, event invoked when maxAngle is reached" )]
57 public UnityEvent onMaxAngle;
59 [Tooltip(
"If limited is true, this forces the starting angle to be startAngle, clamped to [minAngle, maxAngle]" )]
60 public bool forceStart =
false;
61 [Tooltip(
"If limited is true and forceStart is true, the starting angle will be this, clamped to [minAngle, maxAngle]" )]
62 public float startAngle = 0.0f;
64 [Tooltip(
"If true, the transform of the GameObject this component is on will be rotated accordingly" )]
65 public bool rotateGameObject =
true;
67 [Tooltip(
"If true, the path of the Hand (red) and the projected value (green) will be drawn" )]
68 public bool debugPath =
false;
69 [Tooltip(
"If debugPath is true, this is the maximum number of GameObjects to create to draw the path" )]
70 public int dbgPathLimit = 50;
72 [Tooltip(
"If not null, the TextMesh will display the linear value and the angular value of this circular drive" )]
73 public TextMesh debugText = null;
75 [Tooltip(
"The output angle value of the drive in degrees, unlimited will increase or decrease without bound, take the 360 modulus to find number of rotations" )]
76 public float outAngle;
78 private Quaternion start;
80 private Vector3 worldPlaneNormal =
new Vector3( 1.0f, 0.0f, 0.0f );
81 private Vector3 localPlaneNormal =
new Vector3( 1.0f, 0.0f, 0.0f );
83 private Vector3 lastHandProjected;
85 private Color red =
new Color( 1.0f, 0.0f, 0.0f );
86 private Color green =
new Color( 0.0f, 1.0f, 0.0f );
88 private GameObject[] dbgHandObjects;
89 private GameObject[] dbgProjObjects;
90 private GameObject dbgObjectsParent;
91 private int dbgObjectCount = 0;
92 private int dbgObjectIndex = 0;
94 private bool driving =
false;
97 private float minMaxAngularThreshold = 1.0f;
99 private bool frozen =
false;
100 private float frozenAngle = 0.0f;
101 private Vector3 frozenHandWorldPos =
new Vector3( 0.0f, 0.0f, 0.0f );
102 private Vector2 frozenSqDistanceMinMaxThreshold =
new Vector2( 0.0f, 0.0f );
104 Hand handHoverLocked = null;
107 private void Freeze(
Hand hand )
110 frozenAngle = outAngle;
111 frozenHandWorldPos = hand.hoverSphereTransform.position;
112 frozenSqDistanceMinMaxThreshold.x = frozenDistanceMinMaxThreshold.x * frozenDistanceMinMaxThreshold.x;
113 frozenSqDistanceMinMaxThreshold.y = frozenDistanceMinMaxThreshold.y * frozenDistanceMinMaxThreshold.y;
118 private void UnFreeze()
121 frozenHandWorldPos.Set( 0.0f, 0.0f, 0.0f );
128 if ( childCollider == null )
130 childCollider = GetComponentInChildren<Collider>();
133 if ( linearMapping == null )
135 linearMapping = GetComponent<LinearMapping>();
138 if ( linearMapping == null )
143 worldPlaneNormal =
new Vector3( 0.0f, 0.0f, 0.0f );
144 worldPlaneNormal[(int)axisOfRotation] = 1.0f;
146 localPlaneNormal = worldPlaneNormal;
148 if ( transform.parent )
150 worldPlaneNormal = transform.parent.localToWorldMatrix.MultiplyVector( worldPlaneNormal ).normalized;
155 start = Quaternion.identity;
156 outAngle = transform.localEulerAngles[(int)axisOfRotation];
160 outAngle = Mathf.Clamp( startAngle, minAngle, maxAngle );
165 start = Quaternion.AngleAxis( transform.localEulerAngles[(int)axisOfRotation], localPlaneNormal );
171 debugText.alignment = TextAlignment.Left;
172 debugText.anchor = TextAnchor.UpperLeft;
182 if ( handHoverLocked )
184 ControllerButtonHints.HideButtonHint( handHoverLocked, Valve.VR.EVRButtonId.k_EButton_SteamVR_Trigger );
185 handHoverLocked.HoverUnlock( GetComponent<Interactable>() );
186 handHoverLocked = null;
192 private IEnumerator HapticPulses(
SteamVR_Controller.Device controller,
float flMagnitude,
int nCount )
194 if ( controller != null )
196 int nRangeMax = (int)Util.RemapNumberClamped( flMagnitude, 0.0f, 1.0f, 100.0f, 900.0f );
197 nCount = Mathf.Clamp( nCount, 1, 10 );
199 for ( ushort i = 0; i < nCount; ++i )
201 ushort duration = (ushort)Random.Range( 100, nRangeMax );
202 controller.TriggerHapticPulse( duration );
203 yield
return new WaitForSeconds( .01f );
210 private void OnHandHoverBegin(
Hand hand )
212 ControllerButtonHints.ShowButtonHint( hand, Valve.VR.EVRButtonId.k_EButton_SteamVR_Trigger );
217 private void OnHandHoverEnd(
Hand hand )
219 ControllerButtonHints.HideButtonHint( hand, Valve.VR.EVRButtonId.k_EButton_SteamVR_Trigger );
221 if ( driving && hand.GetStandardInteractionButton() )
223 StartCoroutine( HapticPulses( hand.controller, 1.0f, 10 ) );
227 handHoverLocked = null;
232 private void HandHoverUpdate(
Hand hand )
234 if ( hand.GetStandardInteractionButtonDown() )
237 lastHandProjected = ComputeToTransformProjected( hand.hoverSphereTransform );
241 hand.HoverLock( GetComponent<Interactable>() );
242 handHoverLocked = hand;
247 ComputeAngle( hand );
250 ControllerButtonHints.HideButtonHint( hand, Valve.VR.EVRButtonId.k_EButton_SteamVR_Trigger );
252 else if ( hand.GetStandardInteractionButtonUp() )
257 hand.HoverUnlock( GetComponent<Interactable>() );
258 handHoverLocked = null;
261 else if ( driving && hand.GetStandardInteractionButton() && hand.hoveringInteractable == GetComponent<Interactable>() )
263 ComputeAngle( hand );
270 private Vector3 ComputeToTransformProjected( Transform xForm )
272 Vector3 toTransform = ( xForm.position - transform.position ).normalized;
273 Vector3 toTransformProjected =
new Vector3( 0.0f, 0.0f, 0.0f );
276 if ( toTransform.sqrMagnitude > 0.0f )
278 toTransformProjected = Vector3.ProjectOnPlane( toTransform, worldPlaneNormal ).normalized;
282 Debug.LogFormat(
"The collider needs to be a minimum distance away from the CircularDrive GameObject {0}", gameObject.ToString() );
283 Debug.Assert(
false, string.Format(
"The collider needs to be a minimum distance away from the CircularDrive GameObject {0}", gameObject.ToString() ) );
286 if ( debugPath && dbgPathLimit > 0 )
288 DrawDebugPath( xForm, toTransformProjected );
291 return toTransformProjected;
296 private void DrawDebugPath( Transform xForm, Vector3 toTransformProjected )
298 if ( dbgObjectCount == 0 )
300 dbgObjectsParent =
new GameObject(
"Circular Drive Debug" );
301 dbgHandObjects =
new GameObject[dbgPathLimit];
302 dbgProjObjects =
new GameObject[dbgPathLimit];
303 dbgObjectCount = dbgPathLimit;
308 GameObject gSphere = null;
310 if ( dbgHandObjects[dbgObjectIndex] )
312 gSphere = dbgHandObjects[dbgObjectIndex];
316 gSphere = GameObject.CreatePrimitive( PrimitiveType.Sphere );
317 gSphere.transform.SetParent( dbgObjectsParent.transform );
318 dbgHandObjects[dbgObjectIndex] = gSphere;
321 gSphere.name = string.Format(
"actual_{0}", (int)( ( 1.0f - red.r ) * 10.0f ) );
322 gSphere.transform.position = xForm.position;
323 gSphere.transform.rotation = Quaternion.Euler( 0.0f, 0.0f, 0.0f );
324 gSphere.transform.localScale =
new Vector3( 0.004f, 0.004f, 0.004f );
325 gSphere.gameObject.GetComponent<Renderer>().material.color = red;
339 if ( dbgProjObjects[dbgObjectIndex] )
341 gSphere = dbgProjObjects[dbgObjectIndex];
345 gSphere = GameObject.CreatePrimitive( PrimitiveType.Sphere );
346 gSphere.transform.SetParent( dbgObjectsParent.transform );
347 dbgProjObjects[dbgObjectIndex] = gSphere;
350 gSphere.name = string.Format(
"projed_{0}", (int)( ( 1.0f - green.g ) * 10.0f ) );
351 gSphere.transform.position = transform.position + toTransformProjected * 0.25f;
352 gSphere.transform.rotation = Quaternion.Euler( 0.0f, 0.0f, 0.0f );
353 gSphere.transform.localScale =
new Vector3( 0.004f, 0.004f, 0.004f );
354 gSphere.gameObject.GetComponent<Renderer>().material.color = green;
356 if ( green.g > 0.1f )
365 dbgObjectIndex = ( dbgObjectIndex + 1 ) % dbgObjectCount;
372 private void UpdateLinearMapping()
377 linearMapping.value = ( outAngle - minAngle ) / ( maxAngle - minAngle );
382 float flTmp = outAngle / 360.0f;
383 linearMapping.value = flTmp - Mathf.Floor( flTmp );
393 private void UpdateGameObject()
395 if ( rotateGameObject )
397 transform.localRotation = start * Quaternion.AngleAxis( outAngle, localPlaneNormal );
405 private void UpdateDebugText()
409 debugText.text = string.Format(
"Linear: {0}\nAngle: {1}\n", linearMapping.value, outAngle );
417 private void UpdateAll()
419 UpdateLinearMapping();
428 private void ComputeAngle(
Hand hand )
430 Vector3 toHandProjected = ComputeToTransformProjected( hand.hoverSphereTransform );
432 if ( !toHandProjected.Equals( lastHandProjected ) )
434 float absAngleDelta = Vector3.Angle( lastHandProjected, toHandProjected );
436 if ( absAngleDelta > 0.0f )
440 float frozenSqDist = ( hand.hoverSphereTransform.position - frozenHandWorldPos ).sqrMagnitude;
441 if ( frozenSqDist > frozenSqDistanceMinMaxThreshold.x )
443 outAngle = frozenAngle + Random.Range( -1.0f, 1.0f );
445 float magnitude = Util.RemapNumberClamped( frozenSqDist, frozenSqDistanceMinMaxThreshold.x, frozenSqDistanceMinMaxThreshold.y, 0.0f, 1.0f );
448 StartCoroutine( HapticPulses( hand.controller, magnitude, 10 ) );
452 StartCoroutine( HapticPulses( hand.controller, 0.5f, 10 ) );
455 if ( frozenSqDist >= frozenSqDistanceMinMaxThreshold.y )
457 onFrozenDistanceThreshold.Invoke();
463 Vector3 cross = Vector3.Cross( lastHandProjected, toHandProjected ).normalized;
464 float dot = Vector3.Dot( worldPlaneNormal, cross );
466 float signedAngleDelta = absAngleDelta;
470 signedAngleDelta = -signedAngleDelta;
475 float angleTmp = Mathf.Clamp( outAngle + signedAngleDelta, minAngle, maxAngle );
477 if ( outAngle == minAngle )
479 if ( angleTmp > minAngle && absAngleDelta < minMaxAngularThreshold )
482 lastHandProjected = toHandProjected;
485 else if ( outAngle == maxAngle )
487 if ( angleTmp < maxAngle && absAngleDelta < minMaxAngularThreshold )
490 lastHandProjected = toHandProjected;
493 else if ( angleTmp == minAngle )
496 lastHandProjected = toHandProjected;
503 else if ( angleTmp == maxAngle )
506 lastHandProjected = toHandProjected;
516 lastHandProjected = toHandProjected;
521 outAngle += signedAngleDelta;
522 lastHandProjected = toHandProjected;