Skip to content

Using with Unity

Ahmed Shariff edited this page Sep 3, 2023 · 3 revisions

For now I am documenting how I am setting up configs to use in Unity project. This example also uses UXF, but the configs can be used without it as well. The steps are as follows:

  1. In the root of the Unity project, create the following directory structure:
<unity-project-root>/External/Assets/

This way you don't have to deal with the pesky meta files for anything in the above directories.

  1. Copy the sample_config.toml into a new config in the Assets directory created and add the configurations I intend to use. (Here I am renaming it to study_config.toml)
<unity-project-root>/External/Assets/study_config.toml
  1. I personally use poetry to manage my python projects, hence inside the External directory, run poetry init to set that up.

  2. Add experiment-server as a project dependency (during the init process, editing the pyproject.toml file or using poetry add.

  3. (optional) create a run.bat/run.sh file in the External directory with the following, so that I have to type a lot less whenever I want to spin up the experiment-server

poetry run experiment-server run /Assets/study1_config.toml -i 1

I use the -i 1 when I am developing, and usually get rid of it when I start piloting.

I am using poetry throughout, but one can use their choice python environment.

  1. Now that the config is set up, I'd add a Component in Unity to get the above information. There are several ways I could interface with the experiment-server:
  • Query for the next block at the end of each block
  • Get the complete config at the beginning of the session.
  • Use experiment-server generate-config-json (See docs for more info. Then load the generated configs into Unity. For example, configs can be written to <unity-project-root>/Assets/StreamingAssets/, which is a location used by UXF to load settings. Note that UXF's "settings" and "configs"/"configurations" in experiment-server are the same thing. The following is an example of how to do that:
poetry run experiment-server generate-config-json /Assets/study1_config.toml --participant-range 5`

Again, poetry is used here, remove/replace poetry run accordingly if you are using a different approach to managing python envs.

The above would generate configs for 5 participants (consider the scenario where the configs have to be counterbalanced.)

Below is an example script with UXF using option 1 above (which is also the one that needs the most work, hence). A more elaborate version of this is available as a package at ovi-lab/UXF-extensions.

using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Networking;
using UXF;
using Newtonsoft.Json;

namespace SampleProject.study1
{
    public class ExperimentManager : MonoBehaviour
    {
        [SerializeField]
        [Tooltip("The url address to the experiment server.")]
        string experimentServerUrl = "http://127.0.0.1:5000";

        #region UNITY_FUNCTIONS
        public void Start() {
            Session session = Session.instance;
            session.onSessionBegin.AddListener(OnSessionBegin);
            session.onBlockBegin.AddListener(OnBlockBegin);
            session.onBlockEnd.AddListener(OnBlockEnd);
            session.onTrialBegin.AddListener(OnTrialBegin);
            session.onTrialEnd.AddListener(OnTrialEnd);
            session.onSessionEnd.AddListener(OnSessionEnd);

            // other UXF setup

            // Strating the session after a few seconds
            // Doing this to allow the scene to fully load
            StartCoroutine(StartSessionAfterWait(session));
        }

        private IEnumerator StartSessionAfterWait(Session session)
        {
            yield return new WaitForSeconds(2.0f);
            StartCoroutine(GetJsonUrl("api/global-data", (jsonText) =>
            {
                Debug.Log($"Starting session");
                // Get participant index and start session
                (int participant_index, int config_length) data = JsonConvert.DeserializeObject<(int participant_index, int config_length)>(jsonText);
                Session.instance.Begin("SampleProject.study1", $"{data.participant_index}");
            }));
        }
        #endregion

        #region UFX_FUNCTIONS
        private void OnSessionBegin(Session session)
        {
            GetNextBlock();
        }

        private void GetNextBlock()
        {
            StartCoroutine(GetJsonUrl("api/move-to-next", (jsonText) =>
            {
                BlockData el = JsonConvert.DeserializeObject<BlockData>(jsonText);
                if (el.name != "end")
                {
                    StartCoroutine(GetJsonUrl("api/config", (jsonText) =>
                    {
                        el = JsonConvert.DeserializeObject<BlockData>(jsonText);
                        ConfigureBlock(el);
                        Session.instance.BeginNextTrial();
                    }));
                }
                else
                {
                    Session.instance.End();
                }
            }, post: true));
        }

        private Block ConfigureBlock(BlockData el)
        {
            // Generate the trial here based on data in el
        }

        private void OnBlockBegin(Block block)
        {
            // block begin stuff
        }

        private void OnTrialBegin(Trial trial)
        {
            // trial begin stuff
        }
        
        private void OnTrialEnd(Trial trial)
        {
            // on trial end code
        }

        private void OnBlockEnd(Block block)
        {
            // on block end code
            GetNextBlock();
        }

        private void OnSessionEnd(Session session)
        {
            // on session end code
        }
        #endregion

        #region HELPER_FUNCTIONS
        // Copied from  UFX.UI.UIController
        // Used to get the trial config from the server
        IEnumerator GetJsonUrl(string endpoint, System.Action<string> action, bool post=false)
        {
            string url = $"{experimentServerUrl}/{endpoint}";
            using (UnityWebRequest webRequest = post ? UnityWebRequest.Post(url, "") : UnityWebRequest.Get(url))
            {
                webRequest.timeout = 5;
                yield return webRequest.SendWebRequest();

                bool error;
#if UNITY_2020_OR_NEWER
                error = webRequest.result != UnityWebRequest.Result.Success;
#else
#pragma warning disable
                error = webRequest.isHttpError || webRequest.isNetworkError;
#pragma warning restore
#endif

                if (error)
                {
                    Debug.LogError($"Request for {experimentServerUrl} failed with: {webRequest.error}");
                    yield break;
                }
                action(System.Text.Encoding.UTF8.GetString(webRequest.downloadHandler.data));
            }
        }
        #endregion
    }

    class BlockData
    {
        public int trialsPerItem, param1, param2, param3; // These are based on the config. See `sample_config.toml`
    }
}
Clone this wiki locally