Your first game
This page goes from an empty directory to a ROM running in an emulator. It assumes the gbsharp tool and its toolchain from installation: dotnet tool install --global gbsharp then gbsharp doctor --fix. Working from a checkout instead, read every gbsharp <command> below as dotnet run --project GBSharp.CLI -- <command>.
Create a project
gbsharp new MyGame --template sprite
new takes a template with --template (or -t), a machine with --target (gb for the original Game Boy, the default, or gbc for Game Boy Color), and a directory with --out (defaulting to one named after the project). It refuses a directory that already has files in it unless you pass --force: the one thing this command must never do is overwrite work.
There are three templates:
- empty (the default) is a
Mainthat enables the display and runs the canonical GB# frame loop:while (true)withGame.WaitVBlank()at the bottom, and a comment marking where everything else goes. - sprite is a sprite you move with the d-pad. Its tile data is written by hand as bytes in a
static readonlyarray, which puts it in the cartridge rather than work RAM; the build report shows what it cost. - background is a full-screen image loaded through the asset pipeline and scrolled. This is the only template that needs art, and rather than shipping a checked-in binary nobody can review, the CLI synthesises a placeholder PNG (
Assets/tiles.png) when it writes the project.
The sprite template's Program.cs, in full:
using GB;
using static GB.Hardware;
public static class Program
{
// Two tiles of 2bpp data: 16 bytes each, in the cartridge because it
// is 'static readonly'. The build report shows what it cost.
private static readonly byte[] Shape =
{
0x3C, 0x3C, 0x42, 0x7E, 0x81, 0xFF, 0xA5, 0xFF,
0x81, 0xFF, 0xBD, 0xFF, 0x42, 0x7E, 0x3C, 0x3C,
};
public static void Main()
{
Tiles.LoadSprite(0, 1, Shape);
Display.Enable();
Display.ShowSprites();
byte x = 80;
byte y = 72;
Sprites[0].Tile = 0;
while (true)
{
if (Input.Right) x++;
if (Input.Left) x--;
if (Input.Down) y++;
if (Input.Up) y--;
Sprites.Move(0, x, y);
Game.WaitVBlank();
}
}
}
That is a complete game: it boots, draws a sprite, and responds to input. What the C# subset does and does not include is covered in the language subset.
What new scaffolds
Every template writes the same frame around Program.cs:
gbsharp.jsonis the project file, holding just the name and target to start with. See project layout for what else can go in it.MyGame.csprojis for your editor, not the build. It references the GB# framework and analyzers throughGBSharp.Sdk, so you get completion, navigation and GB# diagnostics as you type. Building it is an error by design;gbsharp buildmakes the ROM..gitignoreis one line, ignoringbuild/..vscode/tasks.jsonhas tasks forgbsharp: build(wired as the default build task, so Ctrl+Shift+B builds the ROM),run,analyzeandclean. They shell out to the samegbsharppipeline documented here, with the path to the CLI you rannewfrom baked in, so the editor and the terminal reach the exact same compiler..vscode/launch.jsonmakes F5 build and launch the emulator, withBuild onlyandAnalyze (lint)configurations beside it. This is not a real debug session: there is no GBZ80 debugger wired into VS Code, so the configurations run the CLI in a terminal. Source-level debugging happens in the emulator itself, from the.symfile written beside the ROM.
Build it
gbsharp build MyGame
The build prints its stages (parsing, GB# validation, lowering, C generation, GBDK compilation, linking) and ends with the build report:
GB# Build Report
────────────────────────────────
Target Game Boy Color
ROM 32.0 KB
WRAM used 63 B / 4.0 KB
Static objects (declared) 38 B
ROM Banks
Bank 0 2.5 KB / 16.0 KB ███░░░░░░░░░░░░░░░░░
Cycle estimates
Frame budget 70,224 cycles @ 59.7 Hz
Frame loop 130 cycles ░░░░░░░░░░░░░░░░░░░░ 0%
Call stack
Deepest path 3 calls Program.Main() -> Program.Setup() -> FixedList<Enemy>.Add
Work RAM free 3.9 KB for stack and locals
Three things worth knowing on first read. WRAM used and static objects declared are different numbers on purpose: the first is what the linker actually placed, and the difference is the stack, shadow OAM and GBDK's own state. Reporting one number would be a useful-sounding lie. The cycle estimates are computed statically from the IR, so read them as ceilings for comparing changes, not as measurements; the frame budget is printed exactly because it is the only figure that is a fact. And the call stack depth is exact: GB# rejects delegates and has no function pointers, so the call graph is the complete account of what can reach what.
Run it
gbsharp run MyGame
run builds and then launches the ROM in the bundled GB# Player, unless the project's "emulator" setting or --emulator says otherwise. --emulator takes player for the bundled Player, the id of a known debugging emulator, or a path to any executable. The emulators in the catalog load the .sym file GB# writes beside the ROM, so naming one is a first-class choice for source-level debugging rather than a workaround.
A missing emulator has never failed a build and still does not: the ROM is the deliverable and running it is a convenience, so run warns, tells you where the ROM is, and exits successfully.
The loop
From here the loop is edit, then gbsharp run again. Two commands are worth knowing alongside it:
gbsharp analyze MyGame
checks the project without building a ROM (it needs no C toolchain at all, which is what makes it a fast CI lint job) and
gbsharp build MyGame --emit-c
keeps the generated C next to the ROM so you can see exactly what your C# became.
Where everything lands
The ROM is written to MyGame/build/MyGame.gb (or .gbc for a Game Boy Color target), with the linker's map and symbol files beside it and, with --emit-c, the generated C under build/c/. The full inventory of the build directory is in project layout.
When the template stops being interesting, move a sprite builds a game up from the empty template, and publishing turns the result into something people without an emulator can run.