TouchableButton.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287
  1. using System.Collections;
  2. using System.Collections.Generic;
  3. using UnityEngine;
  4. using UnityEngine.EventSystems;
  5. using SC.XR.Unity.Module_InputSystem;
  6. using DG.Tweening;
  7. using UnityEngine.Events;
  8. using SC.XR.Unity.Module_InputSystem.InputDeviceHand;
  9. [RequireComponent(typeof(BoxCollider))]
  10. [AddComponentMenu("ShadowSDK/TouchableButton")]
  11. public class TouchableButton : PokeHandler {
  12. [SerializeField]
  13. private List<InteractionTouchableEntry> m_Delegates;
  14. public List<InteractionTouchableEntry> Triggers {
  15. get {
  16. if(m_Delegates == null)
  17. m_Delegates = new List<InteractionTouchableEntry>();
  18. return m_Delegates;
  19. }
  20. set { m_Delegates = value; }
  21. }
  22. [SerializeField]
  23. protected SCAudiosConfig.AudioType PressAudio = SCAudiosConfig.AudioType.ButtonPress;
  24. [SerializeField]
  25. protected SCAudiosConfig.AudioType ReleaseAudio = SCAudiosConfig.AudioType.ButtonUnpress;
  26. public Transform VisualMove;
  27. public bool useCustomMovePosition = false;
  28. public Vector3 visualMoveStartLocalPosition;
  29. public Vector3 visualMoveEndLocalPosition;
  30. public Transform VisualScale;
  31. public bool useCustomScale = false;
  32. public Vector3 visualStartLocalScale = Vector3.one;
  33. public Vector3 visualEndLocalScale = new Vector3(1, 1, 0);
  34. protected Vector3 MoveObjInitLocalPosition;
  35. protected Vector3 ScaleObjInitLocalScale;
  36. [SerializeField]
  37. [Tooltip("Ensures that the button can only be pushed from the front. Touching the button from the back or side is prevented.")]
  38. private bool enforceFrontPush = true;
  39. [SerializeField]
  40. [Range(0.2f, 0.8f)]
  41. [Tooltip("The minimum percentage of the original scale the compressableButtonVisuals can be compressed to.")]
  42. protected float minCompressPercentage = 0.25f;
  43. NearInteractionTouchable nearInterationTouchable;
  44. public NearInteractionTouchable NearInterationTouchable {
  45. get {
  46. if(nearInterationTouchable == null) {
  47. nearInterationTouchable = GetComponent<NearInteractionTouchable>();
  48. if(nearInterationTouchable == null) {
  49. nearInterationTouchable = gameObject.AddComponent<NearInteractionTouchable>();
  50. }
  51. }
  52. return nearInterationTouchable;
  53. }
  54. }
  55. protected BoxCollider BoxCollider {
  56. get {
  57. return NearInterationTouchable.BoxCollider;
  58. }
  59. }
  60. public Transform Center {
  61. get {
  62. return NearInterationTouchable.Center;
  63. }
  64. }
  65. /// <summary>
  66. /// The press direction of the button as defined by a NearInteractionTouchableSurface.
  67. /// </summary>
  68. private Vector3 Normal {
  69. get {
  70. return NearInterationTouchable.Normal;
  71. }
  72. }
  73. private float StartPushDistance;
  74. protected float startPushDistance {
  75. get {
  76. if(NearInterationTouchable.NormalType == NormalType.NZ) {
  77. return StartPushDistance = BoxCollider.size.z * BoxCollider.transform.lossyScale.z / 2.0f * -1;
  78. } else if(NearInterationTouchable.NormalType == NormalType.Z) {
  79. return StartPushDistance = BoxCollider.size.z * BoxCollider.transform.lossyScale.z / 2.0f * 1;
  80. } else if(NearInterationTouchable.NormalType == NormalType.NY) {
  81. return StartPushDistance = BoxCollider.size.y * BoxCollider.transform.lossyScale.y / 2.0f * -1;
  82. } else if(NearInterationTouchable.NormalType == NormalType.Y) {
  83. return StartPushDistance = BoxCollider.size.y * BoxCollider.transform.lossyScale.y / 2.0f * 1;
  84. } else if(NearInterationTouchable.NormalType == NormalType.NX) {
  85. return StartPushDistance = BoxCollider.size.x * BoxCollider.transform.lossyScale.x / 2.0f * -1;
  86. } else {
  87. return StartPushDistance = BoxCollider.size.x * BoxCollider.transform.lossyScale.x / 2.0f * 1;
  88. }
  89. }
  90. }
  91. private float MaxPushDistance;
  92. private float maxPushDistance {
  93. get {
  94. return MaxPushDistance = -startPushDistance;
  95. }
  96. }
  97. private float PressDistance;
  98. private float pressDistance {
  99. get {
  100. return PressDistance = maxPushDistance * (1.0f - minCompressPercentage);
  101. }
  102. }
  103. private float ReleaseDistance;
  104. private float releaseDistance {
  105. get {
  106. return ReleaseDistance = -pressDistance;
  107. }
  108. }
  109. private float currentPushDistance = 0.0f;
  110. /// <summary>
  111. /// Represents the state of whether the button is currently being pressed.
  112. /// </summary>
  113. public bool IsPressing { get; private set; }
  114. private bool isTouching = false;
  115. ///<summary>
  116. /// Represents the state of whether or not a finger is currently touching this button.
  117. ///</summary>
  118. public bool IsTouching {
  119. get => isTouching;
  120. set {
  121. if(value != isTouching) {
  122. isTouching = value;
  123. }
  124. }
  125. }
  126. public virtual void Awake() {
  127. if(VisualScale != null) {
  128. ScaleObjInitLocalScale = VisualScale.localScale;
  129. }
  130. if(VisualMove != null) {
  131. MoveObjInitLocalPosition = VisualMove.localPosition;
  132. }
  133. }
  134. /// <summary>
  135. /// Returns the local distance along the push direction for the passed in world position
  136. /// </summary>
  137. private float GetDistanceAlongPushDirection(Vector3 positionWorldSpace) {
  138. Vector3 worldVector = transform.TransformPoint(BoxCollider.center) - positionWorldSpace;
  139. return Vector3.Dot(worldVector, Normal.normalized); ;
  140. }
  141. // This function projects the current touch positions onto the 1D press direction of the button.
  142. // It will output the farthest pushed distance from the button's initial position.
  143. private float GetFarthestDistanceAlongPressDirection(Vector3 positionWorldSpace) {
  144. float testDistance = GetDistanceAlongPushDirection(positionWorldSpace);
  145. return Mathf.Clamp(testDistance, startPushDistance, maxPushDistance);
  146. }
  147. private void PulseProximityLight(TouchPointer touchPointer) {
  148. // Pulse each proximity light on pointer cursors' interacting with this button.
  149. ProximityLight[] proximityLights = touchPointer.cursorBase?.gameObject?.GetComponentsInChildren<ProximityLight>();
  150. if(proximityLights != null) {
  151. foreach(var proximityLight in proximityLights) {
  152. proximityLight.Pulse();
  153. }
  154. }
  155. }
  156. private void UpdatePressedState(float pushDistance, TouchPointer touchPointer) {
  157. // If we aren't in a press and can't start a simple one.
  158. if(!IsPressing) {
  159. // Compare to our previous push depth. Use previous push distance to handle back-presses.
  160. if(pushDistance >= pressDistance) {
  161. IsPressing = true;
  162. AudioSystem.getInstance.PlayAudioOneShot(gameObject, PressAudio);
  163. Execute(InteractionTouchableType.PokePress, null);
  164. PulseProximityLight(touchPointer);
  165. }
  166. }
  167. // If we're in a press, check if the press is released now.
  168. else {
  169. //float releaseDistance = pressDistance - releaseDistanceDelta;
  170. if(pushDistance <= releaseDistance) {
  171. IsPressing = false;
  172. AudioSystem.getInstance.PlayAudioOneShot(gameObject, ReleaseAudio);
  173. Execute(InteractionTouchableType.PokeRelease, null);
  174. }
  175. }
  176. }
  177. void UpdateVisual(float currentPushDistance) {
  178. if(VisualMove != null) {
  179. //Debug.Log("Update Visual");
  180. Vector3 position = Vector3.zero;
  181. if(useCustomMovePosition) {
  182. Vector3 localVisualPosition = Vector3.Lerp(visualMoveStartLocalPosition, visualMoveEndLocalPosition, (currentPushDistance - startPushDistance) / (maxPushDistance - startPushDistance));
  183. position = transform.TransformPoint(localVisualPosition);
  184. } else {
  185. position = transform.position + transform.TransformVector(MoveObjInitLocalPosition) - (currentPushDistance - startPushDistance) * Normal.normalized;
  186. }
  187. VisualMove.position = position;
  188. }
  189. if(VisualScale != null) {
  190. Vector3 scale = Vector3.one;
  191. float pressPercentage = 1.0f - (currentPushDistance - startPushDistance) / (maxPushDistance - startPushDistance);
  192. if(useCustomScale) {
  193. scale = Vector3.Lerp(visualEndLocalScale, visualStartLocalScale, pressPercentage); ;
  194. } else {
  195. scale = ScaleObjInitLocalScale;
  196. scale.z = ScaleObjInitLocalScale.z * pressPercentage;
  197. }
  198. VisualScale.transform.localScale = scale;
  199. }
  200. }
  201. void ResetVisual() {
  202. if(VisualMove) {
  203. VisualMove.DOMove(useCustomMovePosition ? transform.TransformPoint(visualMoveStartLocalPosition) : transform.position + transform.TransformVector(MoveObjInitLocalPosition), 0.2f);
  204. }
  205. if(VisualScale) {
  206. VisualScale.DOScale(useCustomScale ? visualStartLocalScale : ScaleObjInitLocalScale, 0.2f);
  207. }
  208. }
  209. private bool HasPassedThroughStartPlane(Vector3 positionWorldSpace) {
  210. float distanceAlongPushDirection = GetDistanceAlongPushDirection(positionWorldSpace);
  211. return distanceAlongPushDirection <= startPushDistance;
  212. }
  213. public override void OnPokeDown(TouchPointer touchPointer, SCPointEventData eventData) {
  214. AudioSystem.getInstance.PlayAudioOneShot(gameObject, PokeDownAudio);
  215. Execute(InteractionTouchableType.PokeDown, eventData);
  216. // Back-Press Detection:
  217. // Accept touch only if controller pushed from the front.
  218. if(enforceFrontPush && !HasPassedThroughStartPlane(touchPointer.PreviousTouchPosition)) {
  219. Debug.Log("Not Front Press");
  220. return;
  221. }
  222. IsTouching = true;
  223. }
  224. public override void OnPokeUpdated(TouchPointer touchPointer, SCPointEventData eventData) {
  225. Execute(InteractionTouchableType.PokeUpdate, eventData);
  226. if(IsTouching) {
  227. currentPushDistance = Mathf.Lerp(currentPushDistance, GetFarthestDistanceAlongPressDirection(touchPointer.TouchPosition), 0.5f);
  228. UpdatePressedState(currentPushDistance, touchPointer);
  229. UpdateVisual(currentPushDistance);
  230. }
  231. }
  232. public override void OnPokeUp(TouchPointer touchPointer, SCPointEventData eventData) {
  233. AudioSystem.getInstance.PlayAudioOneShot(gameObject, PokeUpAudio);
  234. Execute(InteractionTouchableType.PokeUp, eventData);
  235. ResetVisual();
  236. if(IsPressing) {
  237. IsPressing = false;
  238. AudioSystem.getInstance.PlayAudioOneShot(gameObject, SCAudiosConfig.AudioType.ButtonUnpress);
  239. Execute(InteractionTouchableType.PokeRelease, eventData);
  240. }
  241. IsTouching = false;
  242. }
  243. private void Execute(InteractionTouchableType id, BaseEventData eventData) {
  244. for(int i = 0; i < Triggers.Count; i++) {
  245. InteractionTouchableEntry entry = Triggers[i];
  246. if(entry.eventID == id) {
  247. entry.callback?.Invoke(eventData);
  248. }
  249. }
  250. }
  251. }