Creating Custom Events

The XR Recorder can record almost any type of event and the framework allows for the creation of custom events.

For this tutorial, we will implement a custom event type, capturing and playback method: Let’s suppose that we have a feature that gives a randomly generated number to the user on each action. This could be a unique identifier for example that would be given by some external service to the application. Of course, we should not make the same request during the playback of the application as that would not give us the same number that was given in the session.

The XR Recorder framework, expects the user to create 3 different classes in order to implement a custom event:

  1. A class that will hold the event data (implements the ICapturedEvent interface)

  2. A class that will capture the event during a session (implements the ICaptor interface)

  3. A class that will play back said event when viewing a recording (implements the IInterpreter interface)

Example Feature

We begin by creating the example script that will “fetch” this unique number:

using System;
using System.Collections;
using System.Collections.Generic;
using MAGES;
using MAGES.Recorder;
using UnityEngine;
using UnityEngine.Events;

public class UniqueNumberScript : MonoBehaviour
{
    public UnityEvent<int> onNewNumber = new UnityEvent<int>();

    private System.Random random = new System.Random();

    // Start is called before the first frame update
    void Start()
    {
        Hub.Instance.Get<SceneGraphModule>().ActionPerformed(OnActionPerformed);
    }

    public void InvokeNumber(int i)
    {
        Debug.Log($"Unique Number :: {i}");
        onNewNumber.Invoke(i);
    }

    private void OnActionPerformed(BaseActionData data, bool skipped)
    {
        if (data.IsStartAction)
        {
            return;
        }

        // If it's not recording, then we are in playback or "demo" mode.
        if (!Hub.Instance.Get<RecorderModule>().Armed)
        {
            return;
        }

        // Generate random integer
        int number = GetUniqueNumber();
        InvokeNumber(number);
    }

    private int GetUniqueNumber()
    {
        return random.Next();
    }
}

We then create a new GameObject and attach the UniqueNumberScript component to it.

If we run the application now, we’ll be able to see that our UniqueNumberScript is generating numbers for each action performed

../../../_images/custom-01.png

Creating the event

Let’s start adding support for recording this functionality by first determining what needs to be captured: for a simple feature like this, there are mostly two pieces of information that need to be captured:

  • When a unique number was generated, and

  • What that unique number was

The XR Recorder is shipped with templates for creating custom events, captors and interprets, so we will create a new Captured Event class (named “UniqueNumberCapturedEvent”) by right-clicking in the project pane and proceeding as shown below:

../../../_images/custom-02.png

Opening the class in the editor, we can see that the XR Recorder has already added some fields that are required for any type of Captured Event:

using System.Collections;
using System.Collections.Generic;
using MAGES.Recorder;
using UnityEngine;

[System.Serializable]
public class UniqueNumberCapturedEvent : ICapturedEvent
{
    [SerializeField]
    private double createTime;

    [SerializeField]
    private double captureTime;

    [SerializeField]
    private long createFrame;

    /// <summary>
    /// Gets the estimated time (wall-clock time) when the event was created (NOT captured).
    /// </summary>
    public double CreateTime => createTime;

    /// <summary>
    /// Gets the estimated time (wall-clock time) when the event was captured.
    /// </summary>
    public double CaptureTime => captureTime;

    /// <summary>
    /// Gets the frame the event was captured on.
    /// </summary>
    public long CreateFrame => createFrame;
}

Generally, the only field that we care about for the scope of this tutorial is the CaptureTime field. This is the exact distance in time from the beginning of the recording when the event was captured.

Let’s add the unique number field and a constructor for this class:

using System.Collections;
using System.Collections.Generic;
using MAGES.Recorder;
using UnityEngine;

[System.Serializable]
public class UniqueNumberCapturedEvent : ICapturedEvent
{
    [SerializeField]
    private double createTime;

    [SerializeField]
    private double captureTime;

    [SerializeField]
    private long createFrame;

    // >>> The data itself
    [SerializeField]
    private int uniqueNumber;

    // >>> The constructor
    public UniqueNumberCapturedEvent(int uniqueNumber, long frameCount, double captureTime)
    {
        this.uniqueNumber = uniqueNumber;
        this.captureTime = captureTime;
        createFrame = frameCount;
        createTime = Time.realtimeSinceStartupAsDouble;
    }

    /// <summary>
    /// Gets the estimated time (wall-clock time) when the event was created (NOT captured).
    /// </summary>
    public double CreateTime => createTime;

    /// <summary>
    /// Gets the estimated time (wall-clock time) when the event was captured.
    /// </summary>
    public double CaptureTime => captureTime;

    /// <summary>
    /// Gets the frame the event was captured on.
    /// </summary>
    public long CreateFrame => createFrame;

    // >>> The accessor to the data
    public int UniqueNumber => uniqueNumber;
}

And that’s all we need for the event class! Now on to capturing the event.

Capturing the event

To capture the event we just created, we will need to create a class that hooks into the onNewNumber event, create the UniqueNumberCapturedEvent and push it to the instance of the recorder. We will start in the same manner as previously, by using the right-click menu to create an ICaptor class named UniqueNumberCaptor:

../../../_images/custom-03.png

Right ahead, we see that the recorder has added some preliminary code.

using System.Collections.Generic;
using MAGES.Recorder;
using UnityEngine;

public class UniqueNumberCaptor : ICaptor
{
    /// <summary>
    /// Gets a value indicating whether the captor needs to be polled.
    /// </summary>
    public bool IsPolling => true;

    /// <summary>
    /// Initializes this captor.
    /// </summary>
    public void Init()
    {
    }

    /// <summary>
    /// Polls the captor for new data.
    /// </summary>
    /// <param name="timestamp">The timestamp.</param>
    /// <remarks>
    /// This method will only be called if <see cref="IsPolling"/> is true.
    /// </remarks>
    public void Poll(double timestamp)
    {
    }
}

And this is what the script looks alike after implementing the captor:

using System;
using System.Collections;
using System.Collections.Generic;
using MAGES;
using MAGES.Recorder;
using UnityEngine;

public class UniqueNumberCaptor : ICaptor
{
    /// <summary>
    /// >>> The recorder module
    /// </summary>
    private RecorderModule rm;

    /// <summary>
    /// Gets a value indicating whether the captor needs to be polled.
    /// </summary>
    public bool IsPolling => true;

    /// <summary>
    /// Initializes this captor.
    /// </summary>
    public void Init()
    {
        // >>> Get & save recorder module since we'll need it later on.
        rm = Hub.Instance.Get<RecorderModule>();

        // >>> Find the said script and hook into its events
        UniqueNumberScript script = GameObject.FindObjectOfType<UniqueNumberScript>();
        script.onNewNumber.AddListener(OnNewNumber);
    }

    /// <summary>
    /// Polls the captor for new data.
    /// </summary>
    /// <param name="timestamp">The timestamp.</param>
    /// <remarks>
    /// This method will only be called if <see cref="IsPolling"/> is true.
    /// </remarks>
    public void Poll(double timestamp)
    {
    }

    private void OnNewNumber(int number)
    {
        // >>> Create the new captured event
        UniqueNumberCapturedEvent evt = new UniqueNumberCapturedEvent(
            number,
            rm.RecorderInstance.CurrentFrame,
            rm.RecorderInstance.Cursor);

        // >>> Push the new captured event into the recorder
        rm.RecorderInstance.Add(evt);
    }
}

This is all that is required for implementing the logic. There is no need to register or add it somewhere in the recorder - it will discover this script automatically.

Replaying the event

Now, on to the last part: playing back the event. As we’ve done before, we’ll start by using the right-click menu to create an IInterpreter class named “UniqueNumberInterpreter”:

../../../_images/custom-04.png

And the generated code this time is a tad more “expansive”:

namespace MAGES.Recorder
{
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    public class UniqueNumberInterpreter : IInterpreter
    {
        /// <summary>
        /// Gets a value indicating whether the interpreter is enabled and should receive update events.
        /// </summary>
        public bool Enabled => true;

        /// <summary>
        /// Initializes this instance.
        /// </summary>
        public void Initialize()
        {
        }

        /// <summary>
        /// Deinitializes this instance.
        /// </summary>
        public void Deinitialize()
        {
        }

        /// <summary>
        /// Interprets the event.
        /// </summary>
        /// <param name="evt">The event.</param>
        public void Interpret(ICapturedEvent evt)
        {
        }

        /// <summary>
        /// Called at the end of the physics update.
        /// </summary>
        public void FixedUpdate()
        {
        }

        /// <summary>
        /// Called at the start of the update loop.
        /// </summary>
        public void LateUpdate()
        {
        }

        /// <summary>
        /// Called at the end of the update loop.
        /// </summary>
        public void Update()
        {
        }
    }
}

But, don’t worry, we are going to use just two of these functions; the rest are there for coverage of special cases.

namespace MAGES.Recorder
{
    using System.Collections;
    using System.Collections.Generic;
    using UnityEngine;

    // >>> This is required
    [InterpretsEvent(typeof(UniqueNumberCapturedEvent))]
    public class UniqueNumberInterpreter : IInterpreter
    {
        private RecorderModule rm;
        private UniqueNumberScript script;

        /// <summary>
        /// Gets a value indicating whether the interpreter is enabled and should receive update events.
        /// </summary>
        public bool Enabled => true;

        /// <summary>
        /// Initializes this instance.
        /// </summary>
        public void Initialize()
        {
            // >>> Find the script and save it for later
            script = GameObject.FindObjectOfType<UniqueNumberScript>();
            // >>> Same for the recorder module
            rm = Hub.Instance.Get<RecorderModule>();
        }

        /// <summary>
        /// Deinitializes this instance.
        /// </summary>
        public void Deinitialize()
        {
        }

        /// <summary>
        /// Interprets the event.
        /// </summary>
        /// <param name="evt">The event.</param>
        public void Interpret(ICapturedEvent evt)
        {
            var uniqueNumberEvent = evt as UniqueNumberCapturedEvent;

            rm.PlayerInstance.Schedule(() => {
                script.InvokeNumber(uniqueNumberEvent.UniqueNumber);
            }, evt.CaptureTime);
        }

        /// <summary>
        /// Called at the end of the physics update.
        /// </summary>
        public void FixedUpdate()
        {
        }

        /// <summary>
        /// Called at the start of the update loop.
        /// </summary>
        public void LateUpdate()
        {
        }

        /// <summary>
        /// Called at the end of the update loop.
        /// </summary>
        public void Update()
        {
        }
    }
}

Let’s walk through the changes we made from top to bottom:

  • Attach the InterpretsEvent attribute: [InterpretsEvent(typeof(UniqueNumberCapturedEvent))]:
    • This is required to tell the recorder what type of events this recorder can interpret

  • Find the UniqueNumberScript inside the scene, and save it to a member variable.

  • Implement the Interpret function:
    • There we cast the argument to the event type we interpret and use the player instance to schedule the rest of the function to happen.

Note

We use the Schedule function because events are typically interpreted before they actually happen. This is done in order to support interpolation.

And that’s it! The recorder can now properly record and playback the UniqueNumberScript:

../../../_images/custom-01.png

Review

Let’s review the changes we made. In general there are three scripts that need to be created:

  • The event data (ICapturedEvent class) (UniqueNumberEvent)

  • The logic that captures the event (ICaptor class) (UniqueNumberCaptor)

  • The logic that plays back the event (IInterpreter) (UniqueNumberInterpreter)