Skip to content

Repository files navigation

GBA Lua SDK

npm version

Make games for the Game Boy Advance by writing a PICO-8-flavored Lua instead of C or ARM assembly.

The SDK ahead-of-time compiles your Lua to C, builds it with a bundled ARM toolchain against a libtonc + maxmod runtime, and produces a .gba ROM that runs in mGBA and on real hardware. No interpreter, no VM: your Lua becomes native ARM machine code.

If you know PICO-8 you'll feel at home (spr/btn/_init/_update/_draw, Lua syntax) - but this SDK leans into what the GBA can do that a fantasy console can't: 128 hardware sprites, rotate/scale (affine) sprites, four scrolling tile layers, Mode 7, hardware windows, alpha blending and fades, mosaic, per-scanline raster effects, battery saves, and streamed module music. Familiarity, not compatibility.

This is a fork of the GameTank Lua SDK; it reuses that compiler front-end and PICO-8 number model, and retargets the back end to native ARM.

Your first game

The whole hello, one main.lua with no asset files: a greeting plus a hardware sprite you move with the d-pad. _update60 runs the movement 60 times a second; _draw redraws the sprite every frame (examples/hello/main.lua):

-- The screen is 240x160. spr uses the built-in default sheet, so no art file
-- is needed. Colors are PICO-8-style indices 0-15 (1 dark-blue, 14 pink).
local x, y = 112, 72

function _update60()               -- 60fps input + movement
  if (btn(1)) then x += 2 end      -- right
  if (btn(0)) then x -= 2 end      -- left
  if (btn(3)) then y += 2 end      -- down
  if (btn(2)) then y -= 2 end      -- up
end

function _draw()
  cls(1)                           -- dark-blue background layer
  print("hello gba", 88, 24, 14)   -- greeting near the top, pink
  spr(0, x, y, 2, 2)               -- the hardware sprite, redrawn every frame
end

a small critter hardware sprite and 'hello gba' text on a dark blue 240x160 GBA screen

Build it and play it in a window - no external emulator needed:

node bin/gbalua.js run examples/hello/main.lua

gbalua run opens a window over the bundled mGBA core (arrows = d-pad, Z/X = B/A, Enter = START), via the shared romdev-core-runner SDL host. Or build a .gba ROM to ship:

node bin/gbalua.js build examples/hello/main.lua -o hello.gba

Run hello.gba in mGBA (or any GBA emulator) or flash it to a cartridge. That's the whole loop: write main.lua, run it, ship the .gba.

Colors are PICO-8-style indices 0-15 (0 black, 1 dark-blue, 10 yellow, 14 pink); pal() / spr_col() reach the full 15-bit BGR555 palette (32768 colors) at runtime when you want more.

Featured example: a full shmup

the starfall shmup: a player ship firing at a formation of alien sprites over a starfield, with a SCORE/LIVES HUD, on the GBA

examples/starfall is a complete little shmup - hardware sprites, a scrolling starfield, music + sfx, a HUD, win/lose states - in one main.lua. That's what this SDK is for:

node bin/gbalua.js build examples/starfall/main.lua \
  --sheet examples/starfall/shmup_sheet.png \
  --map examples/starfall/space_bg.png -o starfall.gba

Requirements

  • Node.js 24+
  • nothing else — npm install brings the whole toolchain as dependencies: arm-gcc + libtonc + maxmod, all as WebAssembly (via romdev-platform-gba + romdev-maxmod). No devkitPro, no native tools to build or install.

The build runs the WASM toolchain in-process (cc1-arm → as → ld → objcopy) — no server, no daemons. It can also talk to a running romdev server instead (GBALUA_BACKEND=mcp, faster for rapid rebuilds since the server keeps warm toolchain workers). See compiler/build-gba.mjs.

gbalua run opens the window through romdev-core-runner (the one SDL host shared across the whole SDK family and the romdev playtest tool). The window needs @kmamal/sdl, an optional dependency of the runner (pulled by npm install, prebuilt for the common platforms); on a headless box or unsupported platform, run prints a clear message and you build a .gba for an external emulator instead. build never needs it.

The screen and the two rendering modes

The GBA screen is 240×160. Two ways to draw, and they are one either-or (a hardware mode bit); sprites, sound, and all the color effects compose over both.

  • Tile mode (Mode 0) - the real game path. Four hardware tile layers that scroll for free, plus 128 hardware sprites on top. This is how scrolling GBA games work; it's what the flagship example (starfall) uses.
  • Bitmap mode (Mode 4) - an immediate-mode 8bpp framebuffer for pset/rect/circ/line + text. Simplest to start with (the hello above), single-buffered, slower per-pixel. Sprites still compose on top.

You don't pick a mode explicitly - using tile-layer verbs (map_show, tileset, layer_*, mode7) puts you in tile mode; the immediate draw verbs use bitmap mode.

The PICO-8 contract

Define _update60() (60 fps) or _update() (30 fps), plus _draw(), and optionally _init(). The runtime latches input before each update and ends the frame after _draw() (OAM flush, vblank).

Numbers are PICO-8 numbers: 16.16 fixed point. sin/cos/atan2 use turns (0..1) with PICO-8's screen-space-inverted sin. The compiler infers which values stay integral and keeps them in fast 32-bit ints - an optimization, never a semantic change.

The dialect keeps PICO-8's syntax: +=-style compound assignment, one-line if (cond) stmt / while (cond) stmt, !=, \ floor division, // comments, hex/binary literals with fractions, and multiple assignment (x, y = 64, 32).

Arrays are 1-indexed (Lua/PICO-8 style): a = array(8), then a[1] is the first element (a[0] is out of bounds).

API at a glance

lifecycle _init _update _update60 _draw
bitmap draw cls camera color pset pget sset clip rect rectfill circ circfill line
16-bit bitmap mode15() true color · rgb15(r,g,b) · cls15 pset15 flip15 - a 160×128 BGR555 framebuffer
sprites spr(n,x,y,[w,h],[fx,fy]) hardware OBJ · spr8(t,x,y,[flip]) · spr_pal spr_prio
affine sprites sprr(n,x,y,angle,scale) rotate+scale · sprr2(n,x,y,angle,sx,sy) non-uniform
tile layers map_show tileset tilemap layer_show layer_pri layer_scroll parallax camera mget/tget/tset
mode 7 mode7() · mode7_cam(x,y,angle,[zoom]) · mode7_off() - an affine plane on BG2
affine BG abg_setup(tiles,ntiles,map,msize,[pal]) · abg_cam(x,y,angle,[zoom]) · abg_off() - your own rotate/scale layer
windows window(x0,y0,x1,y1) spotlight · window_inside/window_outside/window_obj · window_off
color effects fade(amount,[white]) · blend(layer,alpha) · blend_off · mosaic/mosaic2 · backdrop · screen_off/screen_on
palette pal(i,r,g,b) BG · spr_col(i,r,g,b) OBJ · hgradient(table) per-scanline backdrop
animation anim(slot,first,last,fps) loop · anim_once · anim_pingpong · anim_reset · anim_done
input btn(i,[pl]) btnp(i,[pl]) - 0-3 d-pad, 4=A, 5=B, 6=L, 7=R, 8=START, 9=SELECT
math flr ceil abs sgn sqrt min max mid sin cos atan2 rnd srand t/time + bit ops
data array(n,[v]) 16.16 · array8(n,[v]) bytes 0-255 · pool(n) · dma(dst,src,n)/dma_fill fast moves
save save(slot,array8,n) · load(slot,array8,n) - battery SRAM, 16 slots × 1 KB
timer timer_start() · timer_read() - free-running Timer 3, sub-frame profiling
sound music(n,[loop]) module music · sfx(n,[ch]) · sfx_ex(n,vol,pan,pitch) · sfx_volume

See docs/CHEATSHEET.md for every verb with signatures.

Assets

--sheet sprites.png imports a sprite sheet, --map level.png a tilemap, and --mode7 plane.png an affine plane, via a self-contained PNG → tile converter (compiler/png-tiles.mjs). All three also accept the formats artists actually work in — Aseprite (.ase/.aseprite, frame 0 flattened) and Tiled (.tmx, visible layers composited; embed the tileset in the map) — imported, never re-invented. They're CLI flags on build:

node bin/gbalua.js build --target gba mygame/main.lua \
  --sheet mygame/sprites.ase --map mygame/level.tmx -o mygame/game.gba

The examples/ directory shows each subsystem in use:

the showcase demo: shapes, a clipped ring burst, and a line on a dark blue screen

example shows
showcase a scene-cycling tour of the whole feature set (L/R to switch scenes)
starfall a complete shmup - tile mode, sprite HUD, module music + SFX
effects blend / fade in bitmap mode
mode7 an affine plane you rotate, zoom, and drive over
windows a hardware spotlight over the Mode 7 plane
anim frame-range animation helpers
hwtest hgradient raster gradient, SRAM save/load, the timer

Sound

Music is a streamed module (maxmod), and SFX are sampled one-shots - trigger both by index, PICO-8 style:

function _init()
  music(0)                       -- start module 0, looping
end
function _update60()
  if btnp(4) then sfx(0) end      -- A -> a sound effect
  if btnp(5) then sfx_ex(1, 512, 200, 1.5) end  -- B -> louder, panned, +pitch
end

music(-1) stops; music(n, false) plays once. sfx_ex(n, vol, pan, pitch) gives per-shot volume (0-1024), pan (0-255, 128 center), and pitch (a 16.16 multiplier); sfx_volume(v) sets the master SFX level.

Bring your own music with --music song.xm on build (repeatable — music(0) plays the first module, music(1) the second, …; .xm/.mod/ .it/.s3m all work). The soundbank is compiled at build time by romdev-maxmod, a faithful pure-JS port of devkitPro's mmutil. Or link a prebuilt bank with --soundbank bank.bin. With neither flag, the default soundbank (assets/soundbank.bin) ships a chiptune as module 0.

Not-Lua walls (loud, never silent)

Conditions must be boolean (if x ~= 0 then, not if x then - Lua calls 0 truthy, C doesn't, and the compiler refuses to guess). No nil, closures, metatables, coroutines, string concatenation, or goto. Every unsupported feature is a compile-time error that says what to write instead.

Real PICO-8 carts (full language)

gbalua build compiles the static PICO-8-flavored dialect described above. gbalua pico8 compiles unmodified PICO-8 carts instead: .p8, .p8.png, or a .lua file using the whole PICO-8 language (tables, closures, metatables, coroutines, strings). The cart goes through luacretro's dynamic tier to C and links against luacretro's PICO-8 runtime plus gba-sdk/lc_gba.c; there is still no interpreter on the console.

npx gbalua pico8 cart.p8.png -o game.gba

The 128x128 screen sits 1:1 in the middle of the GBA screen. PICO-8 audio comes from luacretro's fast synth (exact sequencer, wavetable voices) on DirectSound; cartdata saves to SRAM. The heap is 120 KB of EWRAM, which fits most carts; CPU is the limit (16.8 MHz), so light carts run at full speed and heavy ones slow down.

See luacretro's DYNAMIC.md for what is supported and known differences.

Repo layout

compiler/ Lua→C compiler + the GBA build driver (build-gba.mjs) and PNG importer (png-tiles.mjs) · gba-sdk/ the C runtime (thin libtonc/maxmod wrappers: gba_api.c frame+sprites+input, gba_bg.c tile layers, gba_mode7.c, gba_win.c, gba_fx.c effects, gba_sound.c, gba_hw.c save+timer, gba_text.c, gba_anim.c, gba_math.c) · assets/ the default soundbank + its generators · bin/gbalua.js CLI · examples/.

Docs

doc what
docs/CHEATSHEET.md the full GBA Lua API reference
docs/CHEATSHEET_FOR_PICO8_USERS.md per-function PICO-8 → GBA map

License

MIT. gba-sdk/ wraps libtonc and maxmod (both zlib/BSD-style); the compiler front-end derives from the GameTank Lua SDK (MIT).

About

Write Game Boy Advance games in PICO-8-flavored Lua, ahead-of-time compiled to native ARM. Hardware sprites, Mode 7, music, bundled WASM toolchain.

Topics

Resources

Stars

1 star

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages