// Button|Controls3D|100020 namespace VRTK { using UnityEngine; /// /// Event Payload /// /// this object /// public delegate void Button3DEventHandler(object sender, Control3DEventArgs e); /// /// 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. /// /// /// The script will instantiate the required Rigidbody and ConstantForce components automatically in case they do not exist yet. /// /// /// `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. /// [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 { /// /// 3D Control Button Directions /// public enum ButtonDirection { /// /// Attempt to auto detect the axis. /// autodetect, /// /// The world x direction. /// x, /// /// The world y direction. /// y, /// /// The world z direction. /// z, /// /// The world negative x direction. /// negX, /// /// The world negative y direction. /// negY, /// /// The world negative z direction. /// 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; /// /// Emitted when the 3D Button has reached its activation distance. /// public event Button3DEventHandler Pushed; /// /// Emitted when the 3D Button's position has become less than activation distance after being pressed. /// 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() == null) { gameObject.AddComponent(); } } protected virtual void SetupRigidbody() { buttonRigidbody = GetComponent(); if (buttonRigidbody == null) { buttonRigidbody = gameObject.AddComponent(); } buttonRigidbody.isKinematic = false; buttonRigidbody.useGravity = false; } protected virtual void SetupConstantForce() { buttonForce = GetComponent(); if (buttonForce == null) { buttonForce = gameObject.AddComponent(); } } protected virtual void SetupConnectedTo() { if (connectedTo != null) { Rigidbody connectedToRigidbody = connectedTo.GetComponent(); if (connectedToRigidbody == null) { connectedToRigidbody = connectedTo.AddComponent(); } connectedToRigidbody.useGravity = false; } } protected override void InitRequiredComponents() { restingPosition = transform.position; SetupCollider(); SetupRigidbody(); SetupConstantForce(); SetupConnectedTo(); } protected virtual void DetectJointSetup() { buttonJoint = GetComponent(); 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(); if (recreate) { buttonJoint.connectedBody = oldBody; buttonJoint.anchor = oldAnchor; buttonJoint.axis = oldAxis; } buttonJoint.connectedBody = (connectedTo != null ? connectedTo.GetComponent() : 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); } } }