LUIDA Docs

State machine & trial lifecycle

How a LUIDA experiment progresses from start to end — the states, the transitions, the trial loop.

Every LUIDA experiment runs through a state machine. States are the chunks an experiment naturally splits into — Instructions, Trial - Start, Stimulus, Response, Trial - Rest, End — and transitions are the rules for moving from one to the next. The LUIDA › Configure experiment automation › State Machine editor is where you draw this graph; the StateListeningItemBase.js runtime is what executes it.

The key idea on this page: your scene's behavior is keyed off state_currentID. Items watch this global integer, and react when it changes.

The shape of a LUIDA state machine

A state machine in LUIDA is an ordered list of states with optional self-loops. It is not an arbitrary directed graph — that's a deliberate constraint, because most experiments are sequential.

The list always has three fixed states at known positions:

StatePositionRole
Trial - Startfirst state of the trial loopConditions for the next trial are computed when this state begins.
Trial - Restbetween-trial breatherRuns once between two consecutive trials. Common place to show a fixation cross or "press to continue" prompt.
Endlast stateTerminal. Once reached, the experiment is over for this participant.

You cannot rename or reorder these three. You can add states between them, repeat a sub-list, set exit timers — that's where customization happens.

A typical Stroop-task state list looks like:

1. Instructions          (manual transition: participant reads, presses Start)
2. CalculationTask       (auto-transition after 30s, repeats 5 times before
                          falling through)
3. Trial - Start         ← fixed; conditions computed here
4. Stimulus              (auto-transition after 1.5s)
5. Response              (manual transition: participant clicks; logs answer)
6. Trial - Rest          ← fixed; runs once per trial
7. PostExperimentSurvey  (qID-bound questionnaire)
8. End                   ← fixed; terminal

The trial loop is implicit: states 3–6 repeat once per trial, with the trial counter advancing automatically. LUIDA computes how many trials to run from your variables; the loop terminates when the counter hits the total.

What makes states transition

A state changes — state_currentID increments — when one of three things happens:

  1. Exit timer expires — if you set "Has Exit Time" on the state and gave it a duration, it auto-transitions after that many seconds.
  2. Explicit signal — something in the world fires the state_triggerTransition signal. The most common source is a LuidaToNextStateGimmick triggered by a button press, a collision, or a state-listening action timer.
  3. Repeat exhaustion — if a state has "Is Repeated" set and a repeat count, the state loops back to a previous state on transition until the count is reached, then falls through.

There's no fourth way. A state will not advance on its own without one of these three triggers, and you'll see this most often when debugging a stuck experiment: nothing happened, because no exit timer was set and no signal fired.

What runs at each state boundary

Each state has three lifecycle hooks. State-listening items can attach actions to any of them:

  • On State Start — runs once, the moment the state becomes active. Use this for: showing a stimulus, starting a timer, logging "trial began."
  • During State — runs every frame while the state is active. Use this for: per-frame position updates, watching for input that isn't a CCK trigger, syncing items to participant bones.
  • On State Exit — runs once, just before the state changes. Use this for: hiding a stimulus, logging response time, clearing a UI.

LUIDA's state-listening item editor is a grid: rows are states, columns are items. Each cell is "what happens to this item when this state is on / starts / ends." The grid format is dense, but it makes the experimentwide structure easier to skim.

The trial loop in detail

Trials are the unit of repetition. One trial = one pass through the trial-loop subset of the state machine.


stateDiagram-v2
[*] --> Setup
Setup --> TrialStart: pNum participants joined
TrialStart --> Stimulus: ConditionManager.assignTrial()
Stimulus --> Response: exit timer
Response --> TrialRest: state_triggerTransition (button press)
TrialRest --> TrialStart: trialID < trialCount
TrialRest --> End: trialID >= trialCount
End --> [*]

When a state transition would move from Trial - Rest back to Trial - Start:

  1. ConditionManager.js increments exp_trialID.
  2. ConditionManager recomputes the CONDITION map for the new trial (which within-subject combination is active this round).
  3. State-listening items running on Trial - Start's "On State Start" see the new CONDITION values.

This is why you read CONDITION["color"] inside actions and get the right value — it's been swapped in by the manager between rest and the next trial.

When exp_trialID reaches trialCount - 1 and another trial would start:

  • ConditionManager emits exp_readyToLeaveTrials. By default, the state machine then advances past the trial loop on the next transition out of Trial - Rest.

Things that look like states but aren't

A few patterns researchers reach for that aren't states:

  • A 1-second pause inside a state — don't make it a separate state. Use the Sleep action inside a state-listening action chain.
  • A "show this for 3 seconds" stimulus — you can make it a state with Has Exit Time = 3s. You can also use a Sleep action. The state approach is cleaner if many items respond to that 3s window.
  • A choose-your-own-adventure branch — LUIDA's state machine is linear by design. If you need branching, use Customized Action to set a flag and have downstream states gate their behavior on the flag (via conditional actions).

How state interacts with conditions

When a state is active, every state-listening action has access to:

  • CONDITION["<within-subjects-variable-name>"] — the active value of each within-subjects variable for the current trial.
  • CONDITION["<between-subjects-variable-name>"] — the value assigned to this participant for the whole session.
  • PARTICIPANTS[1], PARTICIPANTS[2], ... — the player handles, 1-indexed.

So a Stroop trial's "Show stimulus" action might look like:

SetText(CONDITION["text"]);  // either "RED" or "BLUE"
SetChildPosition("ColorBox", 0, CONDITION["depth"] === "near" ? 1.5 : 3.0, 0);
ShowItem();

This is a common pattern in LUIDA: state controls when, condition controls what.

Where to go next