LUIDA Docs
ComponentsUnity templateEditor windows

Configure data collector

Visual builder for the per-scene LuidaDataCollectorConfig: declare the labels participants and gimmicks can write, define the fields uploaded per recording tick, and (optionally) write the calculator body in raw JS.

These pages are not yet fully reviewed. The LUIDA team is continuing to review and improve them. If you find anything wrong on these pages, or have questions that aren't resolved by reading them, please ask or report to the LUIDA team.

LUIDA › Configure data collector is the editor for the per-scene data calculator script — the JS file that runs every time a "Save pushed data" tick fires and decides what record to push onto the upload queue. Open it after you've added a Data Collector to the scene; the window is scene-aware and refuses to load on non-experiment scenes (scenes outside Assets/_Experiment_/Scenes/).

Until 2026-05 this script was written by hand. The new window splits the calculator into two structured sections (labels and fields) plus an escape hatch (Code Mode) for advanced layouts.

Screenshot pending — the Configure data collector window split into the Builder/Code mode toggle, the "Collected data items" list with three entries (timer:Integer, score:Integer, chosen:String), and the "Fields to be saved" list with four field rows (Collected, Arithmetic, Conditional, Direct).

Two modes

A toggle at the top selects how the calculator body is authored.

ModeWhen to useWhat it edits
Builder (default)Most experiments. Records are flat objects keyed by field name; values come from collected labels, global state reads, condition lookups, arithmetic, or simple ternaries.Sections A + B (visual).
Code ModeYou need control flow that the builder can't express (loops, function calls into the rest of your scene's helper code, complex object shapes).Section A still drives the CCK sync header; Section B is replaced by a raw JS textarea.

Switching modes is destructive: Builder → Code Mode seeds the JS area from the current Builder output, but a subsequent Code Mode → Builder discards the raw JS. The dialog warns you before either swap.

Section A — Collected data items

A list of (label, type) pairs. Every entry shows up:

  • In the Configure data collector's Section B as a possible source (the Collected source kind).
  • In the LuidaDataCollectionGimmick inspector as a label-picker dropdown (CCK can carry Bool / Float / Integer / Vector2 / Vector3 values, so those types are CCK-routable; String values can only be written by the SendDataToCollector action because CCK has no String state).
  • In the auto-generated CCK sync header at the top of the calculator script — when a gimmick patches luida_collect_<label>, the header copies the value into $.groupState.collectedData.

Add a row with the + button at the bottom of the list. Each row has a name (must be a valid JS identifier — letters, digits, underscores, no leading digit) and a type. The window warns about duplicate names or invalid identifiers inline.

Section B — Fields to be saved (Builder mode)

A reorderable list of field rows. Each row becomes one key/value entry in the JSON record uploaded per recording tick.

Per row you choose a Source:

SourceProducesTypical use
CollectedCOLLECTED_DATA["label"]Surface the value a gimmick or action pushed.
Global State Read$.getStateCompat(item, key, type)Inspect a CCK global-state key from elsewhere in the scene.
Condition LookupCONDITION["variable"]Record which between-subject condition was active.
ArithmeticMulti-operand expressionDerived metrics (a + b, score * weight, accuracy %, etc.).
Conditionallhs op rhs ? thenValue : elseValueBranch on a value (e.g. time < 1.5 ? "fast" : "slow").
Direct valueLiteral number, string, or raw JS expressionConstants and one-liners.

Each row also shows a colored type badge: green = Bool, blue = Int/Num, purple = Vec2/Vec3, orange = Str — a quick sanity check that the source's output type matches what you intend to record.

A field whose source is left blank is silently skipped at generation time, so partially-configured rows don't break the calculator.

Section B (Code mode) — Raw JS body

A single textarea holding the body of calculateData(). Builder's Section A still runs as a sync header above your code, so $.groupState.collectedData (and the implicit COLLECTED_DATA alias) are populated before your code runs.

In raw mode you're responsible for returning the record object:

const timer    = COLLECTED_DATA["timer"];
const isCorrect = COLLECTED_DATA["chosen"] === CONDITION["answer"];
return {
  cond: CONDITION || {},
  time: timer,
  correct: isCorrect,
  meta: {
    sessionAgeSec: $.getStateCompat("global", "session_age", "float"),
  },
};

Generated output

Saving the window writes Assets/_Experiment_/Scripts/DataCollectors/<SceneName>.js. The file is consumed by the LuidaDataCollector component's Script Asset field and runs every time a "Save pushed data" tick fires (see Pipeline integration below). The file has two parts:

  1. CCK sync header (auto-generated from Section A). Maps luida_collect_<label> global-state keys into $.groupState.collectedData so the rest of the script doesn't have to know whether the value was pushed by an action or by a CCK gimmick.
  2. Builder field block or your raw JS — produces the record object that gets appended to the upload queue.

Don't hand-edit the .js: the next time the window saves, it overwrites the file from the config asset. If you need a tweak the Builder can't express, switch to Code Mode and put the tweak in the raw JS area.

Per-scene config asset

The config lives at:

Assets/_Experiment_/Settings/DataCollectorConfig/<SceneName>.asset

…as a LuidaDataCollectorConfig ScriptableObject. Created lazily the first time you open the window on an experiment scene that doesn't have one yet. The window auto-saves on close, on scene change, and via the explicit Save button at the bottom.

A migrator (LuidaDataCollectorConfigMigrator) runs on load to upgrade older config schemas. You don't normally need to touch it.

Pipeline integration

The Data Collector window only configures what gets saved per recording tick. When recording ticks happen is decided elsewhere:

  • The merged LuidaDataCollectionGimmick CCK component (one per trigger source). Three phase toggles:
    • Push data — write (label, value) into the staging dict via luida_collect_<label>.
    • Save pushed data — fire exp_recordCustomData, triggering this window's calculator.
    • Upload saved data — fire exp_uploadCustomData, flushing the queue to the Web Console.
  • The state-listening actions Push data to collector, Save pushed data in collector, and Upload saved data from collector — same three signals, fired imperatively from action lists.

Either path leads to calculateData() running with CONDITION, PARTICIPANTS, and COLLECTED_DATA in scope. The Builder UI is just a visual editor for what that function returns.

The merged gimmick's "Save pushed data" and "Upload saved data" phases use PatchStatementAtSignal (a Signal), not PatchStatementAt(..., true) (a Bool). A Bool's encoded timestamp never changes, so a Bool-driven trigger would only fire once per session. If you write your own data-collection trigger by reflection or by direct CCK statement manipulation, follow the Signal pattern — otherwise you'll get exactly one upload per Cluster instance and then silence.

Common pitfalls

  • Editing the wrong file. Hand edits to <SceneName>.js get overwritten by the next window save. Use Code Mode if you need control flow.
  • String labels via CCK. The LuidaDataCollectionGimmick can't push String-typed values because CCK has no String state. Use the SendDataToCollector action for those.
  • Invalid label names. Section A warns about identifiers that start with a digit or include punctuation. Until you fix them, those entries are dropped from the generated sync header.
  • Forgetting the Data Collector item. This window edits the calculator script, but if no LuidaDataCollector component is present in the scene, nothing runs the script. The window shows a HelpBox + Create LUIDA-DataCollector in this scene button when that's the case.

Where to go next