// Button|Controls3D|100020
|
|
namespace VRTK
|
|
{
|
|
using UnityEngine;
|
|
|
|
/// <summary>
|
|
/// Event Payload
|
|
/// </summary>
|
|
/// <param name="sender">this object</param>
|
|
/// <param name="e"><see cref="Control3DEventArgs"/></param>
|
|
public delegate void Button3DEventHandler(object sender, Control3DEventArgs e);
|
|
|
|
/// <summary>
|
|
/// Attaching the script to a game object will allow the user to interact with it as if it were a push button. The direction into which the button should be pushable can be freely set and auto-detection is supported. Since this is physics-based there needs to be empty space in the push direction so that the button can move.
|
|
/// </summary>
|
|
/// <remarks>
|
|
/// The script will instantiate the required Rigidbody and ConstantForce components automatically in case they do not exist yet.
|
|
/// </remarks>
|
|
/// <example>
|
|
/// `VRTK/Examples/025_Controls_Overview` shows a collection of pressable buttons that are interacted with by activating the rigidbody on the controller by pressing the grab button without grabbing an object.
|
|
/// </example>
|
|
[AddComponentMenu("VRTK/Scripts/Controls/3D/VRTK_Button")]
|
|
[System.Obsolete("`VRTK.VRTK_Button` has been replaced with `VRTK.Controllables.PhysicsBased.VRTK_PhysicsPusher`. This script will be removed in a future version of VRTK.")]
|
|
public class VRTK_Button : VRTK_Control
|
|
{
|
|
/// <summary>
|
|
/// 3D Control Button Directions
|
|
/// </summary>
|
|
public enum ButtonDirection
|
|
{
|
|
/// <summary>
|
|
/// Attempt to auto detect the axis.
|
|
/// </summary>
|
|
autodetect,
|
|
/// <summary>
|
|
/// The world x direction.
|
|
/// </summary>
|
|
x,
|
|
/// <summary>
|
|
/// The world y direction.
|
|
/// </summary>
|
|
y,
|
|
/// <summary>
|
|
/// The world z direction.
|
|
/// </summary>
|
|
z,
|
|
/// <summary>
|
|
/// The world negative x direction.
|
|
/// </summary>
|
|
negX,
|
|
/// <summary>
|
|
/// The world negative y direction.
|
|
/// </summary>
|
|
negY,
|
|
/// <summary>
|
|
/// The world negative z direction.
|
|
/// </summary>
|
|
negZ
|
|
}
|
|
|
|
[Tooltip("An optional game object to which the button will be connected. If the game object moves the button will follow along.")]
|
|
public GameObject connectedTo;
|
|
[Tooltip("The axis on which the button should move. All other axis will be frozen.")]
|
|
public ButtonDirection direction = ButtonDirection.autodetect;
|
|
[Tooltip("The local distance the button needs to be pushed until a push event is triggered.")]
|
|
public float activationDistance = 1.0f;
|
|
[Tooltip("The amount of force needed to push the button down as well as the speed with which it will go back into its original position.")]
|
|
public float buttonStrength = 5.0f;
|
|
|
|
/// <summary>
|
|
/// Emitted when the 3D Button has reached its activation distance.
|
|
/// </summary>
|
|
public event Button3DEventHandler Pushed;
|
|
|
|
/// <summary>
|
|
/// Emitted when the 3D Button's position has become less than activation distance after being pressed.
|
|
/// </summary>
|
|
public event Button3DEventHandler Released;
|
|
|
|
protected const float MAX_AUTODETECT_ACTIVATION_LENGTH = 4f; // full hight of button
|
|
protected ButtonDirection finalDirection;
|
|
protected Vector3 restingPosition;
|
|
protected Vector3 activationDir;
|
|
protected Rigidbody buttonRigidbody;
|
|
protected ConfigurableJoint buttonJoint;
|
|
protected ConstantForce buttonForce;
|
|
protected int forceCount = 0;
|
|
|
|
public virtual void OnPushed(Control3DEventArgs e)
|
|
{
|
|
if (Pushed != null)
|
|
{
|
|
Pushed(this, e);
|
|
}
|
|
}
|
|
|
|
public virtual void OnReleased(Control3DEventArgs e)
|
|
{
|
|
if (Released != null)
|
|
{
|
|
Released(this, e);
|
|
}
|
|
}
|
|
|
|
protected override void OnDrawGizmos()
|
|
{
|
|
base.OnDrawGizmos();
|
|
if (!enabled || !setupSuccessful)
|
|
{
|
|
return;
|
|
}
|
|
|
|
// visualize activation distance
|
|
Gizmos.DrawLine(bounds.center, bounds.center + activationDir);
|
|
}
|
|
|
|
protected virtual void SetupCollider()
|
|
{
|
|
if (GetComponent<Collider>() == null)
|
|
{
|
|
gameObject.AddComponent<BoxCollider>();
|
|
}
|
|
}
|
|
|
|
protected virtual void SetupRigidbody()
|
|
{
|
|
buttonRigidbody = GetComponent<Rigidbody>();
|
|
if (buttonRigidbody == null)
|
|
{
|
|
buttonRigidbody = gameObject.AddComponent<Rigidbody>();
|
|
}
|
|
buttonRigidbody.isKinematic = false;
|
|
buttonRigidbody.useGravity = false;
|
|
}
|
|
|
|
protected virtual void SetupConstantForce()
|
|
{
|
|
buttonForce = GetComponent<ConstantForce>();
|
|
if (buttonForce == null)
|
|
{
|
|
buttonForce = gameObject.AddComponent<ConstantForce>();
|
|
}
|
|
}
|
|
|
|
protected virtual void SetupConnectedTo()
|
|
{
|
|
if (connectedTo != null)
|
|
{
|
|
Rigidbody connectedToRigidbody = connectedTo.GetComponent<Rigidbody>();
|
|
if (connectedToRigidbody == null)
|
|
{
|
|
connectedToRigidbody = connectedTo.AddComponent<Rigidbody>();
|
|
}
|
|
connectedToRigidbody.useGravity = false;
|
|
}
|
|
}
|
|
|
|
protected override void InitRequiredComponents()
|
|
{
|
|
restingPosition = transform.position;
|
|
|
|
SetupCollider();
|
|
SetupRigidbody();
|
|
SetupConstantForce();
|
|
SetupConnectedTo();
|
|
}
|
|
|
|
protected virtual void DetectJointSetup()
|
|
{
|
|
buttonJoint = GetComponent<ConfigurableJoint>();
|
|
bool recreate = false;
|
|
Rigidbody oldBody = null;
|
|
Vector3 oldAnchor = Vector3.zero;
|
|
Vector3 oldAxis = Vector3.zero;
|
|
|
|
if (buttonJoint != null)
|
|
{
|
|
// save old values, needs to be recreated
|
|
oldBody = buttonJoint.connectedBody;
|
|
oldAnchor = buttonJoint.anchor;
|
|
oldAxis = buttonJoint.axis;
|
|
DestroyImmediate(buttonJoint);
|
|
recreate = true;
|
|
}
|
|
|
|
// since limit applies to both directions object needs to be moved halfway to activation before adding joint
|
|
transform.position = transform.position + ((activationDir.normalized * activationDistance) * 0.5f);
|
|
buttonJoint = gameObject.AddComponent<ConfigurableJoint>();
|
|
|
|
if (recreate)
|
|
{
|
|
buttonJoint.connectedBody = oldBody;
|
|
buttonJoint.anchor = oldAnchor;
|
|
buttonJoint.axis = oldAxis;
|
|
}
|
|
|
|
buttonJoint.connectedBody = (connectedTo != null ? connectedTo.GetComponent<Rigidbody>() : buttonJoint.connectedBody);
|
|
}
|
|
|
|
protected virtual void DetectJointLimitsSetup()
|
|
{
|
|
SoftJointLimit buttonJointLimits = new SoftJointLimit();
|
|
buttonJointLimits.limit = activationDistance * 0.501f; // set limit to half (since it applies to both directions) and a tiny bit larger since otherwise activation distance might be missed
|
|
buttonJoint.linearLimit = buttonJointLimits;
|
|
|
|
buttonJoint.angularXMotion = ConfigurableJointMotion.Locked;
|
|
buttonJoint.angularYMotion = ConfigurableJointMotion.Locked;
|
|
buttonJoint.angularZMotion = ConfigurableJointMotion.Locked;
|
|
buttonJoint.xMotion = ConfigurableJointMotion.Locked;
|
|
buttonJoint.yMotion = ConfigurableJointMotion.Locked;
|
|
buttonJoint.zMotion = ConfigurableJointMotion.Locked;
|
|
}
|
|
|
|
protected virtual void DetectJointDirectionSetup()
|
|
{
|
|
switch (finalDirection)
|
|
{
|
|
case ButtonDirection.x:
|
|
case ButtonDirection.negX:
|
|
if (Mathf.RoundToInt(Mathf.Abs(transform.right.x)) == 1)
|
|
{
|
|
buttonJoint.xMotion = ConfigurableJointMotion.Limited;
|
|
}
|
|
else if (Mathf.RoundToInt(Mathf.Abs(transform.up.x)) == 1)
|
|
{
|
|
buttonJoint.yMotion = ConfigurableJointMotion.Limited;
|
|
}
|
|
else if (Mathf.RoundToInt(Mathf.Abs(transform.forward.x)) == 1)
|
|
{
|
|
buttonJoint.zMotion = ConfigurableJointMotion.Limited;
|
|
}
|
|
break;
|
|
case ButtonDirection.y:
|
|
case ButtonDirection.negY:
|
|
if (Mathf.RoundToInt(Mathf.Abs(transform.right.y)) == 1)
|
|
{
|
|
buttonJoint.xMotion = ConfigurableJointMotion.Limited;
|
|
}
|
|
else if (Mathf.RoundToInt(Mathf.Abs(transform.up.y)) == 1)
|
|
{
|
|
buttonJoint.yMotion = ConfigurableJointMotion.Limited;
|
|
}
|
|
else if (Mathf.RoundToInt(Mathf.Abs(transform.forward.y)) == 1)
|
|
{
|
|
buttonJoint.zMotion = ConfigurableJointMotion.Limited;
|
|
}
|
|
break;
|
|
case ButtonDirection.z:
|
|
case ButtonDirection.negZ:
|
|
if (Mathf.RoundToInt(Mathf.Abs(transform.right.z)) == 1)
|
|
{
|
|
buttonJoint.xMotion = ConfigurableJointMotion.Limited;
|
|
}
|
|
else if (Mathf.RoundToInt(Mathf.Abs(transform.up.z)) == 1)
|
|
{
|
|
buttonJoint.yMotion = ConfigurableJointMotion.Limited;
|
|
}
|
|
else if (Mathf.RoundToInt(Mathf.Abs(transform.forward.z)) == 1)
|
|
{
|
|
buttonJoint.zMotion = ConfigurableJointMotion.Limited;
|
|
}
|
|
break;
|
|
}
|
|
}
|
|
|
|
protected override bool DetectSetup()
|
|
{
|
|
finalDirection = (direction == ButtonDirection.autodetect ? DetectDirection() : direction);
|
|
if (finalDirection == ButtonDirection.autodetect)
|
|
{
|
|
activationDir = Vector3.zero;
|
|
return false;
|
|
}
|
|
|
|
activationDir = (direction != ButtonDirection.autodetect ? CalculateActivationDir() : activationDir);
|
|
|
|
if (buttonForce != null)
|
|
{
|
|
buttonForce.force = GetForceVector();
|
|
}
|
|
|
|
if (Application.isPlaying)
|
|
{
|
|
DetectJointSetup();
|
|
DetectJointLimitsSetup();
|
|
DetectJointDirectionSetup();
|
|
}
|
|
|
|
return true;
|
|
}
|
|
|
|
protected override ControlValueRange RegisterValueRange()
|
|
{
|
|
return new ControlValueRange()
|
|
{
|
|
controlMin = 0,
|
|
controlMax = 1
|
|
};
|
|
}
|
|
|
|
protected override void HandleUpdate()
|
|
{
|
|
// trigger events
|
|
float oldState = value;
|
|
if (ReachedActivationDistance())
|
|
{
|
|
if (oldState == 0)
|
|
{
|
|
value = 1;
|
|
OnPushed(SetControlEvent());
|
|
}
|
|
}
|
|
else
|
|
{
|
|
if (oldState == 1)
|
|
{
|
|
value = 0;
|
|
OnReleased(SetControlEvent());
|
|
}
|
|
}
|
|
}
|
|
|
|
protected virtual void FixedUpdate()
|
|
{
|
|
// update reference position if no force is acting on the button to support scenarios where the button is moved at runtime with a connected body
|
|
if (forceCount == 0 && buttonJoint.connectedBody != null)
|
|
{
|
|
restingPosition = transform.position;
|
|
}
|
|
}
|
|
|
|
protected virtual void OnCollisionExit(Collision collision)
|
|
{
|
|
// TODO: this will not always be triggered for some reason, we probably need some "healing"
|
|
forceCount -= 1;
|
|
}
|
|
|
|
protected virtual void OnCollisionEnter(Collision collision)
|
|
{
|
|
forceCount += 1;
|
|
}
|
|
|
|
protected virtual ButtonDirection DetectDirection()
|
|
{
|
|
ButtonDirection returnDirection = ButtonDirection.autodetect;
|
|
Bounds bounds = VRTK_SharedMethods.GetBounds(transform);
|
|
|
|
// shoot rays from the center of the button to learn about surroundings
|
|
RaycastHit hitForward;
|
|
RaycastHit hitBack;
|
|
RaycastHit hitLeft;
|
|
RaycastHit hitRight;
|
|
RaycastHit hitUp;
|
|
RaycastHit hitDown;
|
|
Physics.Raycast(bounds.center, Vector3.forward, out hitForward, bounds.extents.z * MAX_AUTODETECT_ACTIVATION_LENGTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
|
|
Physics.Raycast(bounds.center, Vector3.back, out hitBack, bounds.extents.z * MAX_AUTODETECT_ACTIVATION_LENGTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
|
|
Physics.Raycast(bounds.center, Vector3.left, out hitLeft, bounds.extents.x * MAX_AUTODETECT_ACTIVATION_LENGTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
|
|
Physics.Raycast(bounds.center, Vector3.right, out hitRight, bounds.extents.x * MAX_AUTODETECT_ACTIVATION_LENGTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
|
|
Physics.Raycast(bounds.center, Vector3.up, out hitUp, bounds.extents.y * MAX_AUTODETECT_ACTIVATION_LENGTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
|
|
Physics.Raycast(bounds.center, Vector3.down, out hitDown, bounds.extents.y * MAX_AUTODETECT_ACTIVATION_LENGTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
|
|
|
|
// shortest valid ray wins
|
|
float lengthX = (hitRight.collider != null) ? hitRight.distance : float.MaxValue;
|
|
float lengthY = (hitDown.collider != null) ? hitDown.distance : float.MaxValue;
|
|
float lengthZ = (hitBack.collider != null) ? hitBack.distance : float.MaxValue;
|
|
float lengthNegX = (hitLeft.collider != null) ? hitLeft.distance : float.MaxValue;
|
|
float lengthNegY = (hitUp.collider != null) ? hitUp.distance : float.MaxValue;
|
|
float lengthNegZ = (hitForward.collider != null) ? hitForward.distance : float.MaxValue;
|
|
|
|
float extents = 0;
|
|
Vector3 hitPoint = Vector3.zero;
|
|
if (VRTK_SharedMethods.IsLowest(lengthX, new float[] { lengthY, lengthZ, lengthNegX, lengthNegY, lengthNegZ }))
|
|
{
|
|
returnDirection = ButtonDirection.negX;
|
|
hitPoint = hitRight.point;
|
|
extents = bounds.extents.x;
|
|
}
|
|
else if (VRTK_SharedMethods.IsLowest(lengthY, new float[] { lengthX, lengthZ, lengthNegX, lengthNegY, lengthNegZ }))
|
|
{
|
|
returnDirection = ButtonDirection.y;
|
|
hitPoint = hitDown.point;
|
|
extents = bounds.extents.y;
|
|
}
|
|
else if (VRTK_SharedMethods.IsLowest(lengthZ, new float[] { lengthX, lengthY, lengthNegX, lengthNegY, lengthNegZ }))
|
|
{
|
|
returnDirection = ButtonDirection.z;
|
|
hitPoint = hitBack.point;
|
|
extents = bounds.extents.z;
|
|
}
|
|
else if (VRTK_SharedMethods.IsLowest(lengthNegX, new float[] { lengthX, lengthY, lengthZ, lengthNegY, lengthNegZ }))
|
|
{
|
|
returnDirection = ButtonDirection.x;
|
|
hitPoint = hitLeft.point;
|
|
extents = bounds.extents.x;
|
|
}
|
|
else if (VRTK_SharedMethods.IsLowest(lengthNegY, new float[] { lengthX, lengthY, lengthZ, lengthNegX, lengthNegZ }))
|
|
{
|
|
returnDirection = ButtonDirection.negY;
|
|
hitPoint = hitUp.point;
|
|
extents = bounds.extents.y;
|
|
}
|
|
else if (VRTK_SharedMethods.IsLowest(lengthNegZ, new float[] { lengthX, lengthY, lengthZ, lengthNegX, lengthNegY }))
|
|
{
|
|
returnDirection = ButtonDirection.negZ;
|
|
hitPoint = hitForward.point;
|
|
extents = bounds.extents.z;
|
|
}
|
|
|
|
// determin activation distance
|
|
activationDistance = (Vector3.Distance(hitPoint, bounds.center) - extents) * 0.95f;
|
|
|
|
if (returnDirection == ButtonDirection.autodetect || activationDistance < 0.001f)
|
|
{
|
|
// auto-detection was not possible or colliding with object already
|
|
returnDirection = ButtonDirection.autodetect;
|
|
activationDistance = 0;
|
|
}
|
|
else
|
|
{
|
|
activationDir = hitPoint - bounds.center;
|
|
}
|
|
|
|
return returnDirection;
|
|
}
|
|
|
|
protected virtual Vector3 CalculateActivationDir()
|
|
{
|
|
Bounds bounds = VRTK_SharedMethods.GetBounds(transform, transform);
|
|
|
|
Vector3 buttonDirection = Vector3.zero;
|
|
float extents = 0;
|
|
switch (direction)
|
|
{
|
|
case ButtonDirection.x:
|
|
case ButtonDirection.negX:
|
|
if (Mathf.RoundToInt(Mathf.Abs(transform.right.x)) == 1)
|
|
{
|
|
buttonDirection = transform.right;
|
|
extents = bounds.extents.x;
|
|
}
|
|
else if (Mathf.RoundToInt(Mathf.Abs(transform.up.x)) == 1)
|
|
{
|
|
buttonDirection = transform.up;
|
|
extents = bounds.extents.y;
|
|
}
|
|
else if (Mathf.RoundToInt(Mathf.Abs(transform.forward.x)) == 1)
|
|
{
|
|
buttonDirection = transform.forward;
|
|
extents = bounds.extents.z;
|
|
}
|
|
buttonDirection *= (direction == ButtonDirection.x) ? -1 : 1;
|
|
break;
|
|
case ButtonDirection.y:
|
|
case ButtonDirection.negY:
|
|
if (Mathf.RoundToInt(Mathf.Abs(transform.right.y)) == 1)
|
|
{
|
|
buttonDirection = transform.right;
|
|
extents = bounds.extents.x;
|
|
}
|
|
else if (Mathf.RoundToInt(Mathf.Abs(transform.up.y)) == 1)
|
|
{
|
|
buttonDirection = transform.up;
|
|
extents = bounds.extents.y;
|
|
}
|
|
else if (Mathf.RoundToInt(Mathf.Abs(transform.forward.y)) == 1)
|
|
{
|
|
buttonDirection = transform.forward;
|
|
extents = bounds.extents.z;
|
|
}
|
|
buttonDirection *= (direction == ButtonDirection.y) ? -1 : 1;
|
|
break;
|
|
case ButtonDirection.z:
|
|
case ButtonDirection.negZ:
|
|
if (Mathf.RoundToInt(Mathf.Abs(transform.right.z)) == 1)
|
|
{
|
|
buttonDirection = transform.right;
|
|
extents = bounds.extents.x;
|
|
}
|
|
else if (Mathf.RoundToInt(Mathf.Abs(transform.up.z)) == 1)
|
|
{
|
|
buttonDirection = transform.up;
|
|
extents = bounds.extents.y;
|
|
}
|
|
else if (Mathf.RoundToInt(Mathf.Abs(transform.forward.z)) == 1)
|
|
{
|
|
buttonDirection = transform.forward;
|
|
extents = bounds.extents.z;
|
|
}
|
|
buttonDirection *= (direction == ButtonDirection.z) ? -1 : 1;
|
|
break;
|
|
}
|
|
|
|
// subtract width of button
|
|
return (buttonDirection * (extents + activationDistance));
|
|
}
|
|
|
|
protected virtual bool ReachedActivationDistance()
|
|
{
|
|
return (Vector3.Distance(transform.position, restingPosition) >= activationDistance);
|
|
}
|
|
|
|
protected virtual Vector3 GetForceVector()
|
|
{
|
|
return (-activationDir.normalized * buttonStrength);
|
|
}
|
|
}
|
|
}
|