-
Notifications
You must be signed in to change notification settings - Fork 0
Using with Unity
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:
- 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.
- Copy the
sample_config.toml
into a new config in theAssets
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
-
I personally use poetry to manage my python projects, hence inside the
External
directory, runpoetry init
to set that up. -
Add
experiment-server
as a project dependency (during theinit
process, editing thepyproject.toml
file or usingpoetry add
. -
(optional) create a
run.bat
/run.sh
file in theExternal
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.
- 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`
}
}