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)
}
Bank 255

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

typecTypeDescription
numberUBYTEUnsigned byte (0–255)
numberWORDSigned 16-bit (-32768 to 32767)
numberUWORDUnsigned 16-bit (0–65535)
sliderUBYTESlider control in editor
checkboxUBYTEBoolean toggle (0 or 1)

Engine Field Timing

Understanding when engine field values become available is critical for correct plugin behavior.

Timing: state_init() vs On Init Script

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 after VM_UNLOCK.

Scene Type Dispatch

GB Studio dispatches scene type calls through assembly function pointer tables.

  • states_caller.s indexes into _state_start_fns and _state_update_fns tables using the scene_type enum value.
  • states_ptrs.s is auto-generated from all plugin engine.json files, listing function pointers for each scene type.
  • Adding a new scene type via plugin automatically extends these tables.
  • The enum order in states_defines.h must match the table order in states_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.).

Core File Conflicts

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)
Undeclared Functions

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.h
  • void 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

  1. Read engine field defaults in state_init() — they are set before scene loading.
  2. Access actors directly: actors[0] = player, actors[N + 1] for scene actor with _index N.
  3. Use INPUT_*_PRESSED macros in state_update() for edge detection.
  4. Let On Init script end normally (no infinite loop) so VM_UNLOCK fires and state_update() runs.
Actor Index Off-by-One

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;
    }
}
ANIM_PAUSED Pattern

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;
}
Parallax Conflicts

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_LOCK at start and VM_UNLOCK at 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 blocks state_update().
  • Actor updateScripts are NOT blocked — they run via script_runner_update() which executes unconditionally.
Never Use Infinite Loops in On Init

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:

  1. remove_LCD_ISRs()
  2. script_runner_init()
  3. load_scene() — loads BG, actors, triggers; queues On Init script
  4. Install standard LCD ISR based on scene_LCD_type
  5. Re-enable sprites (SHOW_SPRITES)
  6. player_init()
  7. state_init() — your plugin's init function
  8. toggle_shadow_OAM()
  9. camera_update()
  10. scroll_repaint()
  11. actors_update()
  12. actors_render() — actors appear BEFORE On Init script runs
  13. activate_shadow_OAM()
  14. Fade in (if enabled)
Actor Flash on Scene Load

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.