Unity Experiment Framework (UXF)
Overview
Unity Experiment Framework (UXF) is set of components which simplify human behaviour experiments developed in the Unity engine. It is built upon a ‘Session - Block - Trial’ concept:

(From Brookes et al., 2020)
Trials and blocks can easily be generated by UXF by supplying it with the task features and values you want to test.
The details of your study can remain independent from the execution of the task, which is convenient for when you’re preparing the Methods section of your paper. UXF also promotes flexibility from Session to Session, allowing you to present a (slightly) different task at different times.
UXF supports experiments for VR, Desktop, as well as Web based experiments for full remote data collection, with different data output modes.
Generating a Session
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
{
[SerializeField] private int m_blocks = 4;
[SerializeField] private int m_trialsPerBlock = 1;
[SerializeField] private bool m_shuffleBlocks = true;
private void OnValidate()
{
if (m_blocks < 1)
{
this.Warning("You cannot have fewer than 1 Blocks");
m_blocks = 1;
}
if (m_trialsPerBlock < 1)
{
this.Warning("You cannot have fewer than 1 Trial per Block");
m_trialsPerBlock = 1;
}
}
/// <summary>
/// This gets called from the OnSessionBegin event on the [UXF_Rig] GameObject.
/// Set it in the Inspector of the Session component.
/// </summary>
/// <param name="session"></param>
public void GenerateSession(Session session)
{
this.Info($"Creating {m_blocks} blocks with {m_trialsPerBlock} trials each");//
var startingBlock = 1;
var endingBlock = m_blocks;
session.settings.SetValue("sesh", 10); // You can set Settings on the Session-level: automatically logged to `settings.json`.//
for (var i = startingBlock; i <= endingBlock; i++)
{
var block = session.CreateBlock(m_trialsPerBlock);
var isFirstHalf = i <= (endingBlock + startingBlock) / 2;
var isEvenBlock = i % 2 == 0;
// this is how we can set values to the Blocks, bool in this case.
block.settings.SetValueStored("poke", isFirstHalf); // this auto-logs itself in the `trial_results.json`, because it registers itself to the "Settings To Log" list.
if (isEvenBlock)
{
block.settings.SetValueStored("async", 300); // setting the async to 300ms
}
else
{
block.settings.SetValueStored("async", 0); // turning async off: 0ms
}
}
if (m_shuffleBlocks)
{
session.blocks.Shuffle();
this.Info("Shuffled blocks to new order");
}
foreach (var block in session.blocks)
{
foreach (var setting in block.GetSettings())
{
this.Verbose($"Our Block {block.number} has {setting.Key}:{setting.Value}");
}
}
}
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
public class ExperimentRunner : MonoBehaviour
{
[SerializeField] [DisableEditing] public bool m_isPokeBlock;
[SerializeField] [DisableEditing] public bool m_isAsyncBlock;
[Tooltip("If set to less than 0, stopping a trial will not automatically start a new one.")]
[SerializeField] [Range(-1, 10f)] private float m_startNextTrialDelay = 2.5f;
private Session _session;
private void Awake()
{
_session = GetComponent<Session>();
}
[Button]
public void StartTrial()
{
if (!_session.IsInitialised())
{
return;
}
if (_session.IsLastTrial())
{
this.Error("Something is wrong. This should not be possible");
return;
}
if (_session.TrialInProgress())
{
this.Warning("We are already in an active Trial. Stop that Trial before starting this new one");
return;
}
_session.BeginNextTrial(); // By itself, this always needs to be at the start, since otherwise the "CurrentTrial" / "currentTrialNum" are either not initialized, or at 0 (and UXF is NON-ZERO-INDEXED!). There's now some other methods to check for this, but it's good to know that quite a few UXF operations won't be possible until at least one Trial has started.
if (_session.CurrentBlock.GetRelativeTrial(1) == _session.CurrentTrial)
{
StartedBlock();
}
this.Info($"Starting Trial {_session.currentTrialNum}/{_session.LastTrial.number} (total) / {_session.CurrentBlock.GetCurrentTrialInBlock().numberInBlock}/{_session.CurrentBlock.trials.Count} (relative) of Block {_session.currentBlockNum}/{_session.blocks.Count}");
}
private void StartedBlock()
{
this.Info($"Starting Block {_session.currentBlockNum}");
m_isPokeBlock = _session.CurrentBlock.settings.GetBool("poke");
m_isAsyncBlock = _session.CurrentBlock.settings.GetBool("async");
foreach (var blockSetting in _session.CurrentBlock.GetSettings())
{
this.Verbose($"Our Block has {blockSetting.Key}:{blockSetting.Value}");
}
}
[Button]
public void StopTrial()
{
if (!_session.IsInitialised())
{
return;
}
if (!_session.TrialInProgress())
{
this.Warning("There is currently no Trial in progress, so we cannot stop a Trial either. Start a new Trial first.");
return;
}
_session.EndCurrentTrial();
this.Info($"Stopping Trial {_session.currentTrialNum}/{_session.LastTrial.number} (total) / {_session.CurrentBlock.GetCurrentTrialInBlock().numberInBlock}/{_session.CurrentBlock.trials.Count} (relative) of Block {_session.currentBlockNum}/{_session.blocks.Count}");
if (_session.CurrentTrial.IsLastTrialInBlock())
{
BlockEnded();
}
if (_session.CurrentTrial == _session.LastTrial)
{
StopSession();
return;
}
if (m_startNextTrialDelay >= 0)
{
this.Info($"Auto-starting next Trial in {m_startNextTrialDelay} seconds");
Invoke(nameof(StartTrial), m_startNextTrialDelay);
}
}
private void BlockEnded()
{
this.Info("That was the last Trial in this Block!");
}
private void StopSession()
{
this.Info("That was the last Trial in our last Block, will be stopping this Session");
_session.End();
// This in turn SHOULD invoke the Session to call the OnSessionEnd event, where I can hook a method that gracefully handles fade to black, scene cleanup, etc.
}
[Button]
public void StopSessionEarly()
{
if (_session.IsInitialised())
{
var currentTrial = _session.CurrentTrial.number;
var remaining = _session.LastTrial.number - currentTrial;
this.Warning($"We're not done yet! We're bailing out of {remaining} Trials!");
for (var i = currentTrial; i < _session.LastTrial.number; i++)
{
_session.endAfterLastTrial = true;
_session.EndCurrentTrial();
_session.BeginNextTrialSafe();
_session.EndCurrentTrial();
}
StopSession();
}
}
}
Data Collection
UXF automates the process of collecting data. Data is stored in CSV files with automatic handling of file & directory naming. Data can be stored locally or online.
Data is saved at 3 levels:
- Per participant (e.g. age, gender)
- Per-trial (responses, e.g. a selection, movement time)
- On every frame (runs on Update loop) within a trial (e.g. position in space)
Links
Research paper in Behavioural Research Methods (2020)
Overview Video
Quick Start Video Tutorial
Git Repo
Text Tutorial
UXF Wiki
{{/code}}