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 thejoymacro) — 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:
| Constant | Value | Button |
|---|---|---|
J_UP | 0x04 | D-pad Up |
J_DOWN | 0x08 | D-pad Down |
J_LEFT | 0x02 | D-pad Left |
J_RIGHT | 0x01 | D-pad Right |
J_A | 0x10 | A button |
J_B | 0x20 | B button |
J_START | 0x80 | Start |
J_SELECT | 0x40 | Select |
Edge Detection Macros
GB Studio provides convenience macros that check joy_pressed for single-frame press detection:
| Macro | Description |
|---|---|
INPUT_UP_PRESSED | True only on the first frame Up is pressed |
INPUT_DOWN_PRESSED | True only on the first frame Down is pressed |
INPUT_LEFT_PRESSED | True only on the first frame Left is pressed |
INPUT_RIGHT_PRESSED | True only on the first frame Right is pressed |
INPUT_A_PRESSED | True only on the first frame A is pressed |
INPUT_B_PRESSED | True only on the first frame B is pressed |
INPUT_START_PRESSED | True only on the first frame Start is pressed |
INPUT_SELECT_PRESSED | True only on the first frame Select is pressed |
(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.
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:
| Command | Description |
|---|---|
VM_INPUT_WAIT | Pause script execution until a specific button is pressed |
VM_INPUT_ATTACH | Attach a script to an input event (runs when button is pressed) |
VM_INPUT_DETACH | Detach a previously attached input script |
VM_INPUT_GET | Read the current input state into a variable |
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 byVM_LOCK.
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
| Command | Description |
|---|---|
VM_TIMER_PREPARE | Configure a timer slot (set script to execute) |
VM_TIMER_SET | Start 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
| Command | Description |
|---|---|
VM_PUSH_TRIGGER_POS | Push a trigger's current position onto the stack |
VM_SET_TRIGGER_POS | Move a trigger to a new position at runtime |
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
| Property | Description |
|---|---|
| Position | X, Y coordinates in subpixels |
| Direction | Movement direction (angle-based) |
| Speed | Movement speed per frame in subpixels |
| Lifetime | Number of frames before auto-destruction |
| Collision group | Bitmask determining which actors this projectile can hit |
| Animation | Sprite sheet and frame used for rendering |
Projectile GBVM Commands
| Command | Description |
|---|---|
VM_PROJECTILE_LAUNCH | Fire a projectile from a position in a given direction |
VM_PROJECTILE_LOAD_TYPE | Configure 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 group0x01— player shots hit enemies. - Enemy projectiles use group
0x02, player has group0x02— 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():
- Active projectiles advance their position by their speed in their direction.
- Lifetime decrements; projectile is destroyed when lifetime reaches zero.
- Collision is tested against all active actors whose collision group matches.
- On collision, the projectile is destroyed and the actor's collision script fires.
- 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_*_PRESSEDmacros instate_update()for edge-triggered actions. - Use
joy & J_*for continuous hold detection (movement, acceleration). - Let the On Init script end normally so
VM_UNLOCKfires —state_update()is blocked byVM_LOCK. - Never use infinite loops in On Init if your scene type relies on
state_update().