Assignment for RMIT Mixed Reality in 2020
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

506 lines
20 KiB

  1. // Button|Controls3D|100020
  2. namespace VRTK
  3. {
  4. using UnityEngine;
  5. /// <summary>
  6. /// Event Payload
  7. /// </summary>
  8. /// <param name="sender">this object</param>
  9. /// <param name="e"><see cref="Control3DEventArgs"/></param>
  10. public delegate void Button3DEventHandler(object sender, Control3DEventArgs e);
  11. /// <summary>
  12. /// 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.
  13. /// </summary>
  14. /// <remarks>
  15. /// The script will instantiate the required Rigidbody and ConstantForce components automatically in case they do not exist yet.
  16. /// </remarks>
  17. /// <example>
  18. /// `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.
  19. /// </example>
  20. [AddComponentMenu("VRTK/Scripts/Controls/3D/VRTK_Button")]
  21. [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.")]
  22. public class VRTK_Button : VRTK_Control
  23. {
  24. /// <summary>
  25. /// 3D Control Button Directions
  26. /// </summary>
  27. public enum ButtonDirection
  28. {
  29. /// <summary>
  30. /// Attempt to auto detect the axis.
  31. /// </summary>
  32. autodetect,
  33. /// <summary>
  34. /// The world x direction.
  35. /// </summary>
  36. x,
  37. /// <summary>
  38. /// The world y direction.
  39. /// </summary>
  40. y,
  41. /// <summary>
  42. /// The world z direction.
  43. /// </summary>
  44. z,
  45. /// <summary>
  46. /// The world negative x direction.
  47. /// </summary>
  48. negX,
  49. /// <summary>
  50. /// The world negative y direction.
  51. /// </summary>
  52. negY,
  53. /// <summary>
  54. /// The world negative z direction.
  55. /// </summary>
  56. negZ
  57. }
  58. [Tooltip("An optional game object to which the button will be connected. If the game object moves the button will follow along.")]
  59. public GameObject connectedTo;
  60. [Tooltip("The axis on which the button should move. All other axis will be frozen.")]
  61. public ButtonDirection direction = ButtonDirection.autodetect;
  62. [Tooltip("The local distance the button needs to be pushed until a push event is triggered.")]
  63. public float activationDistance = 1.0f;
  64. [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.")]
  65. public float buttonStrength = 5.0f;
  66. /// <summary>
  67. /// Emitted when the 3D Button has reached its activation distance.
  68. /// </summary>
  69. public event Button3DEventHandler Pushed;
  70. /// <summary>
  71. /// Emitted when the 3D Button's position has become less than activation distance after being pressed.
  72. /// </summary>
  73. public event Button3DEventHandler Released;
  74. protected const float MAX_AUTODETECT_ACTIVATION_LENGTH = 4f; // full hight of button
  75. protected ButtonDirection finalDirection;
  76. protected Vector3 restingPosition;
  77. protected Vector3 activationDir;
  78. protected Rigidbody buttonRigidbody;
  79. protected ConfigurableJoint buttonJoint;
  80. protected ConstantForce buttonForce;
  81. protected int forceCount = 0;
  82. public virtual void OnPushed(Control3DEventArgs e)
  83. {
  84. if (Pushed != null)
  85. {
  86. Pushed(this, e);
  87. }
  88. }
  89. public virtual void OnReleased(Control3DEventArgs e)
  90. {
  91. if (Released != null)
  92. {
  93. Released(this, e);
  94. }
  95. }
  96. protected override void OnDrawGizmos()
  97. {
  98. base.OnDrawGizmos();
  99. if (!enabled || !setupSuccessful)
  100. {
  101. return;
  102. }
  103. // visualize activation distance
  104. Gizmos.DrawLine(bounds.center, bounds.center + activationDir);
  105. }
  106. protected virtual void SetupCollider()
  107. {
  108. if (GetComponent<Collider>() == null)
  109. {
  110. gameObject.AddComponent<BoxCollider>();
  111. }
  112. }
  113. protected virtual void SetupRigidbody()
  114. {
  115. buttonRigidbody = GetComponent<Rigidbody>();
  116. if (buttonRigidbody == null)
  117. {
  118. buttonRigidbody = gameObject.AddComponent<Rigidbody>();
  119. }
  120. buttonRigidbody.isKinematic = false;
  121. buttonRigidbody.useGravity = false;
  122. }
  123. protected virtual void SetupConstantForce()
  124. {
  125. buttonForce = GetComponent<ConstantForce>();
  126. if (buttonForce == null)
  127. {
  128. buttonForce = gameObject.AddComponent<ConstantForce>();
  129. }
  130. }
  131. protected virtual void SetupConnectedTo()
  132. {
  133. if (connectedTo != null)
  134. {
  135. Rigidbody connectedToRigidbody = connectedTo.GetComponent<Rigidbody>();
  136. if (connectedToRigidbody == null)
  137. {
  138. connectedToRigidbody = connectedTo.AddComponent<Rigidbody>();
  139. }
  140. connectedToRigidbody.useGravity = false;
  141. }
  142. }
  143. protected override void InitRequiredComponents()
  144. {
  145. restingPosition = transform.position;
  146. SetupCollider();
  147. SetupRigidbody();
  148. SetupConstantForce();
  149. SetupConnectedTo();
  150. }
  151. protected virtual void DetectJointSetup()
  152. {
  153. buttonJoint = GetComponent<ConfigurableJoint>();
  154. bool recreate = false;
  155. Rigidbody oldBody = null;
  156. Vector3 oldAnchor = Vector3.zero;
  157. Vector3 oldAxis = Vector3.zero;
  158. if (buttonJoint != null)
  159. {
  160. // save old values, needs to be recreated
  161. oldBody = buttonJoint.connectedBody;
  162. oldAnchor = buttonJoint.anchor;
  163. oldAxis = buttonJoint.axis;
  164. DestroyImmediate(buttonJoint);
  165. recreate = true;
  166. }
  167. // since limit applies to both directions object needs to be moved halfway to activation before adding joint
  168. transform.position = transform.position + ((activationDir.normalized * activationDistance) * 0.5f);
  169. buttonJoint = gameObject.AddComponent<ConfigurableJoint>();
  170. if (recreate)
  171. {
  172. buttonJoint.connectedBody = oldBody;
  173. buttonJoint.anchor = oldAnchor;
  174. buttonJoint.axis = oldAxis;
  175. }
  176. buttonJoint.connectedBody = (connectedTo != null ? connectedTo.GetComponent<Rigidbody>() : buttonJoint.connectedBody);
  177. }
  178. protected virtual void DetectJointLimitsSetup()
  179. {
  180. SoftJointLimit buttonJointLimits = new SoftJointLimit();
  181. 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
  182. buttonJoint.linearLimit = buttonJointLimits;
  183. buttonJoint.angularXMotion = ConfigurableJointMotion.Locked;
  184. buttonJoint.angularYMotion = ConfigurableJointMotion.Locked;
  185. buttonJoint.angularZMotion = ConfigurableJointMotion.Locked;
  186. buttonJoint.xMotion = ConfigurableJointMotion.Locked;
  187. buttonJoint.yMotion = ConfigurableJointMotion.Locked;
  188. buttonJoint.zMotion = ConfigurableJointMotion.Locked;
  189. }
  190. protected virtual void DetectJointDirectionSetup()
  191. {
  192. switch (finalDirection)
  193. {
  194. case ButtonDirection.x:
  195. case ButtonDirection.negX:
  196. if (Mathf.RoundToInt(Mathf.Abs(transform.right.x)) == 1)
  197. {
  198. buttonJoint.xMotion = ConfigurableJointMotion.Limited;
  199. }
  200. else if (Mathf.RoundToInt(Mathf.Abs(transform.up.x)) == 1)
  201. {
  202. buttonJoint.yMotion = ConfigurableJointMotion.Limited;
  203. }
  204. else if (Mathf.RoundToInt(Mathf.Abs(transform.forward.x)) == 1)
  205. {
  206. buttonJoint.zMotion = ConfigurableJointMotion.Limited;
  207. }
  208. break;
  209. case ButtonDirection.y:
  210. case ButtonDirection.negY:
  211. if (Mathf.RoundToInt(Mathf.Abs(transform.right.y)) == 1)
  212. {
  213. buttonJoint.xMotion = ConfigurableJointMotion.Limited;
  214. }
  215. else if (Mathf.RoundToInt(Mathf.Abs(transform.up.y)) == 1)
  216. {
  217. buttonJoint.yMotion = ConfigurableJointMotion.Limited;
  218. }
  219. else if (Mathf.RoundToInt(Mathf.Abs(transform.forward.y)) == 1)
  220. {
  221. buttonJoint.zMotion = ConfigurableJointMotion.Limited;
  222. }
  223. break;
  224. case ButtonDirection.z:
  225. case ButtonDirection.negZ:
  226. if (Mathf.RoundToInt(Mathf.Abs(transform.right.z)) == 1)
  227. {
  228. buttonJoint.xMotion = ConfigurableJointMotion.Limited;
  229. }
  230. else if (Mathf.RoundToInt(Mathf.Abs(transform.up.z)) == 1)
  231. {
  232. buttonJoint.yMotion = ConfigurableJointMotion.Limited;
  233. }
  234. else if (Mathf.RoundToInt(Mathf.Abs(transform.forward.z)) == 1)
  235. {
  236. buttonJoint.zMotion = ConfigurableJointMotion.Limited;
  237. }
  238. break;
  239. }
  240. }
  241. protected override bool DetectSetup()
  242. {
  243. finalDirection = (direction == ButtonDirection.autodetect ? DetectDirection() : direction);
  244. if (finalDirection == ButtonDirection.autodetect)
  245. {
  246. activationDir = Vector3.zero;
  247. return false;
  248. }
  249. activationDir = (direction != ButtonDirection.autodetect ? CalculateActivationDir() : activationDir);
  250. if (buttonForce != null)
  251. {
  252. buttonForce.force = GetForceVector();
  253. }
  254. if (Application.isPlaying)
  255. {
  256. DetectJointSetup();
  257. DetectJointLimitsSetup();
  258. DetectJointDirectionSetup();
  259. }
  260. return true;
  261. }
  262. protected override ControlValueRange RegisterValueRange()
  263. {
  264. return new ControlValueRange()
  265. {
  266. controlMin = 0,
  267. controlMax = 1
  268. };
  269. }
  270. protected override void HandleUpdate()
  271. {
  272. // trigger events
  273. float oldState = value;
  274. if (ReachedActivationDistance())
  275. {
  276. if (oldState == 0)
  277. {
  278. value = 1;
  279. OnPushed(SetControlEvent());
  280. }
  281. }
  282. else
  283. {
  284. if (oldState == 1)
  285. {
  286. value = 0;
  287. OnReleased(SetControlEvent());
  288. }
  289. }
  290. }
  291. protected virtual void FixedUpdate()
  292. {
  293. // 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
  294. if (forceCount == 0 && buttonJoint.connectedBody != null)
  295. {
  296. restingPosition = transform.position;
  297. }
  298. }
  299. protected virtual void OnCollisionExit(Collision collision)
  300. {
  301. // TODO: this will not always be triggered for some reason, we probably need some "healing"
  302. forceCount -= 1;
  303. }
  304. protected virtual void OnCollisionEnter(Collision collision)
  305. {
  306. forceCount += 1;
  307. }
  308. protected virtual ButtonDirection DetectDirection()
  309. {
  310. ButtonDirection returnDirection = ButtonDirection.autodetect;
  311. Bounds bounds = VRTK_SharedMethods.GetBounds(transform);
  312. // shoot rays from the center of the button to learn about surroundings
  313. RaycastHit hitForward;
  314. RaycastHit hitBack;
  315. RaycastHit hitLeft;
  316. RaycastHit hitRight;
  317. RaycastHit hitUp;
  318. RaycastHit hitDown;
  319. Physics.Raycast(bounds.center, Vector3.forward, out hitForward, bounds.extents.z * MAX_AUTODETECT_ACTIVATION_LENGTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
  320. Physics.Raycast(bounds.center, Vector3.back, out hitBack, bounds.extents.z * MAX_AUTODETECT_ACTIVATION_LENGTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
  321. Physics.Raycast(bounds.center, Vector3.left, out hitLeft, bounds.extents.x * MAX_AUTODETECT_ACTIVATION_LENGTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
  322. Physics.Raycast(bounds.center, Vector3.right, out hitRight, bounds.extents.x * MAX_AUTODETECT_ACTIVATION_LENGTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
  323. Physics.Raycast(bounds.center, Vector3.up, out hitUp, bounds.extents.y * MAX_AUTODETECT_ACTIVATION_LENGTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
  324. Physics.Raycast(bounds.center, Vector3.down, out hitDown, bounds.extents.y * MAX_AUTODETECT_ACTIVATION_LENGTH, Physics.DefaultRaycastLayers, QueryTriggerInteraction.UseGlobal);
  325. // shortest valid ray wins
  326. float lengthX = (hitRight.collider != null) ? hitRight.distance : float.MaxValue;
  327. float lengthY = (hitDown.collider != null) ? hitDown.distance : float.MaxValue;
  328. float lengthZ = (hitBack.collider != null) ? hitBack.distance : float.MaxValue;
  329. float lengthNegX = (hitLeft.collider != null) ? hitLeft.distance : float.MaxValue;
  330. float lengthNegY = (hitUp.collider != null) ? hitUp.distance : float.MaxValue;
  331. float lengthNegZ = (hitForward.collider != null) ? hitForward.distance : float.MaxValue;
  332. float extents = 0;
  333. Vector3 hitPoint = Vector3.zero;
  334. if (VRTK_SharedMethods.IsLowest(lengthX, new float[] { lengthY, lengthZ, lengthNegX, lengthNegY, lengthNegZ }))
  335. {
  336. returnDirection = ButtonDirection.negX;
  337. hitPoint = hitRight.point;
  338. extents = bounds.extents.x;
  339. }
  340. else if (VRTK_SharedMethods.IsLowest(lengthY, new float[] { lengthX, lengthZ, lengthNegX, lengthNegY, lengthNegZ }))
  341. {
  342. returnDirection = ButtonDirection.y;
  343. hitPoint = hitDown.point;
  344. extents = bounds.extents.y;
  345. }
  346. else if (VRTK_SharedMethods.IsLowest(lengthZ, new float[] { lengthX, lengthY, lengthNegX, lengthNegY, lengthNegZ }))
  347. {
  348. returnDirection = ButtonDirection.z;
  349. hitPoint = hitBack.point;
  350. extents = bounds.extents.z;
  351. }
  352. else if (VRTK_SharedMethods.IsLowest(lengthNegX, new float[] { lengthX, lengthY, lengthZ, lengthNegY, lengthNegZ }))
  353. {
  354. returnDirection = ButtonDirection.x;
  355. hitPoint = hitLeft.point;
  356. extents = bounds.extents.x;
  357. }
  358. else if (VRTK_SharedMethods.IsLowest(lengthNegY, new float[] { lengthX, lengthY, lengthZ, lengthNegX, lengthNegZ }))
  359. {
  360. returnDirection = ButtonDirection.negY;
  361. hitPoint = hitUp.point;
  362. extents = bounds.extents.y;
  363. }
  364. else if (VRTK_SharedMethods.IsLowest(lengthNegZ, new float[] { lengthX, lengthY, lengthZ, lengthNegX, lengthNegY }))
  365. {
  366. returnDirection = ButtonDirection.negZ;
  367. hitPoint = hitForward.point;
  368. extents = bounds.extents.z;
  369. }
  370. // determin activation distance
  371. activationDistance = (Vector3.Distance(hitPoint, bounds.center) - extents) * 0.95f;
  372. if (returnDirection == ButtonDirection.autodetect || activationDistance < 0.001f)
  373. {
  374. // auto-detection was not possible or colliding with object already
  375. returnDirection = ButtonDirection.autodetect;
  376. activationDistance = 0;
  377. }
  378. else
  379. {
  380. activationDir = hitPoint - bounds.center;
  381. }
  382. return returnDirection;
  383. }
  384. protected virtual Vector3 CalculateActivationDir()
  385. {
  386. Bounds bounds = VRTK_SharedMethods.GetBounds(transform, transform);
  387. Vector3 buttonDirection = Vector3.zero;
  388. float extents = 0;
  389. switch (direction)
  390. {
  391. case ButtonDirection.x:
  392. case ButtonDirection.negX:
  393. if (Mathf.RoundToInt(Mathf.Abs(transform.right.x)) == 1)
  394. {
  395. buttonDirection = transform.right;
  396. extents = bounds.extents.x;
  397. }
  398. else if (Mathf.RoundToInt(Mathf.Abs(transform.up.x)) == 1)
  399. {
  400. buttonDirection = transform.up;
  401. extents = bounds.extents.y;
  402. }
  403. else if (Mathf.RoundToInt(Mathf.Abs(transform.forward.x)) == 1)
  404. {
  405. buttonDirection = transform.forward;
  406. extents = bounds.extents.z;
  407. }
  408. buttonDirection *= (direction == ButtonDirection.x) ? -1 : 1;
  409. break;
  410. case ButtonDirection.y:
  411. case ButtonDirection.negY:
  412. if (Mathf.RoundToInt(Mathf.Abs(transform.right.y)) == 1)
  413. {
  414. buttonDirection = transform.right;
  415. extents = bounds.extents.x;
  416. }
  417. else if (Mathf.RoundToInt(Mathf.Abs(transform.up.y)) == 1)
  418. {
  419. buttonDirection = transform.up;
  420. extents = bounds.extents.y;
  421. }
  422. else if (Mathf.RoundToInt(Mathf.Abs(transform.forward.y)) == 1)
  423. {
  424. buttonDirection = transform.forward;
  425. extents = bounds.extents.z;
  426. }
  427. buttonDirection *= (direction == ButtonDirection.y) ? -1 : 1;
  428. break;
  429. case ButtonDirection.z:
  430. case ButtonDirection.negZ:
  431. if (Mathf.RoundToInt(Mathf.Abs(transform.right.z)) == 1)
  432. {
  433. buttonDirection = transform.right;
  434. extents = bounds.extents.x;
  435. }
  436. else if (Mathf.RoundToInt(Mathf.Abs(transform.up.z)) == 1)
  437. {
  438. buttonDirection = transform.up;
  439. extents = bounds.extents.y;
  440. }
  441. else if (Mathf.RoundToInt(Mathf.Abs(transform.forward.z)) == 1)
  442. {
  443. buttonDirection = transform.forward;
  444. extents = bounds.extents.z;
  445. }
  446. buttonDirection *= (direction == ButtonDirection.z) ? -1 : 1;
  447. break;
  448. }
  449. // subtract width of button
  450. return (buttonDirection * (extents + activationDistance));
  451. }
  452. protected virtual bool ReachedActivationDistance()
  453. {
  454. return (Vector3.Distance(transform.position, restingPosition) >= activationDistance);
  455. }
  456. protected virtual Vector3 GetForceVector()
  457. {
  458. return (-activationDir.normalized * buttonStrength);
  459. }
  460. }
  461. }