Scroll & Camera
The scroll and camera system is implemented in C.
Scroll System Overview
- Game Boy hardware BG map is 32×32 tiles (256×256 px), wrapping
- GB Studio scenes can be much larger — scroll system manages incremental tile loading
scroll_update()computesdraw_scroll_x/draw_scroll_yfrom camera position- Formula:
y = SUBPX_TO_PX(camera_y) - SCREEN_HEIGHT_HALF - Clamped to
[scroll_y_min, scroll_y_max] - Result stored in
draw_scroll_x/draw_scroll_y(used by ISR for SCX/SCY registers)
Scroll Boundaries
scroll_x_max=image_width - SCREEN_WIDTHin pixelsscroll_y_max=image_height - SCREEN_HEIGHTin pixelsscroll_x_min,scroll_y_min— usually 0- Scene dimensions:
image_tile_width,image_tile_height
Incremental Tile Loading
- Refresh window: 23×21 tiles (screen + padding)
PENDING_BATCH_SIZE= 11 — max tiles loaded per frame- As camera scrolls, new rows/columns are loaded from tilemap data
Key Functions
| Function | Description | Notes |
|---|---|---|
scroll_load_row(x, y) |
Load a single tile row | NONBANKED. Not declared in scroll.h — needs manual declaration:void scroll_load_row(UBYTE x, UBYTE y); |
scroll_load_col(x, y) |
Load a single tile column | NONBANKED |
scroll_repaint() |
Full screen repaint (loads all visible rows/columns) | Called during scene loading |
Scroll Offsets
scroll_offset_x,scroll_offset_y— fine offset applied to scroll position- Used by HandheldCamera plugin for screen shake effects
- Added to scroll output:
draw_scroll_x = scroll_x + scroll_offset_x
Camera System
camera_x,camera_y— UINT16, position in subpixels- Camera tracks the player by default
Camera Settings
camera_settings bitmask:
| Flag | Value | Description |
|---|---|---|
CAMERA_UNLOCKED |
0x00 |
Camera follows player |
CAMERA_LOCK_X_FLAG |
0x01 |
Lock horizontal position |
CAMERA_LOCK_Y_FLAG |
0x02 |
Lock vertical position |
Camera Parameters
| Variable | Type | Description |
|---|---|---|
camera_offset_x, camera_offset_y |
BYTE | Offset from player center |
camera_deadzone_x, camera_deadzone_y |
BYTE | How far player can move before camera follows |
camera_clamp_x, camera_clamp_y |
UINT16 | Boundary clamping values |
Camera Update
camera_update()runs every frame (not blocked byVM_LOCK)- When unlocked: tracks player position with offset and deadzone
- When locked: stays at fixed position
Parallax System
- Up to 3 scroll layers via LYC-triggered ISR
parallax_LCD_isr()— NONBANKED ISR that updates SCX at scanline boundaries- Set all
parallax_rowsto 0 to disable parallax in custom scene types
parallax_rows[3] Struct
| Field | Description |
|---|---|
scx | Horizontal scroll value for this layer |
next_y | Scanline where next layer begins |
shift | Scroll speed divisor (bit shift) |
start_tile | First tile row of this layer |
tile_height | Height of this layer in tile rows |
shadow_scx | Buffered SCX for VBlank timing |
Parallax Example
// 3-layer parallax: sky (slow), mountains (medium), ground (fast)
parallax_rows[0].shift = 2; // sky scrolls at 1/4 speed
parallax_rows[0].next_y = 48; // sky is 48 scanlines
parallax_rows[1].shift = 1; // mountains at 1/2 speed
parallax_rows[1].next_y = 96; // mountains end at scanline 96
parallax_rows[2].shift = 0; // ground at full speed
parallax_rows[2].next_y = 144; // rest of screen
GBVM Camera Commands
| Command | Description |
|---|---|
VM_CAMERA_MOVE_TO | Move camera to position |
VM_CAMERA_SET_POS | Instantly set camera position |
VM_CAMERA_LOCK | Lock camera to current position |
VM_CAMERA_UNLOCK | Resume following player |
VM_CAMERA_SET_FOLLOW | Set follow parameters (offset, deadzone) |
Common Patterns
Custom Scene with Locked Camera
void my_scene_init(void) BANKED {
camera_settings = CAMERA_LOCK_X_FLAG | CAMERA_LOCK_Y_FLAG;
camera_x = PX_TO_SUBPX(80); // center of screen
camera_y = PX_TO_SUBPX(72);
}
Pre-loading Tile Rows
// Pre-load rows that aren't visible yet
for (UINT8 y = visible_end; y < visible_end + 3; y++) {
scroll_load_row(0, y);
}
Sprite OAM Timing
draw_scroll_y at the start of state_update still holds the previous frame's value, matching the sprite OAM that was DMA'd. Buffer it for your ISR before scroll_update overwrites it.
Parallax Conflicts
Custom ISR plugins must clear parallax_rows and set scene_LCD_type = LCD_simple to prevent parallax ISR conflicts.