548 lines
16 KiB
C#
548 lines
16 KiB
C#
//======= Copyright (c) Valve Corporation, All rights reserved. ===============
|
|
//
|
|
// Purpose: Interactable that can be used to move in a circular motion
|
|
//
|
|
//=============================================================================
|
|
|
|
using UnityEngine;
|
|
using UnityEngine.Events;
|
|
using System.Collections;
|
|
|
|
namespace Valve.VR.InteractionSystem
|
|
{
|
|
|
|
//-------------------------------------------------------------------------
|
|
[RequireComponent( typeof( Interactable ) )]
|
|
public class CircularDrive : MonoBehaviour
|
|
{
|
|
public enum Axis_t
|
|
{
|
|
XAxis,
|
|
YAxis,
|
|
ZAxis
|
|
};
|
|
|
|
[Tooltip( "The axis around which the circular drive will rotate in local space" )]
|
|
public Axis_t axisOfRotation = Axis_t.XAxis;
|
|
|
|
[Tooltip( "Child GameObject which has the Collider component to initiate interaction, only needs to be set if there is more than one Collider child" )]
|
|
public Collider childCollider = null;
|
|
|
|
[Tooltip( "A LinearMapping component to drive, if not specified one will be dynamically added to this GameObject" )]
|
|
public LinearMapping linearMapping;
|
|
|
|
[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" )]
|
|
public bool hoverLock = false;
|
|
|
|
[HeaderAttribute( "Limited Rotation" )]
|
|
[Tooltip( "If true, the rotation will be limited to [minAngle, maxAngle], if false, the rotation is unlimited" )]
|
|
public bool limited = false;
|
|
public Vector2 frozenDistanceMinMaxThreshold = new Vector2( 0.1f, 0.2f );
|
|
public UnityEvent onFrozenDistanceThreshold;
|
|
|
|
[HeaderAttribute( "Limited Rotation Min" )]
|
|
[Tooltip( "If limited is true, the specifies the lower limit, otherwise value is unused" )]
|
|
public float minAngle = -45.0f;
|
|
[Tooltip( "If limited, set whether drive will freeze its angle when the min angle is reached" )]
|
|
public bool freezeOnMin = false;
|
|
[Tooltip( "If limited, event invoked when minAngle is reached" )]
|
|
public UnityEvent onMinAngle;
|
|
|
|
[HeaderAttribute( "Limited Rotation Max" )]
|
|
[Tooltip( "If limited is true, the specifies the upper limit, otherwise value is unused" )]
|
|
public float maxAngle = 45.0f;
|
|
[Tooltip( "If limited, set whether drive will freeze its angle when the max angle is reached" )]
|
|
public bool freezeOnMax = false;
|
|
[Tooltip( "If limited, event invoked when maxAngle is reached" )]
|
|
public UnityEvent onMaxAngle;
|
|
|
|
[Tooltip( "If limited is true, this forces the starting angle to be startAngle, clamped to [minAngle, maxAngle]" )]
|
|
public bool forceStart = false;
|
|
[Tooltip( "If limited is true and forceStart is true, the starting angle will be this, clamped to [minAngle, maxAngle]" )]
|
|
public float startAngle = 0.0f;
|
|
|
|
[Tooltip( "If true, the transform of the GameObject this component is on will be rotated accordingly" )]
|
|
public bool rotateGameObject = true;
|
|
|
|
[Tooltip( "If true, the path of the Hand (red) and the projected value (green) will be drawn" )]
|
|
public bool debugPath = false;
|
|
[Tooltip( "If debugPath is true, this is the maximum number of GameObjects to create to draw the path" )]
|
|
public int dbgPathLimit = 50;
|
|
|
|
[Tooltip( "If not null, the TextMesh will display the linear value and the angular value of this circular drive" )]
|
|
public TextMesh debugText = null;
|
|
|
|
[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" )]
|
|
public float outAngle;
|
|
|
|
private Quaternion start;
|
|
|
|
private Vector3 worldPlaneNormal = new Vector3( 1.0f, 0.0f, 0.0f );
|
|
private Vector3 localPlaneNormal = new Vector3( 1.0f, 0.0f, 0.0f );
|
|
|
|
private Vector3 lastHandProjected;
|
|
|
|
private Color red = new Color( 1.0f, 0.0f, 0.0f );
|
|
private Color green = new Color( 0.0f, 1.0f, 0.0f );
|
|
|
|
private GameObject[] dbgHandObjects;
|
|
private GameObject[] dbgProjObjects;
|
|
private GameObject dbgObjectsParent;
|
|
private int dbgObjectCount = 0;
|
|
private int dbgObjectIndex = 0;
|
|
|
|
private bool driving = false;
|
|
|
|
// If the drive is limited as is at min/max, angles greater than this are ignored
|
|
private float minMaxAngularThreshold = 1.0f;
|
|
|
|
private bool frozen = false;
|
|
private float frozenAngle = 0.0f;
|
|
private Vector3 frozenHandWorldPos = new Vector3( 0.0f, 0.0f, 0.0f );
|
|
private Vector2 frozenSqDistanceMinMaxThreshold = new Vector2( 0.0f, 0.0f );
|
|
|
|
private Hand handHoverLocked = null;
|
|
|
|
private Interactable interactable;
|
|
|
|
//-------------------------------------------------
|
|
private void Freeze( Hand hand )
|
|
{
|
|
frozen = true;
|
|
frozenAngle = outAngle;
|
|
frozenHandWorldPos = hand.hoverSphereTransform.position;
|
|
frozenSqDistanceMinMaxThreshold.x = frozenDistanceMinMaxThreshold.x * frozenDistanceMinMaxThreshold.x;
|
|
frozenSqDistanceMinMaxThreshold.y = frozenDistanceMinMaxThreshold.y * frozenDistanceMinMaxThreshold.y;
|
|
}
|
|
|
|
|
|
//-------------------------------------------------
|
|
private void UnFreeze()
|
|
{
|
|
frozen = false;
|
|
frozenHandWorldPos.Set( 0.0f, 0.0f, 0.0f );
|
|
}
|
|
|
|
private void Awake()
|
|
{
|
|
interactable = this.GetComponent<Interactable>();
|
|
}
|
|
|
|
//-------------------------------------------------
|
|
private void Start()
|
|
{
|
|
if ( childCollider == null )
|
|
{
|
|
childCollider = GetComponentInChildren<Collider>();
|
|
}
|
|
|
|
if ( linearMapping == null )
|
|
{
|
|
linearMapping = GetComponent<LinearMapping>();
|
|
}
|
|
|
|
if ( linearMapping == null )
|
|
{
|
|
linearMapping = gameObject.AddComponent<LinearMapping>();
|
|
}
|
|
|
|
worldPlaneNormal = new Vector3( 0.0f, 0.0f, 0.0f );
|
|
worldPlaneNormal[(int)axisOfRotation] = 1.0f;
|
|
|
|
localPlaneNormal = worldPlaneNormal;
|
|
|
|
if ( transform.parent )
|
|
{
|
|
worldPlaneNormal = transform.parent.localToWorldMatrix.MultiplyVector( worldPlaneNormal ).normalized;
|
|
}
|
|
|
|
if ( limited )
|
|
{
|
|
start = Quaternion.identity;
|
|
outAngle = transform.localEulerAngles[(int)axisOfRotation];
|
|
|
|
if ( forceStart )
|
|
{
|
|
outAngle = Mathf.Clamp( startAngle, minAngle, maxAngle );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
start = Quaternion.AngleAxis( transform.localEulerAngles[(int)axisOfRotation], localPlaneNormal );
|
|
outAngle = 0.0f;
|
|
}
|
|
|
|
if ( debugText )
|
|
{
|
|
debugText.alignment = TextAlignment.Left;
|
|
debugText.anchor = TextAnchor.UpperLeft;
|
|
}
|
|
|
|
UpdateAll();
|
|
}
|
|
|
|
|
|
//-------------------------------------------------
|
|
void OnDisable()
|
|
{
|
|
if ( handHoverLocked )
|
|
{
|
|
handHoverLocked.HideGrabHint();
|
|
handHoverLocked.HoverUnlock(interactable);
|
|
handHoverLocked = null;
|
|
}
|
|
}
|
|
|
|
|
|
//-------------------------------------------------
|
|
private IEnumerator HapticPulses( Hand hand, float flMagnitude, int nCount )
|
|
{
|
|
if ( hand != null )
|
|
{
|
|
int nRangeMax = (int)Util.RemapNumberClamped( flMagnitude, 0.0f, 1.0f, 100.0f, 900.0f );
|
|
nCount = Mathf.Clamp( nCount, 1, 10 );
|
|
|
|
//float hapticDuration = nRangeMax * nCount;
|
|
|
|
//hand.TriggerHapticPulse(hapticDuration, nRangeMax, flMagnitude);
|
|
|
|
for ( ushort i = 0; i < nCount; ++i )
|
|
{
|
|
ushort duration = (ushort)Random.Range( 100, nRangeMax );
|
|
hand.TriggerHapticPulse( duration );
|
|
yield return new WaitForSeconds( .01f );
|
|
}
|
|
}
|
|
}
|
|
|
|
|
|
//-------------------------------------------------
|
|
private void OnHandHoverBegin( Hand hand )
|
|
{
|
|
hand.ShowGrabHint();
|
|
}
|
|
|
|
|
|
//-------------------------------------------------
|
|
private void OnHandHoverEnd( Hand hand )
|
|
{
|
|
hand.HideGrabHint();
|
|
|
|
if ( driving && hand )
|
|
{
|
|
//hand.TriggerHapticPulse() //todo: fix
|
|
StartCoroutine( HapticPulses( hand, 1.0f, 10 ) );
|
|
}
|
|
|
|
driving = false;
|
|
handHoverLocked = null;
|
|
}
|
|
|
|
private GrabTypes grabbedWithType;
|
|
//-------------------------------------------------
|
|
private void HandHoverUpdate( Hand hand )
|
|
{
|
|
GrabTypes startingGrabType = hand.GetGrabStarting();
|
|
bool isGrabEnding = hand.IsGrabbingWithType(grabbedWithType) == false;
|
|
|
|
if (grabbedWithType == GrabTypes.None && startingGrabType != GrabTypes.None)
|
|
{
|
|
grabbedWithType = startingGrabType;
|
|
// Trigger was just pressed
|
|
lastHandProjected = ComputeToTransformProjected( hand.hoverSphereTransform );
|
|
|
|
if ( hoverLock )
|
|
{
|
|
hand.HoverLock(interactable);
|
|
handHoverLocked = hand;
|
|
}
|
|
|
|
driving = true;
|
|
|
|
ComputeAngle( hand );
|
|
UpdateAll();
|
|
|
|
hand.HideGrabHint();
|
|
}
|
|
else if (grabbedWithType != GrabTypes.None && isGrabEnding)
|
|
{
|
|
// Trigger was just released
|
|
if ( hoverLock )
|
|
{
|
|
hand.HoverUnlock(interactable);
|
|
handHoverLocked = null;
|
|
}
|
|
|
|
driving = false;
|
|
grabbedWithType = GrabTypes.None;
|
|
}
|
|
|
|
if ( driving && isGrabEnding == false && hand.hoveringInteractable == this.interactable )
|
|
{
|
|
ComputeAngle( hand );
|
|
UpdateAll();
|
|
}
|
|
}
|
|
|
|
|
|
//-------------------------------------------------
|
|
private Vector3 ComputeToTransformProjected( Transform xForm )
|
|
{
|
|
Vector3 toTransform = ( xForm.position - transform.position ).normalized;
|
|
Vector3 toTransformProjected = new Vector3( 0.0f, 0.0f, 0.0f );
|
|
|
|
// Need a non-zero distance from the hand to the center of the CircularDrive
|
|
if ( toTransform.sqrMagnitude > 0.0f )
|
|
{
|
|
toTransformProjected = Vector3.ProjectOnPlane( toTransform, worldPlaneNormal ).normalized;
|
|
}
|
|
else
|
|
{
|
|
Debug.LogFormat("<b>[SteamVR Interaction]</b> The collider needs to be a minimum distance away from the CircularDrive GameObject {0}", gameObject.ToString() );
|
|
Debug.Assert( false, string.Format("<b>[SteamVR Interaction]</b> The collider needs to be a minimum distance away from the CircularDrive GameObject {0}", gameObject.ToString() ) );
|
|
}
|
|
|
|
if ( debugPath && dbgPathLimit > 0 )
|
|
{
|
|
DrawDebugPath( xForm, toTransformProjected );
|
|
}
|
|
|
|
return toTransformProjected;
|
|
}
|
|
|
|
|
|
//-------------------------------------------------
|
|
private void DrawDebugPath( Transform xForm, Vector3 toTransformProjected )
|
|
{
|
|
if ( dbgObjectCount == 0 )
|
|
{
|
|
dbgObjectsParent = new GameObject( "Circular Drive Debug" );
|
|
dbgHandObjects = new GameObject[dbgPathLimit];
|
|
dbgProjObjects = new GameObject[dbgPathLimit];
|
|
dbgObjectCount = dbgPathLimit;
|
|
dbgObjectIndex = 0;
|
|
}
|
|
|
|
//Actual path
|
|
GameObject gSphere = null;
|
|
|
|
if ( dbgHandObjects[dbgObjectIndex] )
|
|
{
|
|
gSphere = dbgHandObjects[dbgObjectIndex];
|
|
}
|
|
else
|
|
{
|
|
gSphere = GameObject.CreatePrimitive( PrimitiveType.Sphere );
|
|
gSphere.transform.SetParent( dbgObjectsParent.transform );
|
|
dbgHandObjects[dbgObjectIndex] = gSphere;
|
|
}
|
|
|
|
gSphere.name = string.Format( "actual_{0}", (int)( ( 1.0f - red.r ) * 10.0f ) );
|
|
gSphere.transform.position = xForm.position;
|
|
gSphere.transform.rotation = Quaternion.Euler( 0.0f, 0.0f, 0.0f );
|
|
gSphere.transform.localScale = new Vector3( 0.004f, 0.004f, 0.004f );
|
|
gSphere.gameObject.GetComponent<Renderer>().material.color = red;
|
|
|
|
if ( red.r > 0.1f )
|
|
{
|
|
red.r -= 0.1f;
|
|
}
|
|
else
|
|
{
|
|
red.r = 1.0f;
|
|
}
|
|
|
|
//Projected path
|
|
gSphere = null;
|
|
|
|
if ( dbgProjObjects[dbgObjectIndex] )
|
|
{
|
|
gSphere = dbgProjObjects[dbgObjectIndex];
|
|
}
|
|
else
|
|
{
|
|
gSphere = GameObject.CreatePrimitive( PrimitiveType.Sphere );
|
|
gSphere.transform.SetParent( dbgObjectsParent.transform );
|
|
dbgProjObjects[dbgObjectIndex] = gSphere;
|
|
}
|
|
|
|
gSphere.name = string.Format( "projed_{0}", (int)( ( 1.0f - green.g ) * 10.0f ) );
|
|
gSphere.transform.position = transform.position + toTransformProjected * 0.25f;
|
|
gSphere.transform.rotation = Quaternion.Euler( 0.0f, 0.0f, 0.0f );
|
|
gSphere.transform.localScale = new Vector3( 0.004f, 0.004f, 0.004f );
|
|
gSphere.gameObject.GetComponent<Renderer>().material.color = green;
|
|
|
|
if ( green.g > 0.1f )
|
|
{
|
|
green.g -= 0.1f;
|
|
}
|
|
else
|
|
{
|
|
green.g = 1.0f;
|
|
}
|
|
|
|
dbgObjectIndex = ( dbgObjectIndex + 1 ) % dbgObjectCount;
|
|
}
|
|
|
|
|
|
//-------------------------------------------------
|
|
// Updates the LinearMapping value from the angle
|
|
//-------------------------------------------------
|
|
private void UpdateLinearMapping()
|
|
{
|
|
if ( limited )
|
|
{
|
|
// Map it to a [0, 1] value
|
|
linearMapping.value = ( outAngle - minAngle ) / ( maxAngle - minAngle );
|
|
}
|
|
else
|
|
{
|
|
// Normalize to [0, 1] based on 360 degree windings
|
|
float flTmp = outAngle / 360.0f;
|
|
linearMapping.value = flTmp - Mathf.Floor( flTmp );
|
|
}
|
|
|
|
UpdateDebugText();
|
|
}
|
|
|
|
|
|
//-------------------------------------------------
|
|
// Updates the LinearMapping value from the angle
|
|
//-------------------------------------------------
|
|
private void UpdateGameObject()
|
|
{
|
|
if ( rotateGameObject )
|
|
{
|
|
transform.localRotation = start * Quaternion.AngleAxis( outAngle, localPlaneNormal );
|
|
}
|
|
}
|
|
|
|
|
|
//-------------------------------------------------
|
|
// Updates the Debug TextMesh with the linear mapping value and the angle
|
|
//-------------------------------------------------
|
|
private void UpdateDebugText()
|
|
{
|
|
if ( debugText )
|
|
{
|
|
debugText.text = string.Format( "Linear: {0}\nAngle: {1}\n", linearMapping.value, outAngle );
|
|
}
|
|
}
|
|
|
|
|
|
//-------------------------------------------------
|
|
// Updates the Debug TextMesh with the linear mapping value and the angle
|
|
//-------------------------------------------------
|
|
private void UpdateAll()
|
|
{
|
|
UpdateLinearMapping();
|
|
UpdateGameObject();
|
|
UpdateDebugText();
|
|
}
|
|
|
|
|
|
//-------------------------------------------------
|
|
// Computes the angle to rotate the game object based on the change in the transform
|
|
//-------------------------------------------------
|
|
private void ComputeAngle( Hand hand )
|
|
{
|
|
Vector3 toHandProjected = ComputeToTransformProjected( hand.hoverSphereTransform );
|
|
|
|
if ( !toHandProjected.Equals( lastHandProjected ) )
|
|
{
|
|
float absAngleDelta = Vector3.Angle( lastHandProjected, toHandProjected );
|
|
|
|
if ( absAngleDelta > 0.0f )
|
|
{
|
|
if ( frozen )
|
|
{
|
|
float frozenSqDist = ( hand.hoverSphereTransform.position - frozenHandWorldPos ).sqrMagnitude;
|
|
if ( frozenSqDist > frozenSqDistanceMinMaxThreshold.x )
|
|
{
|
|
outAngle = frozenAngle + Random.Range( -1.0f, 1.0f );
|
|
|
|
float magnitude = Util.RemapNumberClamped( frozenSqDist, frozenSqDistanceMinMaxThreshold.x, frozenSqDistanceMinMaxThreshold.y, 0.0f, 1.0f );
|
|
if ( magnitude > 0 )
|
|
{
|
|
StartCoroutine( HapticPulses( hand, magnitude, 10 ) );
|
|
}
|
|
else
|
|
{
|
|
StartCoroutine( HapticPulses( hand, 0.5f, 10 ) );
|
|
}
|
|
|
|
if ( frozenSqDist >= frozenSqDistanceMinMaxThreshold.y )
|
|
{
|
|
onFrozenDistanceThreshold.Invoke();
|
|
}
|
|
}
|
|
}
|
|
else
|
|
{
|
|
Vector3 cross = Vector3.Cross( lastHandProjected, toHandProjected ).normalized;
|
|
float dot = Vector3.Dot( worldPlaneNormal, cross );
|
|
|
|
float signedAngleDelta = absAngleDelta;
|
|
|
|
if ( dot < 0.0f )
|
|
{
|
|
signedAngleDelta = -signedAngleDelta;
|
|
}
|
|
|
|
if ( limited )
|
|
{
|
|
float angleTmp = Mathf.Clamp( outAngle + signedAngleDelta, minAngle, maxAngle );
|
|
|
|
if ( outAngle == minAngle )
|
|
{
|
|
if ( angleTmp > minAngle && absAngleDelta < minMaxAngularThreshold )
|
|
{
|
|
outAngle = angleTmp;
|
|
lastHandProjected = toHandProjected;
|
|
}
|
|
}
|
|
else if ( outAngle == maxAngle )
|
|
{
|
|
if ( angleTmp < maxAngle && absAngleDelta < minMaxAngularThreshold )
|
|
{
|
|
outAngle = angleTmp;
|
|
lastHandProjected = toHandProjected;
|
|
}
|
|
}
|
|
else if ( angleTmp == minAngle )
|
|
{
|
|
outAngle = angleTmp;
|
|
lastHandProjected = toHandProjected;
|
|
onMinAngle.Invoke();
|
|
if ( freezeOnMin )
|
|
{
|
|
Freeze( hand );
|
|
}
|
|
}
|
|
else if ( angleTmp == maxAngle )
|
|
{
|
|
outAngle = angleTmp;
|
|
lastHandProjected = toHandProjected;
|
|
onMaxAngle.Invoke();
|
|
if ( freezeOnMax )
|
|
{
|
|
Freeze( hand );
|
|
}
|
|
}
|
|
else
|
|
{
|
|
outAngle = angleTmp;
|
|
lastHandProjected = toHandProjected;
|
|
}
|
|
}
|
|
else
|
|
{
|
|
outAngle += signedAngleDelta;
|
|
lastHandProjected = toHandProjected;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|
|
}
|