Scene Type Plugins
Plugin Architecture
Scene type plugins extend GB Studio with custom scene behaviors — flight combat, minigames, elevators, or any game mechanic that needs per-frame logic. The scene type implementation is written in C, while plugin metadata and engine field definitions use JSON. Each plugin lives in a self-contained directory under plugins/.
plugins/[PluginName]/
plugin.json Plugin metadata
engine/
engine.json Scene types + engine fields
include/states/[name].h Header file
src/states/[name].c Implementation
src/core/[file].c Core engine overrides (optional)
events/
event[Name].js Custom event definitions (optional)
plugin.json
Minimal metadata file at the plugin root. Required by GB Studio to recognize the plugin.
{
"name": "My Plugin",
"author": "Your Name",
"version": "1.0.0"
}
engine.json Format
Defines scene types and engine fields. The version field should match your GB Studio engine version. Each scene type entry lists its source files (relative to engine/), and each field declares a global C variable accessible from both C code and the GB Studio editor.
{
"version": "4.2.0-e29",
"sceneTypes": [{
"key": "scene_key",
"label": "Display Name",
"files": [
"include/states/name.h",
"src/states/name.c"
]
}],
"fields": [{
"key": "c_variable_name",
"label": "Editor Label",
"group": "Group Name",
"type": "number",
"cType": "UBYTE",
"defaultValue": 0,
"min": 0,
"max": 255
}]
}
Scene Type Implementation
Every scene type consists of a header and an implementation file. The engine calls name_init() once when the scene loads, and name_update() every frame during gameplay.
Header File (include/states/name.h)
#ifndef STATE_NAME_H
#define STATE_NAME_H
#include <gbdk/platform.h>
BANKREF_EXTERN(STATE_NAME)
void name_init(void) BANKED;
void name_update(void) BANKED;
#endif
Implementation File (src/states/name.c)
#pragma bank 255
#include "data/states_defines.h"
#include "states/name.h"
BANKREF(STATE_NAME)
// Engine fields -- global C variables set by GBVM events
UBYTE my_field;
WORD my_speed;
void name_init(void) BANKED {
// Called once when scene loads
// Runs BEFORE On Init script
}
void name_update(void) BANKED {
// Called every frame
// Blocked by VM_LOCK (On Init script holds lock)
}
All scene type implementations must use #pragma bank 255. This tells SDCC to place the code in auto-banked ROM, which GB Studio manages automatically.
Engine Fields
Engine fields are global C variables declared in the scene type .c file and configured via the fields array in engine.json. They appear in the GB Studio UI under scene properties and can be set at runtime via ENGINE_FIELD_SET events in On Init scripts.
Field Types
| type | cType | Description |
|---|---|---|
number | UBYTE | Unsigned byte (0–255) |
number | WORD | Signed 16-bit (-32768 to 32767) |
number | UWORD | Unsigned 16-bit (0–65535) |
slider | UBYTE | Slider control in editor |
checkbox | UBYTE | Boolean toggle (0 or 1) |
Engine Field Timing
Understanding when engine field values become available is critical for correct plugin behavior.
Engine field defaults from engine.json ARE available in state_init() — they are set during script_engine_init.s before scene loading. However, values overridden by the On Init script are NOT yet applied at that point.
state_init()runs before the On Init script executes.- The On Init script runs later in the main loop via
script_runner_update(). - For simple plugins using only defaults: safe to read fields in
state_init(). - For plugins needing script-configured values: defer reading to the first
state_update()call afterVM_UNLOCK.
Scene Type Dispatch
GB Studio dispatches scene type calls through assembly function pointer tables.
states_caller.sindexes into_state_start_fnsand_state_update_fnstables using thescene_typeenum value.states_ptrs.sis auto-generated from all pluginengine.jsonfiles, listing function pointers for each scene type.- Adding a new scene type via plugin automatically extends these tables.
- The enum order in
states_defines.hmust match the table order instates_ptrs.s.
You do not need to manually edit these files — GB Studio generates them from your engine.json definition.
Core File Overrides
Plugins can override engine source files by placing replacements in engine/src/core/:
plugins/MyPlugin/engine/src/core/actor.c → replaces build/src/src/core/actor.c
plugins/MyPlugin/engine/src/core/trigger.c → replaces build/src/src/core/trigger.c
This is a powerful mechanism for modifying core engine behavior (collision handling, actor management, trigger logic, etc.).
Multiple plugins overriding the same core file will conflict — only one plugin's version will be used. If two plugins both override actor.c, you must manually merge their changes into a single file.
Common Includes
Scene type implementations typically need some combination of these headers:
#include <gbdk/platform.h> // GBDK types, BANKED, BANKREF
#include "system.h" // System-level definitions
#include "camera.h" // camera_x, camera_y, camera_settings
#include "scroll.h" // scroll_update, scroll_offset_x/y
#include "actor.h" // actors[], actor_t, PLAYER macro
#include "input.h" // INPUT_*_PRESSED macros, joy
#include "math.h" // SIN(), COS(), angle constants
#include "interrupts.h" // remove_LCD_ISRs(), add_LCD()
#include "data_manager.h" // image_tile_width, image_tile_height
#include "data/states_defines.h" // Scene type enum (required)
Some engine functions are defined but not declared in their headers. Add manual extern declarations when needed:
extern UBYTE overlay_cut_scanline;— defined in interrupts.c, not in interrupts.hvoid scroll_load_row(UBYTE x, UBYTE y);— defined NONBANKED in scroll.c, not in scroll.h
Complete Example: Minimal Scene Type
A complete, working minimal scene type plugin that moves an actor based on input and engine field values.
plugin.json
{
"name": "MyScene",
"author": "Your Name",
"version": "1.0.0"
}
engine/engine.json
{
"version": "4.2.0-e29",
"sceneTypes": [{
"key": "myscene",
"label": "My Scene",
"files": [
"include/states/myscene.h",
"src/states/myscene.c"
]
}],
"fields": [{
"key": "myscene_speed",
"label": "Move Speed",
"group": "My Scene",
"type": "number",
"cType": "UBYTE",
"defaultValue": 16,
"min": 1,
"max": 64
}, {
"key": "myscene_gravity",
"label": "Gravity",
"group": "My Scene",
"type": "number",
"cType": "WORD",
"defaultValue": 8,
"min": 0,
"max": 100
}]
}
engine/include/states/myscene.h
#ifndef STATE_MYSCENE_H
#define STATE_MYSCENE_H
#include <gbdk/platform.h>
BANKREF_EXTERN(STATE_MYSCENE)
void myscene_init(void) BANKED;
void myscene_update(void) BANKED;
#endif
engine/src/states/myscene.c
#pragma bank 255
#include "data/states_defines.h"
#include "states/myscene.h"
#include <gbdk/platform.h>
#include "actor.h"
#include "input.h"
#include "camera.h"
BANKREF(STATE_MYSCENE)
// Engine fields
UBYTE myscene_speed;
WORD myscene_gravity;
// Internal state
static UBYTE initialized;
static INT16 velocity_y;
void myscene_init(void) BANKED {
initialized = FALSE;
velocity_y = 0;
// Engine field defaults from engine.json are available here
// myscene_speed == 16, myscene_gravity == 8
// Lock camera to player
camera_offset_x = 0;
camera_offset_y = 0;
}
void myscene_update(void) BANKED {
actor_t *player = &actors[0]; // PLAYER
if (!initialized) {
// First update after VM_UNLOCK
// Script-overridden engine field values are now available
initialized = TRUE;
}
// Horizontal movement
if (INPUT_LEFT) {
player->pos.x -= PX_TO_SUBPX(myscene_speed >> 3);
}
if (INPUT_RIGHT) {
player->pos.x += PX_TO_SUBPX(myscene_speed >> 3);
}
// Vertical with gravity
if (INPUT_UP_PRESSED) {
velocity_y = -PX_TO_SUBPX(4); // Jump impulse
}
velocity_y += myscene_gravity; // Apply gravity
player->pos.y += velocity_y;
}
Proven Pattern: Scene Type with Actor Access
This pattern, proven by the Flight plugin, shows how to directly access and control actors from C code in a scene type plugin.
Key Principles
- Read engine field defaults in
state_init()— they are set before scene loading. - Access actors directly:
actors[0]= player,actors[N + 1]for scene actor with_index N. - Use
INPUT_*_PRESSEDmacros instate_update()for edge detection. - Let On Init script end normally (no infinite loop) so
VM_UNLOCKfires andstate_update()runs.
actors[0] is always the player. Scene actors with _index N in the .gbsres file load into actors[N + 1]. Always add 1 to the scene index when accessing actors from C code.
Example: Direct Actor Control
#pragma bank 255
#include "data/states_defines.h"
#include "states/combat.h"
#include <gbdk/platform.h>
#include "actor.h"
#include "input.h"
#include "camera.h"
#include "math.h"
BANKREF(STATE_COMBAT)
UBYTE combat_speed;
void combat_init(void) BANKED {
// Access actors directly at init
actor_t *player = &actors[0];
actor_t *enemy = &actors[2]; // Scene actor _index 1
// Pause animation for manual frame control
player->anim_tick = 255; // ANIM_PAUSED
player->frame = 0;
// Position enemy
enemy->pos.x = PX_TO_SUBPX(120);
enemy->pos.y = PX_TO_SUBPX(64);
// Camera setup
camera_offset_x = 0;
camera_offset_y = 0;
}
void combat_update(void) BANKED {
actor_t *player = &actors[0];
// Edge detection: only triggers on first frame of press
if (INPUT_A_PRESSED) {
// Fire projectile, change state, etc.
player->frame = 2; // Attack frame
}
// Continuous input: held every frame
if (INPUT_LEFT) {
player->pos.x -= PX_TO_SUBPX(combat_speed);
player->frame = 1; // Walk frame
}
if (INPUT_RIGHT) {
player->pos.x += PX_TO_SUBPX(combat_speed);
player->frame = 1;
}
}
Set actor->anim_tick = 255 to stop the engine from auto-advancing animation frames. You can then set actor->frame directly from C code each frame. Without this, the engine's animation system will overwrite your frame values.
Custom LCD Interrupts
Advanced scene types may need a custom LCD interrupt service routine (ISR) for effects like split-screen scrolling, parallax, or raster effects.
ISR Installation
The engine installs a standard ISR after load_scene(). Your plugin must replace it in state_init():
#include "interrupts.h"
// Forward declaration
void my_LCD_isr(void) NONBANKED;
void myscene_init(void) BANKED {
// Remove the standard ISR installed by core.c
remove_LCD_ISRs();
// Install your custom ISR
CRITICAL {
add_LCD(my_LCD_isr);
}
// Disable parallax to prevent conflicts
parallax_rows[0].scx = 0;
parallax_rows[1].scx = 0;
parallax_rows[2].scx = 0;
scene_LCD_type = LCD_simple;
}
Custom ISR plugins must clear parallax_rows and set scene_LCD_type = LCD_simple to prevent the standard parallax ISR from interfering with your custom interrupt handler.
Sprite Visibility
The VBlank ISR checks the hide_sprites variable every frame and overrides the LCDC register. Calling the HIDE_SPRITES macro alone is immediately undone.
// WRONG: overridden next VBlank
HIDE_SPRITES;
// CORRECT: persists across frames
hide_sprites = TRUE;
// Re-enable later
hide_sprites = FALSE;
VM_LOCK and state_update()
The state_update() function is guarded by if (!VM_ISLOCKED()) in the main loop. This has important implications:
- On Init scripts auto-generate
VM_LOCKat start andVM_UNLOCKat end. state_update()will not run until the On Init script completes and releases the lock.- An infinite loop (
LOOP_WHILE) in the On Init script permanently blocksstate_update(). - Actor
updateScripts are NOT blocked — they run viascript_runner_update()which executes unconditionally.
If your scene type relies on state_update() for game logic, never use infinite loops in the On Init script. The VM_UNLOCK at the end will never execute, permanently blocking your update function.
Scene Loading Sequence
Understanding the exact loading order helps avoid initialization bugs:
remove_LCD_ISRs()script_runner_init()load_scene()— loads BG, actors, triggers; queues On Init script- Install standard LCD ISR based on
scene_LCD_type - Re-enable sprites (
SHOW_SPRITES) player_init()state_init()— your plugin's init functiontoggle_shadow_OAM()camera_update()scroll_repaint()actors_update()actors_render()— actors appear BEFORE On Init script runsactivate_shadow_OAM()- Fade in (if enabled)
Actors are rendered in step 12, before the On Init script can deactivate or reposition them. If actors need script-side setup, set hide_sprites = TRUE in state_init() and clear it on the first state_update() call to prevent a single-frame flash.