XRPokeFollowAffordance.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298
  1. using System;
  2. using Unity.Mathematics;
  3. using Unity.XR.CoreUtils.Bindings;
  4. using UnityEngine.XR.Interaction.Toolkit.AffordanceSystem.State;
  5. using UnityEngine.XR.Interaction.Toolkit.Filtering;
  6. using UnityEngine.XR.Interaction.Toolkit.Utilities.Tweenables.Primitives;
  7. namespace UnityEngine.XR.Interaction.Toolkit.Samples.StarterAssets
  8. {
  9. /// <summary>
  10. /// Follow animation affordance for <see cref="IPokeStateDataProvider"/>, such as <see cref="XRPokeFilter"/>.
  11. /// Used to animate a pressed transform, such as a button to follow the poke position.
  12. /// </summary>
  13. /// <remarks>
  14. /// The Affordance System namespace and all associated classes have been deprecated.
  15. /// The existing affordance system will be moved, replaced and updated with a new interaction
  16. /// feedback system in a future version of XRI, including this sample script.
  17. /// </remarks>
  18. [AddComponentMenu("XR/XR Poke Follow Affordance", 22)]
  19. public class XRPokeFollowAffordance : MonoBehaviour
  20. {
  21. [SerializeField]
  22. [Tooltip("Transform that will move in the poke direction when this or a parent GameObject is poked." +
  23. "\nNote: Should be a direct child GameObject.")]
  24. Transform m_PokeFollowTransform;
  25. /// <summary>
  26. /// Transform that will animate along the axis of interaction when this interactable is poked.
  27. /// Note: Must be a direct child GameObject as it moves in local space relative to the poke target's transform.
  28. /// </summary>
  29. public Transform pokeFollowTransform
  30. {
  31. get => m_PokeFollowTransform;
  32. set => m_PokeFollowTransform = value;
  33. }
  34. [SerializeField]
  35. [Range(0f, 20f)]
  36. [Tooltip("Multiplies transform position interpolation as a factor of Time.deltaTime. If 0, no smoothing will be applied.")]
  37. float m_SmoothingSpeed = 16f;
  38. /// <summary>
  39. /// Multiplies transform position interpolation as a factor of <see cref="Time.deltaTime"/>. If <c>0</c>, no smoothing will be applied.
  40. /// </summary>
  41. public float smoothingSpeed
  42. {
  43. get => m_SmoothingSpeed;
  44. set => m_SmoothingSpeed = value;
  45. }
  46. [SerializeField]
  47. [Tooltip("When this component is no longer the target of the poke, the Poke Follow Transform returns to the original position.")]
  48. bool m_ReturnToInitialPosition = true;
  49. /// <summary>
  50. /// When this component is no longer the target of the poke, the <see cref="pokeFollowTransform"/> returns to the original position.
  51. /// </summary>
  52. public bool returnToInitialPosition
  53. {
  54. get => m_ReturnToInitialPosition;
  55. set => m_ReturnToInitialPosition = value;
  56. }
  57. [SerializeField]
  58. [Tooltip("Whether to apply the follow animation if the target of the poke is a child of this transform. " +
  59. "This is useful for UI objects that may have child graphics.")]
  60. bool m_ApplyIfChildIsTarget = true;
  61. /// <summary>
  62. /// Whether to apply the follow animation if the target of the poke is a child of this transform.
  63. /// This is useful for UI objects that may have child graphics.
  64. /// </summary>
  65. public bool applyIfChildIsTarget
  66. {
  67. get => m_ApplyIfChildIsTarget;
  68. set => m_ApplyIfChildIsTarget = value;
  69. }
  70. [SerializeField]
  71. [Tooltip("Whether to keep the Poke Follow Transform from moving past a maximum distance from the poke target.")]
  72. bool m_ClampToMaxDistance;
  73. /// <summary>
  74. /// Whether to keep the <see cref="pokeFollowTransform"/> from moving past <see cref="maxDistance"/> from the poke target.
  75. /// </summary>
  76. public bool clampToMaxDistance
  77. {
  78. get => m_ClampToMaxDistance;
  79. set => m_ClampToMaxDistance = value;
  80. }
  81. [SerializeField]
  82. [Tooltip("The maximum distance from this transform that the Poke Follow Transform can move.")]
  83. float m_MaxDistance;
  84. /// <summary>
  85. /// The maximum distance from this transform that the <see cref="pokeFollowTransform"/> can move when
  86. /// <see cref="clampToMaxDistance"/> is <see langword="true"/>.
  87. /// </summary>
  88. public float maxDistance
  89. {
  90. get => m_MaxDistance;
  91. set => m_MaxDistance = value;
  92. }
  93. /// <summary>
  94. /// The original position of this interactable before any pushes have been applied.
  95. /// </summary>
  96. public Vector3 initialPosition
  97. {
  98. get => m_InitialPosition;
  99. set => m_InitialPosition = value;
  100. }
  101. IPokeStateDataProvider m_PokeDataProvider;
  102. IMultiPokeStateDataProvider m_MultiPokeStateDataProvider;
  103. #pragma warning disable CS0618 // Type or member is obsolete
  104. readonly Vector3TweenableVariable m_TransformTweenableVariable = new Vector3TweenableVariable();
  105. #pragma warning restore CS0618 // Type or member is obsolete
  106. readonly BindingsGroup m_BindingsGroup = new BindingsGroup();
  107. Vector3 m_InitialPosition;
  108. bool m_IsFirstFrame;
  109. [HideInInspector]
  110. [SerializeField]
  111. XRPokeFilter m_PokeFilter = null;
  112. /// <summary>
  113. /// See <see cref="MonoBehaviour"/>.
  114. /// </summary>
  115. protected void Awake()
  116. {
  117. m_MultiPokeStateDataProvider = GetComponentInParent<IMultiPokeStateDataProvider>();
  118. if (m_MultiPokeStateDataProvider == null)
  119. m_PokeDataProvider = GetComponentInParent<IPokeStateDataProvider>();
  120. }
  121. /// <summary>
  122. /// See <see cref="MonoBehaviour"/>.
  123. /// </summary>
  124. protected void Start()
  125. {
  126. if (m_PokeFollowTransform != null)
  127. {
  128. m_InitialPosition = m_PokeFollowTransform.localPosition;
  129. m_BindingsGroup.AddBinding(m_TransformTweenableVariable.Subscribe(OnTransformTweenableVariableUpdated));
  130. if (m_MultiPokeStateDataProvider != null)
  131. m_BindingsGroup.AddBinding(m_MultiPokeStateDataProvider.GetPokeStateDataForTarget(transform).Subscribe(OnPokeStateDataUpdated));
  132. else if (m_PokeDataProvider != null)
  133. m_BindingsGroup.AddBinding(m_PokeDataProvider.pokeStateData.SubscribeAndUpdate(OnPokeStateDataUpdated));
  134. }
  135. else
  136. {
  137. enabled = false;
  138. Debug.LogWarning($"Missing Poke Follow Transform assignment on {this}. Disabling component.", this);
  139. }
  140. }
  141. /// <summary>
  142. /// See <see cref="MonoBehaviour"/>.
  143. /// </summary>
  144. protected void OnDestroy()
  145. {
  146. m_BindingsGroup.Clear();
  147. m_TransformTweenableVariable?.Dispose();
  148. }
  149. /// <summary>
  150. /// See <see cref="MonoBehaviour"/>.
  151. /// </summary>
  152. protected void LateUpdate()
  153. {
  154. if (m_IsFirstFrame)
  155. {
  156. m_TransformTweenableVariable.HandleTween(1f);
  157. m_IsFirstFrame = false;
  158. return;
  159. }
  160. m_TransformTweenableVariable.HandleTween(m_SmoothingSpeed > 0f ? Time.deltaTime * m_SmoothingSpeed : 1f);
  161. }
  162. protected virtual void OnTransformTweenableVariableUpdated(float3 position)
  163. {
  164. // UI Anchors can cause this to not work correctly, so we check if it's a RectTransform and set the localPosition Z only
  165. if (m_PokeFollowTransform is RectTransform)
  166. {
  167. var targetPosition = m_PokeFollowTransform.localPosition;
  168. targetPosition.z = position.z;
  169. m_PokeFollowTransform.localPosition = targetPosition;
  170. }
  171. else
  172. {
  173. m_PokeFollowTransform.localPosition = position;
  174. }
  175. }
  176. void OnPokeStateDataUpdated(PokeStateData data)
  177. {
  178. var pokeTarget = data.target;
  179. var applyFollow = m_ApplyIfChildIsTarget
  180. ? pokeTarget != null && pokeTarget.IsChildOf(transform)
  181. : pokeTarget == transform;
  182. if (applyFollow)
  183. {
  184. var targetPosition = pokeTarget.InverseTransformPoint(data.axisAlignedPokeInteractionPoint);
  185. if (m_ClampToMaxDistance && targetPosition.sqrMagnitude > m_MaxDistance * m_MaxDistance)
  186. targetPosition = Vector3.ClampMagnitude(targetPosition, m_MaxDistance);
  187. m_TransformTweenableVariable.target = targetPosition;
  188. }
  189. else if (m_ReturnToInitialPosition)
  190. {
  191. m_TransformTweenableVariable.target = m_InitialPosition;
  192. }
  193. }
  194. public void ResetFollowTransform()
  195. {
  196. if (!m_ClampToMaxDistance || m_PokeFollowTransform == null)
  197. return;
  198. m_PokeFollowTransform.localPosition = m_InitialPosition;
  199. }
  200. void OnDrawGizmos()
  201. {
  202. if (!TryGetTargetEndPoint(out var endPoint))
  203. return;
  204. Gizmos.color = Color.yellow;
  205. Gizmos.DrawLine(transform.position, endPoint);
  206. }
  207. bool TryGetTargetEndPoint(out Vector3 endPoint)
  208. {
  209. if (!m_ClampToMaxDistance || m_PokeFilter == null)
  210. {
  211. endPoint = Vector3.zero;
  212. return false;
  213. }
  214. Vector3 origin = transform.position;
  215. Vector3 direction = ComputeRotatedDepthEvaluationAxis(m_PokeFilter.pokeConfiguration);
  216. endPoint = origin + direction.normalized * m_MaxDistance;
  217. return true;
  218. }
  219. Vector3 ComputeRotatedDepthEvaluationAxis(PokeThresholdData pokeThresholdData)
  220. {
  221. if (pokeThresholdData == null)
  222. return Vector3.zero;
  223. Vector3 rotatedDepthEvaluationAxis = Vector3.zero;
  224. switch (pokeThresholdData.pokeDirection)
  225. {
  226. case PokeAxis.X:
  227. case PokeAxis.NegativeX:
  228. rotatedDepthEvaluationAxis = transform.right;
  229. break;
  230. case PokeAxis.Y:
  231. case PokeAxis.NegativeY:
  232. rotatedDepthEvaluationAxis = transform.up;
  233. break;
  234. case PokeAxis.Z:
  235. case PokeAxis.NegativeZ:
  236. rotatedDepthEvaluationAxis = transform.forward;
  237. break;
  238. }
  239. switch (pokeThresholdData.pokeDirection)
  240. {
  241. case PokeAxis.X:
  242. case PokeAxis.Y:
  243. case PokeAxis.Z:
  244. rotatedDepthEvaluationAxis = -rotatedDepthEvaluationAxis;
  245. break;
  246. }
  247. return rotatedDepthEvaluationAxis;
  248. }
  249. void OnValidate()
  250. {
  251. if (m_PokeFilter == null)
  252. {
  253. m_PokeFilter = GetComponentInParent<XRPokeFilter>();
  254. }
  255. // Visually update the end point to match the target clamped position
  256. if (m_PokeFollowTransform != null && TryGetTargetEndPoint(out var endPoint))
  257. m_PokeFollowTransform.position = endPoint;
  258. }
  259. }
  260. }