InputActionTrace.cs 20 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564
  1. using System;
  2. using System.Collections;
  3. using System.Collections.Generic;
  4. using System.Text;
  5. using Unity.Collections.LowLevel.Unsafe;
  6. using UnityEngine.InputSystem.LowLevel;
  7. ////REVIEW: why not switch to this being the default mechanism? seems like this could allow us to also solve
  8. //// the actions-update-when-not-expected problem; plus give us access to easy polling
  9. ////REVIEW: should this automatically unsubscribe itself on disposal?
  10. ////TODO: make it possible to persist this same way that it should be possible to persist InputEventTrace
  11. ////TODO: make this one thread-safe
  12. ////TODO: add random access capability
  13. ////TODO: protect traces against controls changing configuration (if state layouts change, we're affected)
  14. namespace UnityEngine.InputSystem.Utilities
  15. {
  16. /// <summary>
  17. /// Records the triggering of actions into a sequence of events that can be replayed at will.
  18. /// </summary>
  19. /// <remarks>
  20. /// This is an alternate way to the callback-based responses (such as <see cref="InputAction.performed"/>)
  21. /// of <see cref="InputAction">input actions</see>. Instead of executing response code right away whenever
  22. /// an action triggers, an <see cref="RecordAction">event is recorded</see> which can then be queried on demand.
  23. ///
  24. /// The recorded data will stay valid even if the bindings on the actions are changed (e.g. by enabling a different
  25. /// set of bindings through altering <see cref="InputAction.bindingMask"/> or <see cref="InputActionMap.devices"/> or
  26. /// when modifying the paths of bindings altogether). Note, however, that when this happens, a trace will have
  27. /// to make a private copy of the data that stores the binding resolution state. This means that there can be
  28. /// GC allocation spike when reconfiguring actions that have recorded data in traces.
  29. ///
  30. /// <example>
  31. /// <code>
  32. /// var trace = new InputActionTrace();
  33. ///
  34. /// // Subscribe trace to single action.
  35. /// // (Use UnsubscribeFrom to unsubscribe)
  36. /// trace.SubscribeTo(myAction);
  37. ///
  38. /// // Subscribe trace to entire action map.
  39. /// // (Use UnsubscribeFrom to unsubscribe)
  40. /// trace.SubscribeTo(myActionMap);
  41. ///
  42. /// // Subscribe trace to all actions in the system.
  43. /// trace.SubscribeToAll();
  44. ///
  45. /// // Record a single triggering of an action.
  46. /// myAction.performed +=
  47. /// ctx =>
  48. /// {
  49. /// if (ctx.ReadValue&lt;float&gt;() &gt; 0.5f)
  50. /// trace.RecordAction(ctx);
  51. /// };
  52. ///
  53. /// // Output trace to console.
  54. /// Debug.Log(string.Join(",\n", trace));
  55. ///
  56. /// // Walk through all recorded actions and then clear trace.
  57. /// foreach (var record in trace)
  58. /// {
  59. /// Debug.Log($"{record.action} was {record.phase} by control {record.control} at {record.time}");
  60. ///
  61. /// // To read out the value, you either have to know the value type or read the
  62. /// // value out as a generic byte buffer. Here we assume that the value type is
  63. /// // float.
  64. ///
  65. /// Debug.Log("Value: " + record.ReadValue&lt;float&gt;());
  66. ///
  67. /// // An alternative is read the value as an object. In this case, you don't have
  68. /// // to know the value type but there will be a boxed object allocation.
  69. /// Debug.Log("Value: " + record.ReadValueAsObject());
  70. /// }
  71. /// trace.Clear();
  72. ///
  73. /// // Unsubscribe trace from everything.
  74. /// trace.UnsubscribeFromAll();
  75. ///
  76. /// // Release memory held by trace.
  77. /// trace.Dispose();
  78. /// </code>
  79. /// </example>
  80. /// </remarks>
  81. /// <seealso cref="InputAction.started"/>
  82. /// <seealso cref="InputAction.performed"/>
  83. /// <seealso cref="InputAction.canceled"/>
  84. /// <seealso cref="InputSystem.onActionChange"/>
  85. public sealed class InputActionTrace : IEnumerable<InputActionTrace.ActionEventPtr>, IDisposable
  86. {
  87. ////REVIEW: this is of limited use without having access to ActionEvent
  88. /// <summary>
  89. /// Directly access the underlying raw memory queue.
  90. /// </summary>
  91. public InputEventBuffer buffer => m_EventBuffer;
  92. public int count => m_EventBuffer.eventCount;
  93. public InputActionTrace()
  94. {
  95. }
  96. public InputActionTrace(InputAction action)
  97. {
  98. if (action == null)
  99. throw new ArgumentNullException(nameof(action));
  100. SubscribeTo(action);
  101. }
  102. public InputActionTrace(InputActionMap actionMap)
  103. {
  104. if (actionMap == null)
  105. throw new ArgumentNullException(nameof(actionMap));
  106. SubscribeTo(actionMap);
  107. }
  108. /// <summary>
  109. /// Record any action getting triggered anywhere.
  110. /// </summary>
  111. /// <remarks>
  112. /// This does not require the trace to actually hook into every single action or action map in the system.
  113. /// Instead, the trace will listen to <see cref="InputSystem.onActionChange"/> and automatically record
  114. /// every triggered action.
  115. /// </remarks>
  116. public void SubscribeToAll()
  117. {
  118. if (m_SubscribedToAll)
  119. return;
  120. HookOnActionChange();
  121. m_SubscribedToAll = true;
  122. // Remove manually created subscriptions.
  123. while (m_SubscribedActions.length > 0)
  124. UnsubscribeFrom(m_SubscribedActions[m_SubscribedActions.length - 1]);
  125. while (m_SubscribedActionMaps.length > 0)
  126. UnsubscribeFrom(m_SubscribedActionMaps[m_SubscribedActionMaps.length - 1]);
  127. }
  128. public void UnsubscribeFromAll()
  129. {
  130. // Only unhook from OnActionChange if we don't have any recorded actions. If we do have
  131. // any, we still need the callback to be notified about when binding data changes.
  132. if (count == 0)
  133. UnhookOnActionChange();
  134. m_SubscribedToAll = false;
  135. while (m_SubscribedActions.length > 0)
  136. UnsubscribeFrom(m_SubscribedActions[m_SubscribedActions.length - 1]);
  137. while (m_SubscribedActionMaps.length > 0)
  138. UnsubscribeFrom(m_SubscribedActionMaps[m_SubscribedActionMaps.length - 1]);
  139. }
  140. public void SubscribeTo(InputAction action)
  141. {
  142. if (action == null)
  143. throw new ArgumentNullException(nameof(action));
  144. if (m_CallbackDelegate == null)
  145. m_CallbackDelegate = RecordAction;
  146. action.performed += m_CallbackDelegate;
  147. action.started += m_CallbackDelegate;
  148. action.canceled += m_CallbackDelegate;
  149. m_SubscribedActions.AppendWithCapacity(action);
  150. }
  151. public void SubscribeTo(InputActionMap actionMap)
  152. {
  153. if (actionMap == null)
  154. throw new ArgumentNullException(nameof(actionMap));
  155. if (m_CallbackDelegate == null)
  156. m_CallbackDelegate = RecordAction;
  157. actionMap.actionTriggered += m_CallbackDelegate;
  158. m_SubscribedActionMaps.AppendWithCapacity(actionMap);
  159. }
  160. public void UnsubscribeFrom(InputAction action)
  161. {
  162. if (action == null)
  163. throw new ArgumentNullException(nameof(action));
  164. if (m_CallbackDelegate == null)
  165. return;
  166. action.performed -= m_CallbackDelegate;
  167. action.started -= m_CallbackDelegate;
  168. action.canceled -= m_CallbackDelegate;
  169. var index = m_SubscribedActions.IndexOfReference(action);
  170. if (index != -1)
  171. m_SubscribedActions.RemoveAtWithCapacity(index);
  172. }
  173. public void UnsubscribeFrom(InputActionMap actionMap)
  174. {
  175. if (actionMap == null)
  176. throw new ArgumentNullException(nameof(actionMap));
  177. if (m_CallbackDelegate == null)
  178. return;
  179. actionMap.actionTriggered -= m_CallbackDelegate;
  180. var index = m_SubscribedActionMaps.IndexOfReference(actionMap);
  181. if (index != -1)
  182. m_SubscribedActionMaps.RemoveAtWithCapacity(index);
  183. }
  184. /// <summary>
  185. /// Record the triggering of an action as an <see cref="ActionEventPtr">action event</see>.
  186. /// </summary>
  187. /// <param name="context"></param>
  188. /// <see cref="InputAction.performed"/>
  189. /// <see cref="InputAction.started"/>
  190. /// <see cref="InputAction.canceled"/>
  191. /// <see cref="InputActionMap.actionTriggered"/>
  192. public unsafe void RecordAction(InputAction.CallbackContext context)
  193. {
  194. // Find/add state.
  195. var stateIndex = m_ActionMapStates.IndexOfReference(context.m_State);
  196. if (stateIndex == -1)
  197. stateIndex = m_ActionMapStates.AppendWithCapacity(context.m_State);
  198. // Make sure we get notified if there's a change to binding setups.
  199. HookOnActionChange();
  200. // Allocate event.
  201. var valueSizeInBytes = context.valueSizeInBytes;
  202. var eventPtr =
  203. (ActionEvent*)m_EventBuffer.AllocateEvent(ActionEvent.GetEventSizeWithValueSize(valueSizeInBytes));
  204. // Initialize event.
  205. ref var triggerState = ref context.m_State.actionStates[context.m_ActionIndex];
  206. eventPtr->baseEvent.type = ActionEvent.Type;
  207. eventPtr->baseEvent.time = triggerState.time;
  208. eventPtr->stateIndex = stateIndex;
  209. eventPtr->controlIndex = triggerState.controlIndex;
  210. eventPtr->bindingIndex = triggerState.bindingIndex;
  211. eventPtr->interactionIndex = triggerState.interactionIndex;
  212. eventPtr->startTime = triggerState.startTime;
  213. eventPtr->phase = triggerState.phase;
  214. // Store value.
  215. var valueBuffer = eventPtr->valueData;
  216. context.ReadValue(valueBuffer, valueSizeInBytes);
  217. }
  218. public void Clear()
  219. {
  220. m_EventBuffer.Reset();
  221. m_ActionMapStates.ClearWithCapacity();
  222. }
  223. ~InputActionTrace()
  224. {
  225. DisposeInternal();
  226. }
  227. public override string ToString()
  228. {
  229. if (count == 0)
  230. return "[]";
  231. var str = new StringBuilder();
  232. str.Append('[');
  233. var isFirst = true;
  234. foreach (var eventPtr in this)
  235. {
  236. if (!isFirst)
  237. str.Append(",\n");
  238. str.Append(eventPtr.ToString());
  239. isFirst = false;
  240. }
  241. str.Append(']');
  242. return str.ToString();
  243. }
  244. public void Dispose()
  245. {
  246. UnsubscribeFromAll();
  247. DisposeInternal();
  248. }
  249. private void DisposeInternal()
  250. {
  251. // Nuke clones we made of InputActionMapStates.
  252. for (var i = 0; i < m_ActionMapStateClones.length; ++i)
  253. m_ActionMapStateClones[i].Dispose();
  254. m_EventBuffer.Dispose();
  255. m_ActionMapStates.Clear();
  256. m_ActionMapStateClones.Clear();
  257. if (m_ActionChangeDelegate != null)
  258. {
  259. InputSystem.onActionChange -= m_ActionChangeDelegate;
  260. m_ActionChangeDelegate = null;
  261. }
  262. }
  263. public IEnumerator<ActionEventPtr> GetEnumerator()
  264. {
  265. return new Enumerator(this);
  266. }
  267. IEnumerator IEnumerable.GetEnumerator()
  268. {
  269. return GetEnumerator();
  270. }
  271. private bool m_SubscribedToAll;
  272. private bool m_OnActionChangeHooked;
  273. private InlinedArray<InputAction> m_SubscribedActions;
  274. private InlinedArray<InputActionMap> m_SubscribedActionMaps;
  275. private InputEventBuffer m_EventBuffer;
  276. private InlinedArray<InputActionState> m_ActionMapStates;
  277. private InlinedArray<InputActionState> m_ActionMapStateClones;
  278. private Action<InputAction.CallbackContext> m_CallbackDelegate;
  279. private Action<object, InputActionChange> m_ActionChangeDelegate;
  280. private void HookOnActionChange()
  281. {
  282. if (m_OnActionChangeHooked)
  283. return;
  284. if (m_ActionChangeDelegate == null)
  285. m_ActionChangeDelegate = OnActionChange;
  286. InputSystem.onActionChange += m_ActionChangeDelegate;
  287. m_OnActionChangeHooked = true;
  288. }
  289. private void UnhookOnActionChange()
  290. {
  291. if (!m_OnActionChangeHooked)
  292. return;
  293. InputSystem.onActionChange -= m_ActionChangeDelegate;
  294. m_OnActionChangeHooked = false;
  295. }
  296. private void OnActionChange(object actionOrMap, InputActionChange change)
  297. {
  298. // If we're subscribed to all actions, check if an action got triggered.
  299. if (m_SubscribedToAll)
  300. {
  301. switch (change)
  302. {
  303. case InputActionChange.ActionStarted:
  304. case InputActionChange.ActionPerformed:
  305. case InputActionChange.ActionCanceled:
  306. Debug.Assert(actionOrMap is InputAction, "Expected an action");
  307. var triggeredAction = (InputAction)actionOrMap;
  308. var actionIndex = triggeredAction.m_ActionIndexInState;
  309. var stateForAction = triggeredAction.m_ActionMap.m_State;
  310. var context = new InputAction.CallbackContext
  311. {
  312. m_State = stateForAction,
  313. m_ActionIndex = actionIndex,
  314. };
  315. RecordAction(context);
  316. return;
  317. }
  318. }
  319. // We're only interested in changes to the binding resolution state of actions.
  320. if (change != InputActionChange.BoundControlsAboutToChange)
  321. return;
  322. // Grab the associated action map.
  323. var action = actionOrMap as InputAction;
  324. InputActionMap actionMap;
  325. if (action != null)
  326. actionMap = action.m_ActionMap;
  327. else
  328. {
  329. actionMap = actionOrMap as InputActionMap;
  330. Debug.Assert(actionMap != null, "Given object is neither an InputAction nor an InputActionMap");
  331. }
  332. // Grab the state.
  333. var state = actionMap.m_State;
  334. if (state == null)
  335. {
  336. // Bindings have not been resolved yet for this action map. We shouldn't even be
  337. // on the notification list in this case, but just in case, ignore.
  338. return;
  339. }
  340. // See if we're using the given state.
  341. var stateIndex = m_ActionMapStates.IndexOfReference(state);
  342. if (stateIndex == -1)
  343. return;
  344. // Yes, we are so make our own private copy of its current state.
  345. // NOTE: We do not put these local InputActionMapStates on the global list.
  346. var clone = state.Clone();
  347. m_ActionMapStateClones.Append(clone);
  348. m_ActionMapStates[stateIndex] = clone;
  349. }
  350. /// <summary>
  351. /// A wrapper around <see cref="ActionEvent"/> that automatically translates all the
  352. /// information in events into their high-level representations.
  353. /// </summary>
  354. /// <remarks>
  355. /// For example, instead of returning <see cref="ActionEvent.controlIndex">control indices</see>,
  356. /// it automatically resolves and returns the respective <see cref="InputControl">controls</see>.
  357. /// </remarks>
  358. public unsafe struct ActionEventPtr
  359. {
  360. internal InputActionState m_State;
  361. internal ActionEvent* m_Ptr;
  362. public InputAction action => m_State.GetActionOrNull(m_Ptr->bindingIndex);
  363. public InputActionPhase phase => m_Ptr->phase;
  364. public InputControl control => m_State.controls[m_Ptr->controlIndex];
  365. public IInputInteraction interaction
  366. {
  367. get
  368. {
  369. var index = m_Ptr->interactionIndex;
  370. if (index == InputActionState.kInvalidIndex)
  371. return null;
  372. return m_State.interactions[index];
  373. }
  374. }
  375. public double time => m_Ptr->baseEvent.time;
  376. public double startTime => m_Ptr->startTime;
  377. public double duration => time - startTime;
  378. public int valueSizeInBytes => m_Ptr->valueSizeInBytes;
  379. public object ReadValueAsObject()
  380. {
  381. var valueSizeInBytes = m_Ptr->valueSizeInBytes;
  382. var valuePtr = m_Ptr->valueData;
  383. return control.ReadValueFromBufferAsObject(valuePtr, valueSizeInBytes);
  384. }
  385. public void ReadValue(void* buffer, int bufferSize)
  386. {
  387. var valueSizeInBytes = m_Ptr->valueSizeInBytes;
  388. ////REVIEW: do we want more checking than this?
  389. if (bufferSize < valueSizeInBytes)
  390. throw new ArgumentException(
  391. $"Expected buffer of at least {valueSizeInBytes} bytes but got buffer of just {bufferSize} bytes instead",
  392. nameof(bufferSize));
  393. UnsafeUtility.MemCpy(buffer, m_Ptr->valueData, valueSizeInBytes);
  394. }
  395. public TValue ReadValue<TValue>()
  396. where TValue : struct
  397. {
  398. var valueSizeInBytes = m_Ptr->valueSizeInBytes;
  399. ////REVIEW: do we want more checking than this?
  400. if (UnsafeUtility.SizeOf<TValue>() != valueSizeInBytes)
  401. throw new InvalidOperationException(
  402. $"Cannot read a value of type '{typeof(TValue).Name}' with size {UnsafeUtility.SizeOf<TValue>()} from event on action '{action}' with value size {valueSizeInBytes}");
  403. var result = new TValue();
  404. var resultPtr = UnsafeUtility.AddressOf(ref result);
  405. UnsafeUtility.MemCpy(resultPtr, m_Ptr->valueData, valueSizeInBytes);
  406. return result;
  407. }
  408. public override string ToString()
  409. {
  410. if (m_Ptr == null)
  411. return "<null>";
  412. var actionName = action.actionMap != null ? $"{action.actionMap.name}/{action.name}" : action.name;
  413. return $"{{ action={actionName} phase={phase} time={time} control={control} value={ReadValueAsObject()} interaction={interaction} duration={duration} }}";
  414. }
  415. }
  416. private unsafe struct Enumerator : IEnumerator<ActionEventPtr>
  417. {
  418. private readonly InputActionTrace m_Trace;
  419. private readonly ActionEvent* m_Buffer;
  420. private readonly int m_EventCount;
  421. private ActionEvent* m_CurrentEvent;
  422. private int m_CurrentIndex;
  423. public Enumerator(InputActionTrace trace)
  424. {
  425. m_Trace = trace;
  426. m_Buffer = (ActionEvent*)trace.m_EventBuffer.bufferPtr.data;
  427. m_EventCount = trace.m_EventBuffer.eventCount;
  428. m_CurrentEvent = null;
  429. m_CurrentIndex = 0;
  430. }
  431. public bool MoveNext()
  432. {
  433. if (m_CurrentIndex == m_EventCount)
  434. return false;
  435. if (m_CurrentEvent == null)
  436. {
  437. m_CurrentEvent = m_Buffer;
  438. return m_CurrentEvent != null;
  439. }
  440. Debug.Assert(m_CurrentEvent != null);
  441. ++m_CurrentIndex;
  442. if (m_CurrentIndex == m_EventCount)
  443. return false;
  444. m_CurrentEvent = (ActionEvent*)InputEvent.GetNextInMemory((InputEvent*)m_CurrentEvent);
  445. return true;
  446. }
  447. public void Reset()
  448. {
  449. m_CurrentEvent = null;
  450. m_CurrentIndex = 0;
  451. }
  452. public void Dispose()
  453. {
  454. }
  455. public ActionEventPtr Current
  456. {
  457. get
  458. {
  459. var state = m_Trace.m_ActionMapStates[m_CurrentEvent->stateIndex];
  460. return new ActionEventPtr
  461. {
  462. m_State = state,
  463. m_Ptr = m_CurrentEvent,
  464. };
  465. }
  466. }
  467. object IEnumerator.Current => Current;
  468. }
  469. }
  470. }