IMHOTEP Framework
 All Classes Namespaces Functions Variables Enumerations Enumerator Properties Pages
CircularDrive.cs
1 //======= Copyright (c) Valve Corporation, All rights reserved. ===============
2 //
3 // Purpose: Interactable that can be used to move in a circular motion
4 //
5 //=============================================================================
6 
7 using UnityEngine;
8 using UnityEngine.Events;
9 using System.Collections;
10 
11 namespace Valve.VR.InteractionSystem
12 {
13 
14  //-------------------------------------------------------------------------
15  [RequireComponent( typeof( Interactable ) )]
16  public class CircularDrive : MonoBehaviour
17  {
18  public enum Axis_t
19  {
20  XAxis,
21  YAxis,
22  ZAxis
23  };
24 
25  [Tooltip( "The axis around which the circular drive will rotate in local space" )]
26  public Axis_t axisOfRotation = Axis_t.XAxis;
27 
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;
30 
31  [Tooltip( "A LinearMapping component to drive, if not specified one will be dynamically added to this GameObject" )]
32  public LinearMapping linearMapping;
33 
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;
36 
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;
42 
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;
50 
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;
58 
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;
63 
64  [Tooltip( "If true, the transform of the GameObject this component is on will be rotated accordingly" )]
65  public bool rotateGameObject = true;
66 
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;
71 
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;
74 
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;
77 
78  private Quaternion start;
79 
80  private Vector3 worldPlaneNormal = new Vector3( 1.0f, 0.0f, 0.0f );
81  private Vector3 localPlaneNormal = new Vector3( 1.0f, 0.0f, 0.0f );
82 
83  private Vector3 lastHandProjected;
84 
85  private Color red = new Color( 1.0f, 0.0f, 0.0f );
86  private Color green = new Color( 0.0f, 1.0f, 0.0f );
87 
88  private GameObject[] dbgHandObjects;
89  private GameObject[] dbgProjObjects;
90  private GameObject dbgObjectsParent;
91  private int dbgObjectCount = 0;
92  private int dbgObjectIndex = 0;
93 
94  private bool driving = false;
95 
96  // If the drive is limited as is at min/max, angles greater than this are ignored
97  private float minMaxAngularThreshold = 1.0f;
98 
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 );
103 
104  Hand handHoverLocked = null;
105 
106  //-------------------------------------------------
107  private void Freeze( Hand hand )
108  {
109  frozen = true;
110  frozenAngle = outAngle;
111  frozenHandWorldPos = hand.hoverSphereTransform.position;
112  frozenSqDistanceMinMaxThreshold.x = frozenDistanceMinMaxThreshold.x * frozenDistanceMinMaxThreshold.x;
113  frozenSqDistanceMinMaxThreshold.y = frozenDistanceMinMaxThreshold.y * frozenDistanceMinMaxThreshold.y;
114  }
115 
116 
117  //-------------------------------------------------
118  private void UnFreeze()
119  {
120  frozen = false;
121  frozenHandWorldPos.Set( 0.0f, 0.0f, 0.0f );
122  }
123 
124 
125  //-------------------------------------------------
126  void Start()
127  {
128  if ( childCollider == null )
129  {
130  childCollider = GetComponentInChildren<Collider>();
131  }
132 
133  if ( linearMapping == null )
134  {
135  linearMapping = GetComponent<LinearMapping>();
136  }
137 
138  if ( linearMapping == null )
139  {
140  linearMapping = gameObject.AddComponent<LinearMapping>();
141  }
142 
143  worldPlaneNormal = new Vector3( 0.0f, 0.0f, 0.0f );
144  worldPlaneNormal[(int)axisOfRotation] = 1.0f;
145 
146  localPlaneNormal = worldPlaneNormal;
147 
148  if ( transform.parent )
149  {
150  worldPlaneNormal = transform.parent.localToWorldMatrix.MultiplyVector( worldPlaneNormal ).normalized;
151  }
152 
153  if ( limited )
154  {
155  start = Quaternion.identity;
156  outAngle = transform.localEulerAngles[(int)axisOfRotation];
157 
158  if ( forceStart )
159  {
160  outAngle = Mathf.Clamp( startAngle, minAngle, maxAngle );
161  }
162  }
163  else
164  {
165  start = Quaternion.AngleAxis( transform.localEulerAngles[(int)axisOfRotation], localPlaneNormal );
166  outAngle = 0.0f;
167  }
168 
169  if ( debugText )
170  {
171  debugText.alignment = TextAlignment.Left;
172  debugText.anchor = TextAnchor.UpperLeft;
173  }
174 
175  UpdateAll();
176  }
177 
178 
179  //-------------------------------------------------
180  void OnDisable()
181  {
182  if ( handHoverLocked )
183  {
184  ControllerButtonHints.HideButtonHint( handHoverLocked, Valve.VR.EVRButtonId.k_EButton_SteamVR_Trigger );
185  handHoverLocked.HoverUnlock( GetComponent<Interactable>() );
186  handHoverLocked = null;
187  }
188  }
189 
190 
191  //-------------------------------------------------
192  private IEnumerator HapticPulses( SteamVR_Controller.Device controller, float flMagnitude, int nCount )
193  {
194  if ( controller != null )
195  {
196  int nRangeMax = (int)Util.RemapNumberClamped( flMagnitude, 0.0f, 1.0f, 100.0f, 900.0f );
197  nCount = Mathf.Clamp( nCount, 1, 10 );
198 
199  for ( ushort i = 0; i < nCount; ++i )
200  {
201  ushort duration = (ushort)Random.Range( 100, nRangeMax );
202  controller.TriggerHapticPulse( duration );
203  yield return new WaitForSeconds( .01f );
204  }
205  }
206  }
207 
208 
209  //-------------------------------------------------
210  private void OnHandHoverBegin( Hand hand )
211  {
212  ControllerButtonHints.ShowButtonHint( hand, Valve.VR.EVRButtonId.k_EButton_SteamVR_Trigger );
213  }
214 
215 
216  //-------------------------------------------------
217  private void OnHandHoverEnd( Hand hand )
218  {
219  ControllerButtonHints.HideButtonHint( hand, Valve.VR.EVRButtonId.k_EButton_SteamVR_Trigger );
220 
221  if ( driving && hand.GetStandardInteractionButton() )
222  {
223  StartCoroutine( HapticPulses( hand.controller, 1.0f, 10 ) );
224  }
225 
226  driving = false;
227  handHoverLocked = null;
228  }
229 
230 
231  //-------------------------------------------------
232  private void HandHoverUpdate( Hand hand )
233  {
234  if ( hand.GetStandardInteractionButtonDown() )
235  {
236  // Trigger was just pressed
237  lastHandProjected = ComputeToTransformProjected( hand.hoverSphereTransform );
238 
239  if ( hoverLock )
240  {
241  hand.HoverLock( GetComponent<Interactable>() );
242  handHoverLocked = hand;
243  }
244 
245  driving = true;
246 
247  ComputeAngle( hand );
248  UpdateAll();
249 
250  ControllerButtonHints.HideButtonHint( hand, Valve.VR.EVRButtonId.k_EButton_SteamVR_Trigger );
251  }
252  else if ( hand.GetStandardInteractionButtonUp() )
253  {
254  // Trigger was just released
255  if ( hoverLock )
256  {
257  hand.HoverUnlock( GetComponent<Interactable>() );
258  handHoverLocked = null;
259  }
260  }
261  else if ( driving && hand.GetStandardInteractionButton() && hand.hoveringInteractable == GetComponent<Interactable>() )
262  {
263  ComputeAngle( hand );
264  UpdateAll();
265  }
266  }
267 
268 
269  //-------------------------------------------------
270  private Vector3 ComputeToTransformProjected( Transform xForm )
271  {
272  Vector3 toTransform = ( xForm.position - transform.position ).normalized;
273  Vector3 toTransformProjected = new Vector3( 0.0f, 0.0f, 0.0f );
274 
275  // Need a non-zero distance from the hand to the center of the CircularDrive
276  if ( toTransform.sqrMagnitude > 0.0f )
277  {
278  toTransformProjected = Vector3.ProjectOnPlane( toTransform, worldPlaneNormal ).normalized;
279  }
280  else
281  {
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() ) );
284  }
285 
286  if ( debugPath && dbgPathLimit > 0 )
287  {
288  DrawDebugPath( xForm, toTransformProjected );
289  }
290 
291  return toTransformProjected;
292  }
293 
294 
295  //-------------------------------------------------
296  private void DrawDebugPath( Transform xForm, Vector3 toTransformProjected )
297  {
298  if ( dbgObjectCount == 0 )
299  {
300  dbgObjectsParent = new GameObject( "Circular Drive Debug" );
301  dbgHandObjects = new GameObject[dbgPathLimit];
302  dbgProjObjects = new GameObject[dbgPathLimit];
303  dbgObjectCount = dbgPathLimit;
304  dbgObjectIndex = 0;
305  }
306 
307  //Actual path
308  GameObject gSphere = null;
309 
310  if ( dbgHandObjects[dbgObjectIndex] )
311  {
312  gSphere = dbgHandObjects[dbgObjectIndex];
313  }
314  else
315  {
316  gSphere = GameObject.CreatePrimitive( PrimitiveType.Sphere );
317  gSphere.transform.SetParent( dbgObjectsParent.transform );
318  dbgHandObjects[dbgObjectIndex] = gSphere;
319  }
320 
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;
326 
327  if ( red.r > 0.1f )
328  {
329  red.r -= 0.1f;
330  }
331  else
332  {
333  red.r = 1.0f;
334  }
335 
336  //Projected path
337  gSphere = null;
338 
339  if ( dbgProjObjects[dbgObjectIndex] )
340  {
341  gSphere = dbgProjObjects[dbgObjectIndex];
342  }
343  else
344  {
345  gSphere = GameObject.CreatePrimitive( PrimitiveType.Sphere );
346  gSphere.transform.SetParent( dbgObjectsParent.transform );
347  dbgProjObjects[dbgObjectIndex] = gSphere;
348  }
349 
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;
355 
356  if ( green.g > 0.1f )
357  {
358  green.g -= 0.1f;
359  }
360  else
361  {
362  green.g = 1.0f;
363  }
364 
365  dbgObjectIndex = ( dbgObjectIndex + 1 ) % dbgObjectCount;
366  }
367 
368 
369  //-------------------------------------------------
370  // Updates the LinearMapping value from the angle
371  //-------------------------------------------------
372  private void UpdateLinearMapping()
373  {
374  if ( limited )
375  {
376  // Map it to a [0, 1] value
377  linearMapping.value = ( outAngle - minAngle ) / ( maxAngle - minAngle );
378  }
379  else
380  {
381  // Normalize to [0, 1] based on 360 degree windings
382  float flTmp = outAngle / 360.0f;
383  linearMapping.value = flTmp - Mathf.Floor( flTmp );
384  }
385 
386  UpdateDebugText();
387  }
388 
389 
390  //-------------------------------------------------
391  // Updates the LinearMapping value from the angle
392  //-------------------------------------------------
393  private void UpdateGameObject()
394  {
395  if ( rotateGameObject )
396  {
397  transform.localRotation = start * Quaternion.AngleAxis( outAngle, localPlaneNormal );
398  }
399  }
400 
401 
402  //-------------------------------------------------
403  // Updates the Debug TextMesh with the linear mapping value and the angle
404  //-------------------------------------------------
405  private void UpdateDebugText()
406  {
407  if ( debugText )
408  {
409  debugText.text = string.Format( "Linear: {0}\nAngle: {1}\n", linearMapping.value, outAngle );
410  }
411  }
412 
413 
414  //-------------------------------------------------
415  // Updates the Debug TextMesh with the linear mapping value and the angle
416  //-------------------------------------------------
417  private void UpdateAll()
418  {
419  UpdateLinearMapping();
420  UpdateGameObject();
421  UpdateDebugText();
422  }
423 
424 
425  //-------------------------------------------------
426  // Computes the angle to rotate the game object based on the change in the transform
427  //-------------------------------------------------
428  private void ComputeAngle( Hand hand )
429  {
430  Vector3 toHandProjected = ComputeToTransformProjected( hand.hoverSphereTransform );
431 
432  if ( !toHandProjected.Equals( lastHandProjected ) )
433  {
434  float absAngleDelta = Vector3.Angle( lastHandProjected, toHandProjected );
435 
436  if ( absAngleDelta > 0.0f )
437  {
438  if ( frozen )
439  {
440  float frozenSqDist = ( hand.hoverSphereTransform.position - frozenHandWorldPos ).sqrMagnitude;
441  if ( frozenSqDist > frozenSqDistanceMinMaxThreshold.x )
442  {
443  outAngle = frozenAngle + Random.Range( -1.0f, 1.0f );
444 
445  float magnitude = Util.RemapNumberClamped( frozenSqDist, frozenSqDistanceMinMaxThreshold.x, frozenSqDistanceMinMaxThreshold.y, 0.0f, 1.0f );
446  if ( magnitude > 0 )
447  {
448  StartCoroutine( HapticPulses( hand.controller, magnitude, 10 ) );
449  }
450  else
451  {
452  StartCoroutine( HapticPulses( hand.controller, 0.5f, 10 ) );
453  }
454 
455  if ( frozenSqDist >= frozenSqDistanceMinMaxThreshold.y )
456  {
457  onFrozenDistanceThreshold.Invoke();
458  }
459  }
460  }
461  else
462  {
463  Vector3 cross = Vector3.Cross( lastHandProjected, toHandProjected ).normalized;
464  float dot = Vector3.Dot( worldPlaneNormal, cross );
465 
466  float signedAngleDelta = absAngleDelta;
467 
468  if ( dot < 0.0f )
469  {
470  signedAngleDelta = -signedAngleDelta;
471  }
472 
473  if ( limited )
474  {
475  float angleTmp = Mathf.Clamp( outAngle + signedAngleDelta, minAngle, maxAngle );
476 
477  if ( outAngle == minAngle )
478  {
479  if ( angleTmp > minAngle && absAngleDelta < minMaxAngularThreshold )
480  {
481  outAngle = angleTmp;
482  lastHandProjected = toHandProjected;
483  }
484  }
485  else if ( outAngle == maxAngle )
486  {
487  if ( angleTmp < maxAngle && absAngleDelta < minMaxAngularThreshold )
488  {
489  outAngle = angleTmp;
490  lastHandProjected = toHandProjected;
491  }
492  }
493  else if ( angleTmp == minAngle )
494  {
495  outAngle = angleTmp;
496  lastHandProjected = toHandProjected;
497  onMinAngle.Invoke();
498  if ( freezeOnMin )
499  {
500  Freeze( hand );
501  }
502  }
503  else if ( angleTmp == maxAngle )
504  {
505  outAngle = angleTmp;
506  lastHandProjected = toHandProjected;
507  onMaxAngle.Invoke();
508  if ( freezeOnMax )
509  {
510  Freeze( hand );
511  }
512  }
513  else
514  {
515  outAngle = angleTmp;
516  lastHandProjected = toHandProjected;
517  }
518  }
519  else
520  {
521  outAngle += signedAngleDelta;
522  lastHandProjected = toHandProjected;
523  }
524  }
525  }
526  }
527  }
528  }
529 }