Loading...
Searching...
No Matches
Continuous Scroll

‍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.

Controls

Button Action
D-Pad Move the character

Build & Run

cd $OPENSNES_HOME
make -C examples/graphics/backgrounds/continuous_scroll

Then open continuous_scroll.sfc in your emulator (Mesen2 recommended).

What You'll Learn

  • How SNES background scrolling actually works (it's not moving tiles – it's moving the camera)
  • The threshold pattern: only scroll when the player reaches the screen edge
  • Parallax scrolling: two layers with independent scroll offsets
  • Why scroll register updates must happen during VBlank (and how bgSetScroll's dirty-flag mechanism guarantees it)

Walkthrough

1. Two Layers, Two Speeds

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.

REG_TM = 0x13; /* Enable OBJ + BG2 + BG1 */
#define BG_MODE1
Definition video.h:28
#define REG_TM
Main screen designation (W)
Definition registers.h:181
void setMode(u8 mode, u8 flags)
Set background mode.

Both layers get their own tile data and tilemaps, loaded to different VRAM regions to avoid overlap:

bgSetMapPtr(0, 0x0000, SC_32x32); /* BG1 tilemap at $0000 */
bgSetMapPtr(1, 0x0800, SC_32x32); /* BG2 tilemap at $0800 */
bgInitTileSet(0, bg1_tiles, bg1_pal, 2, /* BG1 tiles at $2000 */
..., BG_16COLORS, 0x2000);
bgInitTileSet(1, bg2_tiles, bg2_pal, 4, /* BG2 tiles at $4000 */
..., BG_16COLORS, 0x4000);
void bgInitTileSet(u8 bgNumber, u8 *tileSource, u8 *tilePalette, u8 paletteEntry, u16 tileSize, u16 paletteSize, u16 colorMode, u16 vramAddr)
Initialize tileset with tiles and palette.
void bgSetMapPtr(u8 bg, u16 vramAddr, u8 mapSize)
Set background tilemap address and size.
u8 bg2_tiles[]
u8 bg2_pal[]
u8 bg1_tiles[]
u8 bg1_pal[]
#define BG_16COLORS
Definition background.h:47
#define SC_32x32
Definition background.h:36

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.

2. The Threshold Scroll Pattern

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:

#define SCROLL_THRESHOLD_RIGHT 140
#define SCROLL_THRESHOLD_LEFT 80
game.player_x -= 1; /* Push player back to keep them visually centered */
}
GameState game
Global game state instance with initial values.
Definition main.c:103
#define MAX_SCROLL_X
Definition main.c:64
#define SCROLL_THRESHOLD_RIGHT
Definition main.c:65
s16 bg1_scroll_x
Definition main.c:96
s16 player_x
Definition main.c:94
s16 bg2_scroll_x
Definition main.c:98

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.

3. Scroll Updates via bgSetScroll Dirty Flags

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:

/* In the main loop -- sets dirty flags, does NOT touch PPU registers */
void bgSetScroll(u8 bg, u16 x, u16 y)
Set background scroll position.
s16 bg2_scroll_y
Definition main.c:99
s16 bg1_scroll_y
Definition main.c:97

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.

4. Input Reading

The example reads the joypad directly from hardware registers after waiting for the auto-joypad read to complete:

while (REG_HVBJOY & 0x01) { } /* Wait for auto-joypad */
pad = REG_JOY1L | (REG_JOY1H << 8);
void WaitForVBlank(void)
Wait for next VBlank period.
static u16 bx
Definition main.c:159
#define REG_JOY1H
Joypad 1 data high (R)
Definition registers.h:328
#define REG_JOY1L
Joypad 1 data low (R)
Definition registers.h:325
#define REG_HVBJOY
H/V blank and joypad status (R)
Definition registers.h:301

5. The Game State Struct

All game variables live in one struct:

typedef struct {
s16 bg1_scroll_x;
s16 bg1_scroll_y;
s16 bg2_scroll_x;
s16 bg2_scroll_y;
GameState game = {20, 100, 0, 32, 0, 32};
static s16 player_y
Player Y position in screen coordinates.
Definition main.c:57
static s16 player_x
Player X position in screen coordinates.
Definition main.c:55
signed short s16
16-bit signed integer (-32768 to 32767)
Definition types.h:49
Centralized game state structure.
Definition main.c:93

Why a struct instead of separate variables? The OpenSNES compiler has a known quirk: separate static u16 variables 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.

</blockquote>

Tips & Tricks

  • Player gets stuck at the edge? Check your threshold values. If SCROLL_THRESHOLD_RIGHT is too high (close to 256), the player can walk off-screen before scrolling kicks in.
  • Want true parallax? Change BG2's scroll increment to be slower than BG1's. bg2_scroll_x += 1 every other frame while bg1_scroll_x += 1 every frame. The speed difference creates the depth illusion.
  • Sprite disappears at the left edge? The SNES uses 9-bit X coordinates for sprites. Values below 0 wrap to 256+, which is off-screen. Clamp player_x to a minimum of 0.

Go Further

  • Add tile streaming: Right now the tilemap is static. For larger worlds, you'd upload new tile columns as the camera scrolls. See LikeMario for a working implementation.
  • Add vertical scrolling: Same pattern, but with Y thresholds and bg_scroll_y. Vertical scrolling is slightly trickier because the SNES tilemap wraps every 32 rows.
  • Add animation: Use different sprite tiles based on direction. Tile 0 for right, tile 1 for left, advance every few frames for a walk cycle.
  • Next example: LikeMario – full platformer with gravity, collision, and tile streaming.

Under the Hood: The Build

The Makefile

TARGET := continuous_scroll.sfc
USE_LIB := 1
LIB_MODULES := console sprite input background dma
CSRC := main.c
ASMSRC := data.asm

Why These Modules?

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

Technical Reference

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

Files

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

Credits

  • Original: odelot (PVSnesLib example)
  • Sprite: Calciumtrice (CC-BY 3.0)
  • Backgrounds inspired by Streets of Rage 2