Architecture¶
The template separates three concerns: a persistent Base Scene that runs for the whole session, experiment scenes loaded on top of it, and a flow that sequences the experiment. Understanding how these fit together is the key to building your own study.
The Base Scene¶
The Base Scene holds the components that must survive across experiment scenes. Experiment scenes are loaded additively (added to the running Base Scene rather than replacing it), so these components keep tracking and recording without interruption.
flowchart TB
subgraph Base["Base Scene (persistent)"]
PI[ProjectInitializer]
SM[ResXRSceneManager]
P[ResXRPlayer]
DM[ResXRDataManager]
end
subgraph Exp["Experiment Scene (additive)"]
SR[*_SceneReferencer]
Flow[*_SessionManager / TaskManager / TrialManager]
end
PI --> SM
SM -->|loads| Exp
P -.tracking.-> DM
Flow -.events + custom tables.-> DM
| Component | Role |
|---|---|
ProjectInitializer |
Entry point. On Start it optionally runs room calibration, then hands control to the scene manager. Fields _shouldProjectUseCalibration and _shouldCalibrateOnEditor decide whether calibration runs at all and whether it also runs in the editor. |
ResXRSceneManager |
Loads and switches experiment scenes additively, with fade-to-black transitions. BaseSceneIndex (default 0) and FirstSceneToLoadIndex (default 1) select what loads at startup; SwitchActiveScene(name) changes scenes at runtime. It also repositions the player when a scene provides a PlayerRepositioner. |
ResXRPlayer |
The tracking rig facade. Exposes the head and hand transforms, the ResXREyeTracker, face expressions, and helpers like FadeViewToColor(...), RepositionPlayer(...), and SetPassthrough(...). |
ResXRDataManager |
The recorder. Builds the CSV schemas, runs the collectors every tick, and writes all output files. |
Singletons
The managers derive from ResXRSingleton<T>, a base class that exposes a single global instance (ResXRPlayer.Instance, ResXRDataManager.Instance, …) and an overridable DoInAwake() hook. This is why experiment code can reach the player or recorder from anywhere without wiring up references.
The flow: Session → Task → Trial¶
An experiment is structured as a three-level hierarchy. Each level has a manager that runs the level below it:
flowchart LR
S[SessionManager<br/>RunSessionFlow] -->|each Task| T[TaskManager<br/>RunTaskFlow]
T -->|each Trial| Tr[TrialManager<br/>RunTrialFlow]
SessionManagerholds aTask[]and runs each task in turn (RunSessionFlow).StartSession()/EndSession()bracket the session;EndSession()callsApplication.Quit()so the recorder finalizes its files.TaskManagerholds aTrial[]and runs each trial (RunTaskFlow), withStartTask()/EndTask()and aBetweenTrialsFlow()hook.TrialManagerruns a single trial (RunTrialFlow), withStartTrial()/EndTrial().
The control flow is asynchronous: each Run…Flow method is a UniTask (UniTask is a Unity-friendly form of async/await), so a step can simply await a participant action — a touch, a button press, a timer — pausing until it happens without freezing the rest of the app.
Task was formerly Round
The middle level is now Task; the SessionManager field carries a [FormerlySerializedAs("_rounds")] attribute so older scenes still deserialize. If you find references to a "round manager" in older material, that is this level.
Scaffold vs. worked examples¶
The generic Flow Management classes under Assets/ResXR/Flow Management/ are an intentionally minimal scaffold: Task and Trial are empty containers, and the Start…/End… methods are empty hooks. They define the shape of an experiment without prescribing its content. Extend Task/Trial with the fields your study needs:
[System.Serializable]
public class MyTrial : Trial
{
public GameObject stimulus;
public float durationSeconds;
}
The bare scaffold does not run unmodified
SessionManager.BetweenTasksFlow() throws NotImplementedException, so running the unedited SessionManager with more than one Task stops with an exception between tasks. Fill in (or remove) that hook before relying on it.
The three paradigms are the worked implementations of this pattern. Each ships its own *_SessionManager, *_TaskManager, *_TrialManager, and *_SceneReferencer with real logic, instruction panels, interactions, and data logging. These are independent classes that follow the same pattern — they do not inherit from the generic SessionManager/TaskManager/Task/Trial. When building a new experiment, copy the paradigm closest to yours (its whole set of flow files) and edit it, rather than trying to subclass the bare scaffold.
Where recording fits¶
Recording is decoupled from the flow. ResXRDataManager samples tracking continuously regardless of which task or trial is active, so the continuous and face files are one unbroken timeline for the whole session. The flow's job is to annotate that timeline: your Start…/End… hooks call ReportEvent(...) to drop event markers and LogCustom(...) to append per-trial rows, all on the same timeSinceStartup clock.
sequenceDiagram
participant Flow as Flow (Task/Trial)
participant DM as ResXRDataManager
participant Disk
loop every FixedUpdate
DM->>Disk: write ContinuousData row
end
Flow->>DM: ReportEvent("trial_start", t, 0)
Flow->>DM: LogCustom(new TrialsData(...))
Note over DM,Disk: rows appended on the same clock
The recorder is set up in DoInAwake() (schemas built, files opened) and finalized in OnDestroy() (last rows flushed, custom-tables sidecar written). That finalization is why a clean Application.Quit() matters; see Data Output. For the public methods involved, see Scripting & API.