A game about forced loneliness, made by TACStudios
at master 736 lines 32 kB view raw
1using System; 2using System.Collections; 3using System.Collections.Generic; 4using System.Runtime.InteropServices; 5using System.Text; 6using Unity.Collections.LowLevel.Unsafe; 7using UnityEngine.InputSystem.LowLevel; 8 9////REVIEW: why not switch to this being the default mechanism? seems like this could allow us to also solve 10//// the actions-update-when-not-expected problem; plus give us access to easy polling 11 12////REVIEW: should this automatically unsubscribe itself on disposal? 13 14////TODO: make it possible to persist this same way that it should be possible to persist InputEventTrace 15 16////TODO: make this one thread-safe 17 18////TODO: add random access capability 19 20////TODO: protect traces against controls changing configuration (if state layouts change, we're affected) 21 22namespace UnityEngine.InputSystem.Utilities 23{ 24 /// <summary> 25 /// Records the triggering of actions into a sequence of events that can be replayed at will. 26 /// </summary> 27 /// <remarks> 28 /// This is an alternate way to the callback-based responses (such as <see cref="InputAction.performed"/>) 29 /// of <see cref="InputAction">input actions</see>. Instead of executing response code right away whenever 30 /// an action triggers, an <see cref="RecordAction">event is recorded</see> which can then be queried on demand. 31 /// 32 /// The recorded data will stay valid even if the bindings on the actions are changed (e.g. by enabling a different 33 /// set of bindings through altering <see cref="InputAction.bindingMask"/> or <see cref="InputActionMap.devices"/> or 34 /// when modifying the paths of bindings altogether). Note, however, that when this happens, a trace will have 35 /// to make a private copy of the data that stores the binding resolution state. This means that there can be 36 /// GC allocation spike when reconfiguring actions that have recorded data in traces. 37 /// 38 /// <example> 39 /// <code> 40 /// var trace = new InputActionTrace(); 41 /// 42 /// // Subscribe trace to single action. 43 /// // (Use UnsubscribeFrom to unsubscribe) 44 /// trace.SubscribeTo(myAction); 45 /// 46 /// // Subscribe trace to entire action map. 47 /// // (Use UnsubscribeFrom to unsubscribe) 48 /// trace.SubscribeTo(myActionMap); 49 /// 50 /// // Subscribe trace to all actions in the system. 51 /// trace.SubscribeToAll(); 52 /// 53 /// // Record a single triggering of an action. 54 /// myAction.performed += 55 /// ctx => 56 /// { 57 /// if (ctx.ReadValue&lt;float&gt;() &gt; 0.5f) 58 /// trace.RecordAction(ctx); 59 /// }; 60 /// 61 /// // Output trace to console. 62 /// Debug.Log(string.Join(",\n", trace)); 63 /// 64 /// // Walk through all recorded actions and then clear trace. 65 /// foreach (var record in trace) 66 /// { 67 /// Debug.Log($"{record.action} was {record.phase} by control {record.control} at {record.time}"); 68 /// 69 /// // To read out the value, you either have to know the value type or read the 70 /// // value out as a generic byte buffer. Here we assume that the value type is 71 /// // float. 72 /// 73 /// Debug.Log("Value: " + record.ReadValue&lt;float&gt;()); 74 /// 75 /// // An alternative is read the value as an object. In this case, you don't have 76 /// // to know the value type but there will be a boxed object allocation. 77 /// Debug.Log("Value: " + record.ReadValueAsObject()); 78 /// } 79 /// trace.Clear(); 80 /// 81 /// // Unsubscribe trace from everything. 82 /// trace.UnsubscribeFromAll(); 83 /// 84 /// // Release memory held by trace. 85 /// trace.Dispose(); 86 /// </code> 87 /// </example> 88 /// </remarks> 89 /// <seealso cref="InputAction.started"/> 90 /// <seealso cref="InputAction.performed"/> 91 /// <seealso cref="InputAction.canceled"/> 92 /// <seealso cref="InputSystem.onActionChange"/> 93 public sealed class InputActionTrace : IEnumerable<InputActionTrace.ActionEventPtr>, IDisposable 94 { 95 ////REVIEW: this is of limited use without having access to ActionEvent 96 /// <summary> 97 /// Directly access the underlying raw memory queue. 98 /// </summary> 99 public InputEventBuffer buffer => m_EventBuffer; 100 101 /// <summary> 102 /// Returns the number of events in the associated event buffer. 103 /// </summary> 104 public int count => m_EventBuffer.eventCount; 105 106 /// <summary> 107 /// Constructs a new default initialized <c>InputActionTrace</c>. 108 /// </summary> 109 /// <remarks> 110 /// When you use this constructor, the new InputActionTrace object does not start recording any actions. 111 /// To record actions, you must explicitly set them up after creating the object. 112 /// Alternatively, you can use one of the other constructor overloads which begin recording actions immediately. 113 /// </remarks> 114 /// <seealso cref="SubscribeTo(InputAction)"/> 115 /// <seealso cref="SubscribeTo(InputActionMap)"/> 116 /// <seealso cref="SubscribeToAll"/> 117 public InputActionTrace() 118 { 119 } 120 121 /// <summary> 122 /// Constructs a new <c>InputActionTrace</c> that records <paramref name="action"/>. 123 /// </summary> 124 /// <param name="action">The action to be recorded.</param> 125 /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="action"/> is <c>null</c>.</exception> 126 public InputActionTrace(InputAction action) 127 { 128 if (action == null) 129 throw new ArgumentNullException(nameof(action)); 130 SubscribeTo(action); 131 } 132 133 /// <summary> 134 /// Constructs a new <c>InputActionTrace</c> that records all actions in <paramref name="actionMap"/>. 135 /// </summary> 136 /// <param name="actionMap">The action-map containing actions to be recorded.</param> 137 /// <exception cref="System.ArgumentNullException">Thrown if <paramref name="action"/> is <c>null</c>.</exception> 138 public InputActionTrace(InputActionMap actionMap) 139 { 140 if (actionMap == null) 141 throw new ArgumentNullException(nameof(actionMap)); 142 SubscribeTo(actionMap); 143 } 144 145 /// <summary> 146 /// Record any action getting triggered anywhere. 147 /// </summary> 148 /// <remarks> 149 /// This does not require the trace to actually hook into every single action or action map in the system. 150 /// Instead, the trace will listen to <see cref="InputSystem.onActionChange"/> and automatically record 151 /// every triggered action. 152 /// </remarks> 153 /// <seealso cref="SubscribeTo(InputAction)"/> 154 /// <seealso cref="SubscribeTo(InputActionMap)"/> 155 public void SubscribeToAll() 156 { 157 if (m_SubscribedToAll) 158 return; 159 160 HookOnActionChange(); 161 m_SubscribedToAll = true; 162 163 // Remove manually created subscriptions. 164 while (m_SubscribedActions.length > 0) 165 UnsubscribeFrom(m_SubscribedActions[m_SubscribedActions.length - 1]); 166 while (m_SubscribedActionMaps.length > 0) 167 UnsubscribeFrom(m_SubscribedActionMaps[m_SubscribedActionMaps.length - 1]); 168 } 169 170 /// <summary> 171 /// Unsubscribes from all actions currently being recorded. 172 /// </summary> 173 /// <seealso cref="UnsubscribeFrom(InputAction)"/> 174 /// <seealso cref="UnsubscribeFrom(InputActionMap)"/> 175 public void UnsubscribeFromAll() 176 { 177 // Only unhook from OnActionChange if we don't have any recorded actions. If we do have 178 // any, we still need the callback to be notified about when binding data changes. 179 if (count == 0) 180 UnhookOnActionChange(); 181 182 m_SubscribedToAll = false; 183 184 while (m_SubscribedActions.length > 0) 185 UnsubscribeFrom(m_SubscribedActions[m_SubscribedActions.length - 1]); 186 while (m_SubscribedActionMaps.length > 0) 187 UnsubscribeFrom(m_SubscribedActionMaps[m_SubscribedActionMaps.length - 1]); 188 } 189 190 /// <summary> 191 /// Subscribes to <paramref name="action"/>. 192 /// </summary> 193 /// <param name="action">The action to be recorded.</param> 194 /// <remarks> 195 /// **Note:** This method does not prevent you from subscribing to the same action multiple times. 196 /// If you subscribe to the same action multiple times, your event buffer will contain duplicate entries. 197 /// </remarks> 198 /// <exception cref="ArgumentNullException">If <paramref name="action"/> is <c>null</c>.</exception> 199 /// <seealso cref="SubscribeTo(InputActionMap)"/> 200 /// <seealso cref="SubscribeToAll"/> 201 public void SubscribeTo(InputAction action) 202 { 203 if (action == null) 204 throw new ArgumentNullException(nameof(action)); 205 206 if (m_CallbackDelegate == null) 207 m_CallbackDelegate = RecordAction; 208 209 action.performed += m_CallbackDelegate; 210 action.started += m_CallbackDelegate; 211 action.canceled += m_CallbackDelegate; 212 213 m_SubscribedActions.AppendWithCapacity(action); 214 } 215 216 /// <summary> 217 /// Subscribes to all actions contained within <paramref name="actionMap"/>. 218 /// </summary> 219 /// <param name="actionMap">The action-map containing all actions to be recorded.</param> 220 /// <remarks> 221 /// **Note:** This method does not prevent you from subscribing to the same action multiple times. 222 /// If you subscribe to the same action multiple times, your event buffer will contain duplicate entries. 223 /// </remarks> 224 /// <exception cref="ArgumentNullException">Thrown if <paramref name="actionMap"/> is null.</exception> 225 /// <seealso cref="SubscribeTo(InputAction)"/> 226 /// <seealso cref="SubscribeToAll"/> 227 public void SubscribeTo(InputActionMap actionMap) 228 { 229 if (actionMap == null) 230 throw new ArgumentNullException(nameof(actionMap)); 231 232 if (m_CallbackDelegate == null) 233 m_CallbackDelegate = RecordAction; 234 235 actionMap.actionTriggered += m_CallbackDelegate; 236 237 m_SubscribedActionMaps.AppendWithCapacity(actionMap); 238 } 239 240 /// <summary> 241 /// Unsubscribes from an action, if that action was previously subscribed to. 242 /// </summary> 243 /// <param name="action">The action to unsubscribe from.</param> 244 /// <remarks> 245 /// **Note:** This method has no side effects if you attempt to unsubscribe from an action that you have not previously subscribed to. 246 /// </remarks> 247 /// <exception cref="ArgumentNullException">Thrown if <paramref name="action"/> is <c>null</c>.</exception> 248 /// <seealso cref="UnsubscribeFrom(InputActionMap)"/> 249 /// <seealso cref="UnsubscribeFromAll"/> 250 public void UnsubscribeFrom(InputAction action) 251 { 252 if (action == null) 253 throw new ArgumentNullException(nameof(action)); 254 255 if (m_CallbackDelegate == null) 256 return; 257 258 action.performed -= m_CallbackDelegate; 259 action.started -= m_CallbackDelegate; 260 action.canceled -= m_CallbackDelegate; 261 262 var index = m_SubscribedActions.IndexOfReference(action); 263 if (index != -1) 264 m_SubscribedActions.RemoveAtWithCapacity(index); 265 } 266 267 /// <summary> 268 /// Unsubscribes from all actions included in <paramref name="actionMap"/>. 269 /// </summary> 270 /// <param name="actionMap">The action-map containing actions to unsubscribe from.</param> 271 /// <remarks> 272 /// **Note:** This method has no side effects if you attempt to unsubscribe from an action-map that you have not previously subscribed to. 273 /// </remarks> 274 /// <exception cref="ArgumentNullException">Thrown if <paramref name="actionMap"/> is <c>null</c>.</exception> 275 /// <seealso cref="UnsubscribeFrom(InputAction)"/> 276 /// <seealso cref="UnsubscribeFromAll"/> 277 public void UnsubscribeFrom(InputActionMap actionMap) 278 { 279 if (actionMap == null) 280 throw new ArgumentNullException(nameof(actionMap)); 281 282 if (m_CallbackDelegate == null) 283 return; 284 285 actionMap.actionTriggered -= m_CallbackDelegate; 286 287 var index = m_SubscribedActionMaps.IndexOfReference(actionMap); 288 if (index != -1) 289 m_SubscribedActionMaps.RemoveAtWithCapacity(index); 290 } 291 292 /// <summary> 293 /// Record the triggering of an action as an <see cref="ActionEventPtr">action event</see>. 294 /// </summary> 295 /// <param name="context"></param> 296 /// <see cref="InputAction.performed"/> 297 /// <see cref="InputAction.started"/> 298 /// <see cref="InputAction.canceled"/> 299 /// <see cref="InputActionMap.actionTriggered"/> 300 public unsafe void RecordAction(InputAction.CallbackContext context) 301 { 302 // Find/add state. 303 var stateIndex = m_ActionMapStates.IndexOfReference(context.m_State); 304 if (stateIndex == -1) 305 stateIndex = m_ActionMapStates.AppendWithCapacity(context.m_State); 306 307 // Make sure we get notified if there's a change to binding setups. 308 HookOnActionChange(); 309 310 // Allocate event. 311 var valueSizeInBytes = context.valueSizeInBytes; 312 var eventPtr = 313 (ActionEvent*)m_EventBuffer.AllocateEvent(ActionEvent.GetEventSizeWithValueSize(valueSizeInBytes)); 314 315 // Initialize event. 316 ref var triggerState = ref context.m_State.actionStates[context.m_ActionIndex]; 317 eventPtr->baseEvent.type = ActionEvent.Type; 318 eventPtr->baseEvent.time = triggerState.time; 319 eventPtr->stateIndex = stateIndex; 320 eventPtr->controlIndex = triggerState.controlIndex; 321 eventPtr->bindingIndex = triggerState.bindingIndex; 322 eventPtr->interactionIndex = triggerState.interactionIndex; 323 eventPtr->startTime = triggerState.startTime; 324 eventPtr->phase = triggerState.phase; 325 326 // Store value. 327 // NOTE: If the action triggered from a composite, this stores the value as 328 // read from the composite. 329 // NOTE: Also, the value we store is a fully processed value. 330 var valueBuffer = eventPtr->valueData; 331 context.ReadValue(valueBuffer, valueSizeInBytes); 332 } 333 334 /// <summary> 335 /// Clears all recorded data. 336 /// </summary> 337 /// <remarks> 338 /// **Note:** This method does not unsubscribe any actions that the instance is listening to, so after clearing the recorded data, new input on those subscribed actions will continue to be recorded. 339 /// </remarks> 340 public void Clear() 341 { 342 m_EventBuffer.Reset(); 343 m_ActionMapStates.ClearWithCapacity(); 344 } 345 346 ~InputActionTrace() 347 { 348 DisposeInternal(); 349 } 350 351 /// <inheritdoc/> 352 public override string ToString() 353 { 354 if (count == 0) 355 return "[]"; 356 357 var str = new StringBuilder(); 358 str.Append('['); 359 var isFirst = true; 360 foreach (var eventPtr in this) 361 { 362 if (!isFirst) 363 str.Append(",\n"); 364 str.Append(eventPtr.ToString()); 365 isFirst = false; 366 } 367 str.Append(']'); 368 return str.ToString(); 369 } 370 371 /// <inheritdoc/> 372 public void Dispose() 373 { 374 UnsubscribeFromAll(); 375 DisposeInternal(); 376 } 377 378 private void DisposeInternal() 379 { 380 // Nuke clones we made of InputActionMapStates. 381 for (var i = 0; i < m_ActionMapStateClones.length; ++i) 382 m_ActionMapStateClones[i].Dispose(); 383 384 m_EventBuffer.Dispose(); 385 m_ActionMapStates.Clear(); 386 m_ActionMapStateClones.Clear(); 387 388 if (m_ActionChangeDelegate != null) 389 { 390 InputSystem.onActionChange -= m_ActionChangeDelegate; 391 m_ActionChangeDelegate = null; 392 } 393 } 394 395 /// <summary> 396 /// Returns an enumerator that enumerates all action events recorded for this instance. 397 /// </summary> 398 /// <returns>Enumerator instance, never <c>null</c>.</returns> 399 /// <seealso cref="ActionEventPtr"/> 400 public IEnumerator<ActionEventPtr> GetEnumerator() 401 { 402 return new Enumerator(this); 403 } 404 405 IEnumerator IEnumerable.GetEnumerator() 406 { 407 return GetEnumerator(); 408 } 409 410 private bool m_SubscribedToAll; 411 private bool m_OnActionChangeHooked; 412 private InlinedArray<InputAction> m_SubscribedActions; 413 private InlinedArray<InputActionMap> m_SubscribedActionMaps; 414 private InputEventBuffer m_EventBuffer; 415 private InlinedArray<InputActionState> m_ActionMapStates; 416 private InlinedArray<InputActionState> m_ActionMapStateClones; 417 private Action<InputAction.CallbackContext> m_CallbackDelegate; 418 private Action<object, InputActionChange> m_ActionChangeDelegate; 419 420 private void HookOnActionChange() 421 { 422 if (m_OnActionChangeHooked) 423 return; 424 425 if (m_ActionChangeDelegate == null) 426 m_ActionChangeDelegate = OnActionChange; 427 428 InputSystem.onActionChange += m_ActionChangeDelegate; 429 m_OnActionChangeHooked = true; 430 } 431 432 private void UnhookOnActionChange() 433 { 434 if (!m_OnActionChangeHooked) 435 return; 436 437 InputSystem.onActionChange -= m_ActionChangeDelegate; 438 m_OnActionChangeHooked = false; 439 } 440 441 private void OnActionChange(object actionOrMapOrAsset, InputActionChange change) 442 { 443 // If we're subscribed to all actions, check if an action got triggered. 444 if (m_SubscribedToAll) 445 { 446 switch (change) 447 { 448 case InputActionChange.ActionStarted: 449 case InputActionChange.ActionPerformed: 450 case InputActionChange.ActionCanceled: 451 Debug.Assert(actionOrMapOrAsset is InputAction, "Expected an action"); 452 var triggeredAction = (InputAction)actionOrMapOrAsset; 453 var actionIndex = triggeredAction.m_ActionIndexInState; 454 var stateForAction = triggeredAction.m_ActionMap.m_State; 455 456 var context = new InputAction.CallbackContext 457 { 458 m_State = stateForAction, 459 m_ActionIndex = actionIndex, 460 }; 461 462 RecordAction(context); 463 464 return; 465 } 466 } 467 468 // We're only interested in changes to the binding resolution state of actions. 469 if (change != InputActionChange.BoundControlsAboutToChange) 470 return; 471 472 // Grab the associated action map(s). 473 if (actionOrMapOrAsset is InputAction action) 474 CloneActionStateBeforeBindingsChange(action.m_ActionMap); 475 else if (actionOrMapOrAsset is InputActionMap actionMap) 476 CloneActionStateBeforeBindingsChange(actionMap); 477 else if (actionOrMapOrAsset is InputActionAsset actionAsset) 478 foreach (var actionMapInAsset in actionAsset.actionMaps) 479 CloneActionStateBeforeBindingsChange(actionMapInAsset); 480 else 481 Debug.Assert(false, "Expected InputAction, InputActionMap or InputActionAsset"); 482 } 483 484 private void CloneActionStateBeforeBindingsChange(InputActionMap actionMap) 485 { 486 // Grab the state. 487 var state = actionMap.m_State; 488 if (state == null) 489 { 490 // Bindings have not been resolved yet for this action map. We shouldn't even be 491 // on the notification list in this case, but just in case, ignore. 492 return; 493 } 494 495 // See if we're using the given state. 496 var stateIndex = m_ActionMapStates.IndexOfReference(state); 497 if (stateIndex == -1) 498 return; 499 500 // Yes, we are so make our own private copy of its current state. 501 // NOTE: We do not put these local InputActionMapStates on the global list. 502 var clone = state.Clone(); 503 m_ActionMapStateClones.Append(clone); 504 m_ActionMapStates[stateIndex] = clone; 505 } 506 507 /// <summary> 508 /// A wrapper around <see cref="ActionEvent"/> that automatically translates all the 509 /// information in events into their high-level representations. 510 /// </summary> 511 /// <remarks> 512 /// For example, instead of returning <see cref="ActionEvent.controlIndex">control indices</see>, 513 /// it automatically resolves and returns the respective <see cref="InputControl">controls</see>. 514 /// </remarks> 515 public unsafe struct ActionEventPtr 516 { 517 internal InputActionState m_State; 518 internal ActionEvent* m_Ptr; 519 520 /// <summary> 521 /// The <see cref="InputAction"/> associated with this action event. 522 /// </summary> 523 public InputAction action => m_State.GetActionOrNull(m_Ptr->bindingIndex); 524 525 /// <summary> 526 /// The <see cref="InputActionPhase"/> associated with this action event. 527 /// </summary> 528 /// <seealso cref="InputAction.phase"/> 529 /// <seealso cref="InputAction.CallbackContext.phase"/> 530 public InputActionPhase phase => m_Ptr->phase; 531 532 /// <summary> 533 /// The <see cref="InputControl"/> instance associated with this action event. 534 /// </summary> 535 public InputControl control => m_State.controls[m_Ptr->controlIndex]; 536 537 /// <summary> 538 /// The <see cref="IInputInteraction"/> instance associated with this action event if applicable, or <c>null</c> if the action event is not associated with an input interaction. 539 /// </summary> 540 public IInputInteraction interaction 541 { 542 get 543 { 544 var index = m_Ptr->interactionIndex; 545 if (index == InputActionState.kInvalidIndex) 546 return null; 547 548 return m_State.interactions[index]; 549 } 550 } 551 552 /// <summary> 553 /// The time, in seconds since your game or app started, that the event occurred. 554 /// </summary> 555 /// <remarks> 556 /// Times are in seconds and progress linearly in real-time. The timeline is the same as for <see cref="Time.realtimeSinceStartup"/>. 557 /// </remarks> 558 public double time => m_Ptr->baseEvent.time; 559 560 /// <summary> 561 /// The time, in seconds since your game or app started, that the <see cref="phase"/> transitioned into <see cref="InputActionPhase.Started"/>. 562 /// </summary> 563 public double startTime => m_Ptr->startTime; 564 565 /// <summary> 566 /// The duration, in seconds, that has elapsed between when this event was generated and when the 567 /// action <see cref="phase"/> transitioned to <see cref="InputActionPhase.Started"/> and has remained active. 568 /// </summary> 569 public double duration => time - startTime; 570 571 /// <summary> 572 /// The size, in bytes, of the value associated with this action event. 573 /// </summary> 574 public int valueSizeInBytes => m_Ptr->valueSizeInBytes; 575 576 /// <summary> 577 /// Reads the value associated with this event as an <c>object</c>. 578 /// </summary> 579 /// <returns><c>object</c> representing the value of this action event.</returns> 580 /// <seealso cref="ReadOnlyArray{TValue}"/> 581 /// <seealso cref="ReadValue(void*, int)"/> 582 public object ReadValueAsObject() 583 { 584 if (m_Ptr == null) 585 throw new InvalidOperationException("ActionEventPtr is invalid"); 586 587 var valuePtr = m_Ptr->valueData; 588 589 // Check if the value came from a composite. 590 var bindingIndex = m_Ptr->bindingIndex; 591 if (m_State.bindingStates[bindingIndex].isPartOfComposite) 592 { 593 // Yes, so have to put the value/struct data we read into a boxed 594 // object based on the value type of the composite. 595 596 var compositeBindingIndex = m_State.bindingStates[bindingIndex].compositeOrCompositeBindingIndex; 597 var compositeIndex = m_State.bindingStates[compositeBindingIndex].compositeOrCompositeBindingIndex; 598 var composite = m_State.composites[compositeIndex]; 599 Debug.Assert(composite != null, "NULL composite instance"); 600 601 var valueType = composite.valueType; 602 if (valueType == null) 603 throw new InvalidOperationException($"Cannot read value from Composite '{composite}' which does not have a valueType set"); 604 605 return Marshal.PtrToStructure(new IntPtr(valuePtr), valueType); 606 } 607 608 // Expecting action to only trigger from part bindings or bindings outside of composites. 609 Debug.Assert(!m_State.bindingStates[bindingIndex].isComposite, "Action should not have triggered directly from a composite binding"); 610 611 // Read value through InputControl. 612 var valueSizeInBytes = m_Ptr->valueSizeInBytes; 613 return control.ReadValueFromBufferAsObject(valuePtr, valueSizeInBytes); 614 } 615 616 /// <summary> 617 /// Reads the value associated with this event into the contiguous memory buffer defined by <c>[buffer, buffer + bufferSize)</c>. 618 /// </summary> 619 /// <param name="buffer">Pointer to the contiguous memory buffer to write value data to.</param> 620 /// <param name="bufferSize">The size, in bytes, of the contiguous buffer pointed to by <paramref name="buffer"/>.</param> 621 /// <exception cref="NullReferenceException">If <paramref name="buffer"/> is <c>null</c>.</exception> 622 /// <exception cref="ArgumentException">If the given <paramref name="bufferSize"/> is less than the number of bytes required to write the event value to <paramref name="buffer"/>.</exception> 623 /// <seealso cref="ReadValueAsObject"/> 624 /// <seealso cref="ReadValue{TValue}"/> 625 public void ReadValue(void* buffer, int bufferSize) 626 { 627 var valueSizeInBytes = m_Ptr->valueSizeInBytes; 628 629 ////REVIEW: do we want more checking than this? 630 if (bufferSize < valueSizeInBytes) 631 throw new ArgumentException( 632 $"Expected buffer of at least {valueSizeInBytes} bytes but got buffer of just {bufferSize} bytes instead", 633 nameof(bufferSize)); 634 635 UnsafeUtility.MemCpy(buffer, m_Ptr->valueData, valueSizeInBytes); 636 } 637 638 /// <summary> 639 /// Reads the value associated with this event as an object of type <typeparamref name="TValue"/>. 640 /// </summary> 641 /// <typeparam name="TValue">The event value type to be used.</typeparam> 642 /// <returns>Object of type <typeparamref name="TValue"/>.</returns> 643 /// <exception cref="InvalidOperationException">In case the size of <typeparamref name="TValue"/> does not match the size of the value associated with this event.</exception> 644 public TValue ReadValue<TValue>() 645 where TValue : struct 646 { 647 var valueSizeInBytes = m_Ptr->valueSizeInBytes; 648 649 ////REVIEW: do we want more checking than this? 650 if (UnsafeUtility.SizeOf<TValue>() != valueSizeInBytes) 651 throw new InvalidOperationException( 652 $"Cannot read a value of type '{typeof(TValue).Name}' with size {UnsafeUtility.SizeOf<TValue>()} from event on action '{action}' with value size {valueSizeInBytes}"); 653 654 var result = new TValue(); 655 var resultPtr = UnsafeUtility.AddressOf(ref result); 656 UnsafeUtility.MemCpy(resultPtr, m_Ptr->valueData, valueSizeInBytes); 657 658 return result; 659 } 660 661 /// <inheritdoc/> 662 public override string ToString() 663 { 664 if (m_Ptr == null) 665 return "<null>"; 666 667 var actionName = action.actionMap != null ? $"{action.actionMap.name}/{action.name}" : action.name; 668 return $"{{ action={actionName} phase={phase} time={time} control={control} value={ReadValueAsObject()} interaction={interaction} duration={duration} }}"; 669 } 670 } 671 672 private unsafe struct Enumerator : IEnumerator<ActionEventPtr> 673 { 674 private readonly InputActionTrace m_Trace; 675 private readonly ActionEvent* m_Buffer; 676 private readonly int m_EventCount; 677 private ActionEvent* m_CurrentEvent; 678 private int m_CurrentIndex; 679 680 public Enumerator(InputActionTrace trace) 681 { 682 m_Trace = trace; 683 m_Buffer = (ActionEvent*)trace.m_EventBuffer.bufferPtr.data; 684 m_EventCount = trace.m_EventBuffer.eventCount; 685 m_CurrentEvent = null; 686 m_CurrentIndex = 0; 687 } 688 689 public bool MoveNext() 690 { 691 if (m_CurrentIndex == m_EventCount) 692 return false; 693 694 if (m_CurrentEvent == null) 695 { 696 m_CurrentEvent = m_Buffer; 697 return m_CurrentEvent != null; 698 } 699 700 Debug.Assert(m_CurrentEvent != null); 701 702 ++m_CurrentIndex; 703 if (m_CurrentIndex == m_EventCount) 704 return false; 705 706 m_CurrentEvent = (ActionEvent*)InputEvent.GetNextInMemory((InputEvent*)m_CurrentEvent); 707 return true; 708 } 709 710 public void Reset() 711 { 712 m_CurrentEvent = null; 713 m_CurrentIndex = 0; 714 } 715 716 public void Dispose() 717 { 718 } 719 720 public ActionEventPtr Current 721 { 722 get 723 { 724 var state = m_Trace.m_ActionMapStates[m_CurrentEvent->stateIndex]; 725 return new ActionEventPtr 726 { 727 m_State = state, 728 m_Ptr = m_CurrentEvent, 729 }; 730 } 731 } 732 733 object IEnumerator.Current => Current; 734 } 735 } 736}