Log reaction times
Measure the gap between a stimulus appearing and a participant pressing a button, per trial, downloadable as CSV.
Goal
For every trial, record how long it took the participant to respond — the time between the stimulus state entering and the participant's button press triggering the state exit.
Smallest setup
- A LUIDA scene with at least one trial state (call it
Trial - Start) followed by a transition trigger (a button via the button cookbook, an interaction trigger, etc.). - A
LUIDA-DataCollectorprefab in the scene (this is part of the standard LUIDA prefab set; if you're working from the template's blank scene, drag it in). - A registered experiment in the Web Console so you can download the resulting CSV.
The recipe
Add a timer state-listening item
In the Hierarchy, create an empty GameObject under LUIDA-ExpManagers named TimeRecorder. Add the standard LUIDA state-listener component to it.
This GameObject doesn't need a mesh; it exists only to hold lifecycle actions.
Start the clock on state entry
Open LUIDA › Configure experiment automation → State-listening Items. Find the row for TimeRecorder and the column for the trial state where you want timing to begin.
Under On State Start, add a Customized Action with this body:
$.state.startedAt = Date.now();That stamps a timestamp into the item's own state at the exact moment the trial begins.
Stop the clock and write the result on state exit
In the same cell, under On State Exit, add another Customized Action:
const rtMs = Date.now() - $.state.startedAt;
SendDataToCollector("rt_ms", rtMs);This computes the elapsed milliseconds and stashes the result on the data scratchpad under the label rt_ms.
Make the value land in a CSV row
The LUIDA-DataCollector GameObject in your scene has a Data Calculator Script Asset attached. Open it and ensure the returned record includes rt_ms:
return {
stateLog: COLLECTED_DATA["stateLog"],
cond: CONDITION || {},
rt_ms: COLLECTED_DATA["rt_ms"],
};Then add the standard two-step persistence at the end of the trial state (typically on Trial - Rest's On State Start, or on a state machine slot dedicated to "trial finalize"):
ProcessAndSaveCollectedData();
UploadCollectedData();Press Play in CSEmulator, walk through several trials, finish the experiment, and download the Custom Data CSV from the Web Console's Data tab. Each trial produces one row with an rt_ms column.
Why this works
LUIDA's data pipeline has three stages for custom data, and this recipe touches all three:
- Scratchpad —
SendDataToCollector("rt_ms", value)writes to an in-memory key/value store scoped to this Cluster instance. - Snapshot —
ProcessAndSaveCollectedData()calls your Data Calculator script to assemble a record from the scratchpad and append it to the queue of pending records. - Upload —
UploadCollectedData()flushes the queue to the Web Console where it lands in your custom-data export.
The reason we time with Date.now() and not a frame counter: state machine transitions are network-mediated, so frame counts on different clients drift slightly. Wall-clock milliseconds on a single participant's client are stable enough for psychology-grade reaction times (10ms-ish jitter from Cluster's networking + frame scheduling, which is fine for most behavioral RT analysis but not for sub-frame ERP work).
See Concepts → Data collection for the full four-stream model, and Tutorial 2 → TimeRecorder for the same pattern inside a Stroop experiment.
Variants
Time multiple events in one trial. Use different keys: $.state.t_stim, $.state.t_response, then send t_stim_to_response, t_response_to_feedback, etc. The scratchpad is a per-key key/value store, so as many labels as you want.
Record what the participant pressed, not just when. Combine with a state-listening item on the buttons:
// On the Red button, On State Exit (or on its interact trigger via signal)
SendDataToCollector("answer", "red");Then both answer and rt_ms end up in the same trial record.
Per-condition reaction times in analysis. You don't need to do anything LUIDA-side — CONDITION is already in the record, so a single grouping column in your downstream analysis (R / Python / pandas) gives you mean RT per condition.
Sub-frame precision. Not currently supported by LUIDA's data pipeline — the wall-clock approach has the precision of Date.now() plus Cluster's network jitter. For sub-frame timing, you'd need an external EEG box with its own marker (and SendViaOsc to mark stimulus onset on the external clock).
Where to go next
- Tutorial 2 → Stroop Effect — the worked example this recipe distills.
- Reference → SendDataToCollector — the action card with all side effects.
- Reference → ProcessAndSaveCollectedData — the snapshot step.
- Reference → UploadCollectedData — the upload step.
- Concepts → Data collection — the four data streams and how they merge in the export.
- Web Console → Downloading data — where the resulting CSV lives.