Input, Timers, Triggers & Projectiles

Joypad handling, edge detection macros, the timer system, trigger collision, and the projectile subsystem in GB Studio 4.2. The input and timer system is implemented in C.

Joypad Input System

The Game Boy has 8 buttons: Up, Down, Left, Right, A, B, Start, Select. Input is processed during input_update() in the main loop, which runs every frame.

Two key variables drive the entire input system:

  • frame_joy (aliased as the joy macro) — bitmask of buttons currently held down this frame.
  • joy_pressed — bitmask of buttons newly pressed this frame (edge detection). A bit is set only on the first frame a button transitions from released to pressed.

Input Constants

Defined in the GBDK headers, these constants correspond to hardware register bits:

ConstantValueButton
J_UP0x04D-pad Up
J_DOWN0x08D-pad Down
J_LEFT0x02D-pad Left
J_RIGHT0x01D-pad Right
J_A0x10A button
J_B0x20B button
J_START0x80Start
J_SELECT0x40Select

Edge Detection Macros

GB Studio provides convenience macros that check joy_pressed for single-frame press detection:

MacroDescription
INPUT_UP_PRESSEDTrue only on the first frame Up is pressed
INPUT_DOWN_PRESSEDTrue only on the first frame Down is pressed
INPUT_LEFT_PRESSEDTrue only on the first frame Left is pressed
INPUT_RIGHT_PRESSEDTrue only on the first frame Right is pressed
INPUT_A_PRESSEDTrue only on the first frame A is pressed
INPUT_B_PRESSEDTrue only on the first frame B is pressed
INPUT_START_PRESSEDTrue only on the first frame Start is pressed
INPUT_SELECT_PRESSEDTrue only on the first frame Select is pressed
How it works
These macros expand to (joy_pressed & J_*). They return a non-zero value (truthy) on the exact frame the button transitions from released to held, and zero on all subsequent frames while the button remains held.

Held vs Pressed

Understanding the difference between continuous hold detection and single-frame press detection is essential:

// Check if a button is currently held down (continuous)
if (joy & J_A) {
    /* A is held down — fires every frame while held */
}

// Check if a button was just pressed this frame (edge)
if (INPUT_A_PRESSED) {
    /* A was just pressed — fires once per press */
}

Use held checks for movement and continuous actions. Use pressed checks for menu selection, firing, toggling, and any action that should happen once per button press.

Never roll your own edge detection

Do not use (joy & J_UP) && !(last_joy & J_UP) for edge detection. joy is a macro that expands to frame_joy, not a plain variable, and last_joy may not behave as expected in all contexts. Always use the INPUT_*_PRESSED macros — they are reliable and tested.

GBVM Input Commands

From GBVM scripts, input can be handled with these opcodes:

CommandDescription
VM_INPUT_WAITPause script execution until a specific button is pressed
VM_INPUT_ATTACHAttach a script to an input event (runs when button is pressed)
VM_INPUT_DETACHDetach a previously attached input script
VM_INPUT_GETRead the current input state into a variable
Input in the main loop
input_update() runs every frame in the main loop, after script_runner_update(). The events_update() call (which dispatches input-attached scripts) is guarded by VM_ISLOCKED() — input events will not fire while a script holds a VM lock.

Timer System

GB Studio provides 4 timer slots that can be configured to fire scripts at regular intervals.

  • Timers tick every 16th frame: the check is game_time & 0x0F == 0.
  • At 60fps, this gives a timer resolution of approximately 267ms per tick.
  • timers_update() is called in the main loop, blocked by VM_LOCK.
VM_LOCK blocks timers
Since timers_update() is inside the if (!VM_ISLOCKED()) guard in the main loop, timers will not fire while any script holds a VM lock. This includes the automatic lock held during On Init scripts.

Timer GBVM Commands

CommandDescription
VM_TIMER_PREPAREConfigure a timer slot (set script to execute)
VM_TIMER_SETStart a timer with a countdown value (in 16-frame ticks)

Calculating Timer Values

Since timers tick every 16 frames at 60fps:

// Desired delay in seconds → timer ticks
ticks = (desired_seconds * 60) / 16

// Examples:
// 1 second  = 60 / 16  = ~4 ticks
// 2 seconds = 120 / 16 = ~8 ticks
// 5 seconds = 300 / 16 = ~19 ticks

Trigger System

Triggers are invisible rectangular regions in a scene that fire scripts when the player enters or leaves them. Each scene supports a maximum of 31 triggers.

Trigger Structure

typedef struct trigger_t {
    UBYTE x, y;           // Position in tiles
    UBYTE width, height;  // Size in tiles
    far_ptr_t script;     // Script to execute
} trigger_t;
  • Position and size are in tiles (8×8 pixel units), not pixels or subpixels.
  • Collision detection uses AABB (axis-aligned bounding box) intersection.
  • Each trigger can have two script types:
    • Enter script — fires when the player enters the trigger area.
    • Leave script — fires when the player exits the trigger area.

Trigger Processing

  • Triggers are checked during events_update() in the main loop.
  • Only player collision is tested — other actors do not activate triggers.
  • Trigger checks are guarded by VM_ISLOCKED(), so triggers will not fire while a VM lock is held.

Trigger GBVM Commands

CommandDescription
VM_PUSH_TRIGGER_POSPush a trigger's current position onto the stack
VM_SET_TRIGGER_POSMove a trigger to a new position at runtime
Runtime trigger repositioning
VM_SET_TRIGGER_POS allows you to dynamically move triggers during gameplay. This is useful for creating moving hazard zones, adjustable boundaries, or triggers that reposition based on game state.

Projectile System

GB Studio supports up to 5 simultaneous projectiles, managed via a linked list pool. Projectiles move in a straight line, have a limited lifetime, and can collide with actors based on collision group matching.

Projectile Properties

PropertyDescription
PositionX, Y coordinates in subpixels
DirectionMovement direction (angle-based)
SpeedMovement speed per frame in subpixels
LifetimeNumber of frames before auto-destruction
Collision groupBitmask determining which actors this projectile can hit
AnimationSprite sheet and frame used for rendering

Projectile GBVM Commands

CommandDescription
VM_PROJECTILE_LAUNCHFire a projectile from a position in a given direction
VM_PROJECTILE_LOAD_TYPEConfigure a projectile type (speed, lifetime, sprite, etc.)

Collision Groups

Both actors and projectiles have collision group bitmasks. A collision fires when:

(projectile_group & actor_group) != 0

This allows you to create teams and prevent friendly fire. For example:

  • Player projectiles use group 0x01, enemies have group 0x01 — player shots hit enemies.
  • Enemy projectiles use group 0x02, player has group 0x02 — enemy shots hit the player.
  • Neither group overlaps with its own source, preventing self-hits.

Projectile Lifecycle

Each frame during projectiles_update() and projectiles_render():

  1. Active projectiles advance their position by their speed in their direction.
  2. Lifetime decrements; projectile is destroyed when lifetime reaches zero.
  3. Collision is tested against all active actors whose collision group matches.
  4. On collision, the projectile is destroyed and the actor's collision script fires.
  5. Active projectiles are rendered to the shadow OAM.

Common Patterns

Input-Driven Movement in a Scene Type

A typical state_update() function for a custom scene type that handles both continuous movement and single-press actions:

void my_scene_update(void) BANKED {
    // Continuous movement (held buttons)
    if (joy & J_LEFT)  player_x -= MOVE_SPEED;
    if (joy & J_RIGHT) player_x += MOVE_SPEED;
    if (joy & J_UP)    player_y -= MOVE_SPEED;
    if (joy & J_DOWN)  player_y += MOVE_SPEED;

    // Single-press action (fires once per press)
    if (INPUT_A_PRESSED) {
        fire_projectile();
    }

    // Toggle on press
    if (INPUT_B_PRESSED) {
        show_menu = !show_menu;
    }
}

Timer-Based Events

Timers fire at 16-frame intervals. For a 2-second repeating timer at 60fps:

// Timer value = (seconds * 60) / 16
// 2 seconds = 120 / 16 = 7.5, round to 8 ticks
VM_TIMER_PREPARE 0, _script_bank_my_timer, _script_my_timer
VM_TIMER_SET 0, 8

Input in Custom Scene Types

When building scene type plugins that need input, follow the proven pattern used by the Flight plugin:

  • Use INPUT_*_PRESSED macros in state_update() for edge-triggered actions.
  • Use joy & J_* for continuous hold detection (movement, acceleration).
  • Let the On Init script end normally so VM_UNLOCK fires — state_update() is blocked by VM_LOCK.
  • Never use infinite loops in On Init if your scene type relies on state_update().