Two-layer parallax scrolling with a player sprite that triggers camera movement. Move with the D-pad. Walk past the threshold and the world scrolls to follow.
| Button | Action |
|---|---|
| D-Pad | Move the character |
Then open continuous_scroll.sfc in your emulator (Mesen2 recommended).
This example uses Mode 1 with two background layers: BG1 is the main scene, BG2 is a distant backdrop. When the camera scrolls, both layers move together (though parallax could use different speeds for a depth illusion). A character sprite overlays the scrolling backgrounds.
Both layers get their own tile data and tilemaps, loaded to different VRAM regions to avoid overlap:
Why palette slot 2 and 4? Each BG palette slot holds 16 colors. Slot 0 starts at CGRAM address 0, slot 2 at address 32, slot 4 at address 64. Using different slots means BG1 and BG2 get independent color palettes.
The player moves freely on screen. But when they reach a threshold (column 140 going right, column 80 going left), the background starts scrolling and the player gets pushed back:
The player walks right, reaches column 140, the world scrolls right while the player stays at column 140. It looks like the camera is following the character, but it's actually the background moving in the opposite direction.
Why push the player back? Without
player_x -= 1, the player would walk past the threshold and keep going off-screen. The push-back keeps them locked to the threshold while the world scrolls beneath them. This is the same pattern used in Super Mario World's camera system.
Scroll registers are write-only and take effect immediately. If you write them during active display (while the PPU is drawing), you'll get a visible tear – the top half of the screen shows the old scroll position, the bottom half shows the new one.
This example calls bgSetScroll() from the main loop, which does NOT write to the PPU directly. Instead, it stores the new scroll values and sets a dirty flag. The NMI handler (triggered at VBlank) checks the dirty flags and writes the actual scroll registers at a safe time:
This deferred-write mechanism ensures glitch-free scrolling without needing a custom VBlank callback. The initial scroll values are also set before setScreenOn() so the first visible frame shows the correct position.
The example reads the joypad directly from hardware registers after waiting for the auto-joypad read to complete:
All game variables live in one struct:
Why a struct instead of separate variables? The OpenSNES compiler has a known quirk: separate
static u16variables can generate broken code for some access patterns, while struct members work correctly. Using a struct also keeps related state together, which makes the code easier to follow.
SCROLL_THRESHOLD_RIGHT is too high (close to 256), the player can walk off-screen before scrolling kicks in.bg2_scroll_x += 1 every other frame while bg1_scroll_x += 1 every frame. The speed difference creates the depth illusion.player_x to a minimum of 0.bg_scroll_y. Vertical scrolling is slightly trickier because the SNES tilemap wraps every 32 rows.| Module | Why it's here |
|---|---|
console | PPU setup, NMI handler, WaitForVBlank() |
sprite | OAM buffer for the player character sprite |
input | Joypad buffer symbols needed by NMI handler |
background | bgSetMapPtr(), bgSetScroll(), bgInitTileSet() with dirty-flag deferred writes |
dma | dmaCopyVram(), dmaCopyCGram() for bulk asset transfers, plus OAM DMA in NMI |
| Register | Address | Role in this example |
|---|---|---|
| BGMODE | $2105 | Mode 1 (two 4bpp layers + one 2bpp) |
| BG1SC | $2107 | BG1 tilemap at $0000 |
| BG2SC | $2108 | BG2 tilemap at $0800 |
| BG1HOFS | $210D | BG1 horizontal scroll (via bgSetScroll) |
| BG2HOFS | $210F | BG2 horizontal scroll |
| TM | $212C | Enable OBJ + BG2 + BG1 |
| INIDISP | $2100 | Force blank / brightness control |
| File | What's in it |
|---|---|
main.c | Game loop, scrolling logic, input handling (~265 lines) |
data.asm | BG1/BG2 tiles, palettes, tilemaps, character sprite |
Makefile | LIB_MODULES := console sprite input background dma |