GPC Combos and Macros: Timing, Structure, Worked Example
How GPC combo blocks work on the Cronus Zen: the wait() timing model, run-once triggers, interrupting combos, and a worked macro built step by step.
On this page
- How GPC combos actually run
- Step 1: Sketch the sequence before you code it
- Step 2: Write the combo block
- Step 3: Choose the trigger condition
- Step 4: Interrupts and sub-combos
- The finished macro
- Common combo mistakes
- FAQ
- What is the difference between a combo and a macro in GPC?
- How do I stop a GPC combo mid-run?
- Why does my combo restart from the beginning instead of continuing?
- Can one combo run another combo?
- How long can a wait() be in a combo?
A GPC combo is a named block of timed, simulated inputs — press this, hold it 50 milliseconds, release, press that — which the Cronus Zen plays back on its own clock while your main loop keeps running. Combos are how GPC expresses everything with duration: macros, skill-move sequences, drop shots, double taps. This tutorial explains the timing model precisely, then builds a complete one-button macro step by step — a fighting-game-style quarter-circle-forward plus attack — including the trigger, an abort button, and a reusable sub-combo.
You should already be comfortable with the basics from the first script tutorial; the syntax reference has the full combo function table.
How GPC combos actually run
The one rule that explains all combo behavior: each wait(ms) holds everything the statements since the previous wait have set, for that many milliseconds. A combo is not a list of instantaneous actions — it is a list of windows, each defined by the set_val lines above a wait and lasting for that wait's duration.
Take this combo:
// Timing-model illustration (complete script).
main {
if (event_press(PS4_L1)) {
combo_run(Windows);
}
}
combo Windows {
set_val(PS4_CROSS, 100); // window 1
wait(80);
set_val(PS4_CROSS, 0); // window 2
set_val(PS4_CIRCLE, 100);
wait(120);
}
What the console receives, window by window:
| Window | Duration | Forced by the combo | Everything else |
|---|---|---|---|
| 1 | 80 ms | Cross held at 100 | Passes through from your hands |
| 2 | 120 ms | Cross forced to 0, Circle held at 100 | Passes through |
| after | — | Nothing; combo has ended | All controls back to physical |
Three more facts complete the model. A running combo continues on its own timeline even if the condition that started it is long gone. Controls the combo does not mention are untouched — you keep moving and aiming while it plays. And wait() accepts 1 to 32767 milliseconds, so a single window can span half a minute if needed.
Step 1: Sketch the sequence before you code it
Our worked example: in many fighting games, a special move is quarter-circle-forward plus an attack button — stick down, roll to down-forward, then forward, then punch. Before writing GPC, write the human action as a timed table:
| Phase | Stick / button | Roughly |
|---|---|---|
| 1 | Left stick down | 45 ms |
| 2 | Left stick down-forward (both axes) | 45 ms |
| 3 | Left stick forward | 45 ms |
| 4 | Square (attack) | 60 ms |
This sketch step is not optional busywork — every combo bug is easier to find when there is a table stating what should be happening in each window. Remember GPC's axis polarity: on the Y axis, +100 is fully down; on the X axis, +100 is fully right (we will treat right as "forward" — real fighting games need a facing check that inputs alone cannot see, which is a fundamental GPC limitation worth knowing early).
Step 2: Write the combo block
Translate the table directly — one window per phase:
// Step 2: the sequence itself, on a test trigger (complete script).
define STEP_MS = 45; // per-window stick time
define HIT_MS = 60; // attack button hold
main {
if (event_press(PS4_L1)) {
combo_run(QuarterCircle);
}
}
combo QuarterCircle {
set_val(PS4_LY, 100); // window 1: stick down
wait(STEP_MS);
set_val(PS4_LY, 100); // window 2: down-forward —
set_val(PS4_LX, 100); // both axes held together
wait(STEP_MS);
set_val(PS4_LX, 100); // window 3: forward only
wait(STEP_MS);
set_val(PS4_SQUARE, 100); // window 4: attack
wait(HIT_MS);
}
Note that window 2 restates PS4_LY — set values do not carry across wait() boundaries; every window declares its own contents. Forgetting this is the most common combo bug: the stick snaps back to your physical position for a window and the game reads a broken motion.
Step 3: Choose the trigger condition
How a combo is triggered matters as much as its contents:
event_pressfor run-once. True only on the pass where the button becomes pressed — one press, one macro. This is what we want here.get_valfor run-while-held. Sincecombo_run()does nothing while the combo is already running, a held condition makes the combo loop back-to-back — right for rapid fire, wrong for a special move.- Add a guard when re-entry would hurt.
event_press(PS4_L1) && !combo_running(QuarterCircle)refuses a second press until the first sequence finishes. Withevent_pressthis is belt-and-suspenders, but it documents intent and protects you if the trigger later changes.
Tip
While tuning, wire the combo to a button you never use in-game. Once the timings are right, move it to its real trigger. Retuning timing and trigger simultaneously doubles the variables.
Step 4: Interrupts and sub-combos
Two refinements turn a demo into a usable macro.
An abort button. A 200 ms sequence is an eternity if it starts by accident. combo_stop() halts a combo instantly and returns all controls to your hands:
// Inside main, alongside the trigger:
if (event_press(PS4_CIRCLE)) {
combo_stop(QuarterCircle); // panic button: abort mid-sequence
}
Reuse with call(). Valid only inside combos, call(Other) suspends the current combo, plays Other to completion, then resumes. If several macros end with the same attack, extract it once:
combo Attack {
set_val(PS4_SQUARE, 100); // shared finisher
wait(60);
}
Any combo can then end with call(Attack); instead of duplicating those lines — one place to retune the hit timing for every macro that uses it.
The finished macro
Everything assembled, commented, and tunable from the top:
// ============================================================
// qcf_macro.gpc — quarter-circle-forward + attack on one button.
// Press L1 to play the sequence. Press Circle to abort it.
// Assumes the character faces right; retune STEP_MS per game.
// ============================================================
define STEP_MS = 45; // stick time per window
define HIT_MS = 60; // attack button hold
main {
// Run-once trigger with a re-entry guard.
if (event_press(PS4_L1) && !combo_running(QuarterCircle)) {
combo_run(QuarterCircle);
}
// Abort at any point in the sequence.
if (event_press(PS4_CIRCLE)) {
combo_stop(QuarterCircle);
}
}
combo QuarterCircle {
set_val(PS4_LY, 100); // down
wait(STEP_MS);
set_val(PS4_LY, 100); // down-forward (restate LY)
set_val(PS4_LX, 100);
wait(STEP_MS);
set_val(PS4_LX, 100); // forward
wait(STEP_MS);
call(Attack); // shared finisher, then done
}
combo Attack {
set_val(PS4_SQUARE, 100); // attack press
wait(HIT_MS);
}
Flash it, test it against a training-mode dummy, and expect to tune. Combo timings are tuned against a specific game build; games buffer inputs differently, and a patch can change the windows without notice. There is no universal set of numbers, which is why every timing above is a define at the top of the file.
Common combo mistakes
| Symptom | Cause | Fix |
|---|---|---|
| Game reads one long press instead of two taps | No release window between presses | Insert a set_val(id, 0); wait(...) window |
| Motion input drops halfway | A held value not restated after a wait() |
Every window redeclares all values it needs |
| Macro fires repeatedly while button held | Triggered with get_val instead of event_press |
Use event_press for run-once macros |
| Sequence works in menus, fails in-game | Windows shorter than the game's input polling | Lengthen wait() values in steps of 10 ms |
| Combo will not restart quickly | combo_run ignored while running |
Use combo_restart() if a hard reset is intended |
| Stick fights your movement mid-macro | Physical and scripted axis values overlap by design | Expected: mentioned controls are overridden until the combo ends |
For deeper background on why overrides behave this way, read the inputs and events explainer — combos are just set_val on a timer, and the same pipeline rules apply.
FAQ
What is the difference between a combo and a macro in GPC?
Nothing formal — "macro" is the gaming term for an automated input sequence, and a combo block is GPC's construct for building one. Every GPC macro is a combo, though combos also serve smaller jobs like a single timed press.
How do I stop a GPC combo mid-run?
combo_stop(Name) halts it immediately and returns all controls to physical input. Wire it to an abort button with event_press, as in the worked example above.
Why does my combo restart from the beginning instead of continuing?
Something is calling combo_restart(), or the combo ended and was re-triggered. Note the opposite is the default: combo_run() on an already-running combo does nothing, so an accidental restart usually means an explicit combo_restart call.
Can one combo run another combo?
Yes, with call(Other) — the current combo pauses, the called combo plays to completion, and the caller resumes at the next statement. call is only valid inside combo blocks. Starting a combo from main always uses combo_run().
How long can a wait() be in a combo?
From 1 to 32767 milliseconds per wait() — about 32 seconds. Chain multiple waits for longer holds. Very long combos keep overriding their mentioned controls the whole time, so pair them with an abort button.
