Introduction
agb is a powerful and easy-to-use library for writing games for the Game Boy Advance (GBA) in rust. It provides an abstracted interface to the hardware, allowing you to take full advantage of its capabilities without needing to know the low-level details of its implementation.
A little bit about agb
agb
is a library for making games on the Game Boy Advance using the Rust
programming language. The library's main focus is to provide an abstraction
that allows you to develop games which take advantage of the GBA's capabilities
without needing to have extensive knowledge of its low-level implementation.
agb provides the following features:
- Simple build process with minimal dependencies
- Built in importing of sprites, backgrounds, music and sound effects
- High performance audio mixer
- Easy to use sprite and tiled background usage
- A global allocator allowing for use of both
core
andalloc
Why rust?
Rust is an excellent choice of language for developing games on low-level embedded hardware like the GBA. Its strong type system, memory safety, and performance optimizations make it well-suited for building reliable and efficient code in this context.
Agb leverages rust's unique features by using the type system to model the GBA's hardware. This approach helps prevent common programming errors and allows developers to quickly build games that function correctly on the GBA platform.
In addition to safety and correctness, rust's performance optimizations are crucial for developing games on the GBA's slow processor. With a limited amount of time per frame, every optimization counts, and rust's speed and efficiency help ensure that games built with agb run smoothly on the GBA hardware.
What is in this book?
This book serves as an introduction to agb, showcasing its capabilities and providing guidance on how to use it to build your own GBA games. It assumes that you have some experience with rust and game development, and provides detailed explanations of the unique challenges of writing games for the GBA.
Who is this book for?
This book is ideal for anyone interested in writing games for the GBA using rust. If you're new to either rust or game development, you may want to start with some introductory resources before diving into this book. This book assumes a basic understanding of rust syntax and semantics, as well as game development concepts.
Helpful links
- agb's GitHub is the primary development hub for the library.
- agb's Discussion Page is a helpful forum where you can ask for help on using agb or share your projects with the community.
- agb's crates.io page the latest version of the library on crates.io.
- agb's documentation is a useful reference for the library's API and features.
- Awesome Game Boy Advance development is a comprehensive resource for GBA development, with links to popular libraries, emulators, and the friendly gbadev Discord server.
- Example games built using agb can be found in the
examples.zip
file attached to the latest release. Additionally, you can also check out The Hat Chooses the Wizard, a game written using agb as part of the GMTK 2021 game jam.
In addition to these resources, this book provides step-by-step instructions for getting started with agb.
The Game Boy Advance hardware
The Game Boy Advance is a handheld gaming console released by Nintendo in March 2001 in Japan and in North America in June of the same year. It features a 2.9 inch screen with a 240x160 pixel resolution and is powered by a 32-bit 16.8MHz ARM CPU. The console was developed as a successor to the Game Boy Color and was internally codenamed the 'Advanced Game Boy' (agb), which is where this crate gets its name.
What makes the GBA unique?
The Game Boy Advance is a (fairly) unique console among retro handheld consoles. It was developed at a time when processors were not powerful enough to push an entire screen of pixels to the screen every frame. As a result, it features a special Pixel Processing Unit (PPU) that is similar to a modern-day graphics card, but is optimized for gaming. The console has a concept of "hardware sprites" and "hardware backgrounds," which we will explain in more detail in the next section. These hardware 2D capabilities give the GBA its unique characteristics.
Despite being a retro console, the GBA is still compatible with modern tools and programming languages thanks to the ARM CPU it contains. The CPU is modern enough to be supported by LLVM and Rust, which provide a reasonably trouble-free experience. This allows developers to take advantage of modern tooling while experiencing what it was like to program for retro consoles at the time.
Capabilities of the hardware
The GBA is fundamentally a 2D system, and a lot of the hardware accelerated graphics is designed to support this. The relevant features for this book are:
- 256 sprites which can be from 8x8 to 64x64 pixels in size
- 4 background layers which are enabled / disabled depending on the graphics mode
- Background tiles, 8x8 pixel tiles are used in the background layers if they are in tile mode.
- 8-bit sound. You have the ability to send 8-bit raw audio data to the speakers, optionally stereo.
You can read more about the specifics of the GBA on gbatek. To simplify the development process, agb abstracts some of the GBA's hardware away from the developer, which reduces the number of things to remember and lessens the chance of something going wrong. If you wish to experiment with the hardware directly, the best place to look is tonc.
Running an example
In this section, we will get to the point where you can build and run the agb template repository. This will prove that your development environment is ready for the future tutorials and later building.
You can run the game using real hardware and a flash card. However, at this stage, it is much easier to play on an emulator. agb is guaranteed to work well using mgba, but other emulators will also work.
Note that some emulators will require a special 'fixed' gba ROM file. See the later steps in this section for how to do this.
Environment setup
Environment setup will depend on the platform you are using. You need to install the rust nightly edition along with (optionally) some additional tools.
See the sub-pages here for platform specific setup guides.
Linux setup
This guide has been tested on Ubuntu, Arch Linux and Raspberry Pi OS running on a raspberry pi 4.
1. Install a recent version of rust
To use agb, you'll need to use nightly rust since it requires a few nightly features. Firstly, ensure that you have rustup installed which you can do by following the instructions on the rust website
If you have already installed rustup, you can update it with rustup update
.
2. git
The source code for the game is hosted on github, so you will need to install git.
- On Debian and derivatives (like Ubuntu):
sudo apt install git
- On Arch Linux and derivatives:
pacman -S git
3. gbafix
In order to be able to play games made with agb on real hardware or on some emulators, you will need to install 'agb-gbafix'.
Agb's implementation can be installed very easily using cargo install agb-gbafix
.
Make sure that the Cargo bin directory is in your PATH
as we'll need to use it later.
That is all you need to get started! You can now move on to 'building the game'.
Windows setup
This guide has been tested on Windows 11 using PowerShell with elevated rights (dont use cmd).
1. Install a recent version of rust
To use agb, you'll need to use nightly rust since it requires a few nightly features. Firstly, ensure that you have rustup installed which you can do by following the instructions on the rust website
If you have installed rustup, you can update it with rustup update
.
If the rustup
-command fails, you'll most probably add the cargo/bin folder to the Path-environment variable.
2. git
The source code for the game is hosted on github, so you will need to install git.
You'd need to follow this official github git guide.
3. mGBA
We recommend using the mGBA emulator which you can download from here.
After installing, you can add the binary to your Path-environment variable and create an alias for the agb run command to use.
Creating link for mgba-qt:
New-Item -itemtype hardlink -path "C:\Program Files\mGBA\mgba-qt.exe" -value "C:\Program Files\mGBA\mGBA.exe"
4. gbafix
In order to be able to play games made with agb on real hardware or on some emulators, you will need to install 'agb-gbafix'.
Agb's implementation can be installed very easily using cargo install agb-gbafix
.
That is all you need to get started! You can now move on to 'building the game'.
Mac setup
This guide has been tested on MacOS 13.0.1 on an M1 chip.
1. Install a recent version of rust
To use agb, you'll need to use nightly rust since it requires a few nightly features. Firstly, ensure that you have rustup installed which you can do by following the instructions on the rust website
If you have already installed rustup, you can update it with rustup update
.
2. Get git
The source code for the game is hosted on github, so you will need git installed. Follow the instructions at git-scm.com
3. GBA Emulator - mGBA
We recommend using the mGBA emulator which you can download for Mac here.
After installing to your /Applications
folder you can add the binary to your path and create an alias for the agb run command to use.
- Add
/Applications/mGBA.app/Contents/MacOS
to/etc/paths
- Inside the
/Applications/mGBA.app/Contents/MacOS
directory (in a terminal) run:ln -s mGBA mgba-qt
4. Real hardware - gbafix
In order to be able to play games made with agb on real hardware or on some emulators, you will need to install 'agb-gbafix'.
Agb's implementation can be installed very easily using cargo install agb-gbafix
.
Make sure that the Cargo bin directory is in your PATH
as we'll need to use it later.
That is all you need to get started! You can now move on to 'building the game'.
Building and running the agb template
In this section, you will learn how to build and run the agb template. By the end of this section, you will have a working GBA game that you can run on your emulator of choice.
1. Clone the repository
The first step is to clone the agb template repository using Git. Open a terminal or command prompt and run the following command:
git clone https://github.com/agbrs/template.git
This will create a copy of the agb template repository on your local machine.
2. Build the template
Next, navigate to the template
directory in the repository and build the template using the following command:
cd template
cargo build --release
This command will compile the agb template in release mode.
The resulting binary file can be found in the target/thumbv4t-none-eabi/release
directory.
Depending on your platform, the file will have either a .elf
extension or no extension.
3. Convert the binary to a GBA file
In order to run the game on an emulator, we need to convert the binary file to a GBA file.
To do this, we'll use the tool agb-gbafix
.
Run the following command to convert the binary file to a GBA ROM:
agb-gbafix target/thumbv4t-none-eabi/release/agb_template -o agb_template.gba
or
agb-gbafix target/thumbv4t-none-eabi/release/agb_template.elf -o agb_template.gba
This command will add the correct GBA header to the template.gba file and it will be playable on real hardware or an emulator.
4. Run the game
Finally, you can run the game on your emulator of choice. Load the template.gba file in your emulator, and you should see the agb template running.
If you have mgba-qt installed on your machine, you can run the game directly from the command line using the following command:
cargo run --release
This will build and run the agb template in a single step.
That's it! You now have a working agb template that you can use as a starting point for your own GBA game.
Learn agb part I: Pong
In this section, you'll learn how to make a simple pong-style game for the Game Boy Advance using agb. By following the steps in this section below, you'll gain an understanding of:
- How to use tiled graphics modes.
- How to import graphics using
agb
. - What Game Boy Advance sprites are, how to create them, and how to display them on the screen.
- How to detect button input and use it to control game objects.
- How to add a static background to your game.
- How to make a dynamic background to display scores.
- How to add music and sound effects to your game.
With this knowledge, you'll be well equipped to start making your own games for the GBA!
Getting started
To get started, create a new repository based on the agb template and name it pong
.
Next, update the name
field in Cargo.toml
to pong
like so:
[package]
name = "pong"
version = "0.1.0"
authors = ["Your name here"]
edition = "2021"
# ...
Now, you're ready to dive and and start learning about agb
!
The Gba struct
In this section, we'll cover the importance of the Gba struct and how it gets created for you.
The importance of the Gba struct
The Gba singleton struct is a crucial part of agb game development. It is used for almost all interactions with the Game Boy Advance's hardware, such as graphics rendering, timer access and audio playback.
You should not create the Gba struct yourself. Instead, it is passed to your main function as an owned reference. This allows rust's borrow checker to ensure that access to the Game Boy Advance hardware is done in a safe and sensible manner, preventing two bits of your code from modifying data in the wrong way.
How all agb games start
To use the Gba struct in your agb game, you'll need to create a function (normally called main
) which takes an owned reference to the Gba instance.
The recommended way to do this is by using the #[agb::entry]
attribute macro provided by the agb
crate.
Replace the content of the main
function with the following:
#![no_std]
#![no_main]
#[agb::entry]
fn main(mut _gba: Gba) -> ! {
loop {} // infinite loop for now
}
This creates an infinite loop and allows you to start building your game.
Running your pong game
At this point, your game won't do much except display a black screen. To run your game, use the cargo run
command as before.
What we covered
In this section, we covered the importance of the Gba struct in agb game development. By using the Gba struct as a gatekeeper for all hardware interactions, you can ensure that your code is safe and efficient. You are now ready to learn about sprites and start getting things onto the screen!
Sprites
In this section, we'll cover what sprites are in the Game Boy Advance and how to put them on the screen in our pong game. We'll briefly cover vblank, and by the end of this section, you'll have a ball bouncing around the screen!
Why do we need sprites?
The Game Boy Advance has a 240x160px screen with 15-bit RGB color support. Setting the color for each pixel manually would require updating 38,400 pixels per frame, or 2,304,000 pixels per second at 60 fps. With a 16 MHz processor, this means calculating 1 pixel every 8 clock cycles, which is pretty much impossible. he Game Boy Advance provides two ways to easily put pixels on the screen: tiles and sprites.
Tiles are 8x8 pixels in size and can be placed in a grid on the screen. You can also scroll the whole tile layer to arbitrary positions, but the tiles will remain in this 8x8 pixel grid.
Sprites are the other way to draw things on the screen, which we'll cover in this section. The Game Boy Advance supports 256 hardware sprites, with different sizes ranging from square 8x8 to more exotic sizes like 8x32 pixels. In our pong game, all the sprites will be 16x16 pixels to make things simpler.
Sprites are stored in a special area of video memory called the 'Object Attribute Memory' (OAM). OAM has space for the 'attributes' of the sprites, such as their location, whether or not they are visible, and which tile to use, but it does not store the actual pixel data. The pixel data is stored in video RAM (VRAM). This split allows multiple sprites to refer to the same tiles in VRAM, which saves space and allows for more objects on screen than would be possible by repeating them.
Since RAM is in short supply and expensive, the tile data is stored as indexed palette data. Instead of storing the full color data for each pixel in the tile, the Game Boy Advance stores a 'palette' of colors, and the tiles that make up the sprites are stored as indexes to the palette. Each sprite can use a maximum of 16 colors out of the total sprite palette of 256 colors.
There are technically two types of sprites: regular and affine sprites. For now, we will only be dealing with regular sprites.
Import the sprite
Firstly, you're going to need to import the sprites into your project.
agb
has excellent support for the aseprite sprite editor which can be bought for $20 or you can compile it yourself for free.
Aseprite files can be natively imported by agb
for use on the Game Boy Advance.
Here is the sprite sheet we will use as a png, but you should download the aseprite file and place it in gfx/sprites.aseprite
.
This contains 5 16x16px
sprites: the end cap for the paddle, the center part of the paddle, which could potentially be repeated a few times, and the ball with various squashed states.
The aseprite file defines tags for these sprites: "Paddle End," "Paddle Mid," and "Ball."
#![allow(unused)] fn main() { use agb::{ include_aseprite, display::object::{Graphics, Tag} }; // Import the sprites in to this static. This holds the sprite // and palette data in a way that is manageable by agb. static GRAPHICS: &Graphics = include_aseprite!("gfx/sprites.aseprite"); // We define some easy ways of referencing the sprites static PADDLE_END: &Tag = GRAPHICS.tags().get("Paddle End"); static PADDLE_MID: &Tag = GRAPHICS.tags().get("Paddle Mid"); static BALL: &Tag = GRAPHICS.tags().get("Ball"); }
This uses the include_aseprite
macro to include the sprites in the given aseprite file.
Now, let's put this on screen by firstly creating the object manager and then creating an object, this will also involve the creation of the main entry function using the entry
macro.
The signature of this function takes the Gba
struct and has the never return type, this means Rust will enforce that this function never returns, for now we will achieve this using a busy loop.
Using the Gba
struct we get the ObjectController
struct which manages loading and unloading sprites and objects.
#[agb::entry] fn main(mut gba: agb::Gba) -> ! { // Get the object manager let object = gba.display.object.get_managed(); // Create an object with the ball sprite let mut ball = object.object_sprite(BALL.sprite(0)); // Place this at some point on the screen, (50, 50) for example ball.set_x(50).set_y(50).show(); // Now commit the object controller so this change is reflected on the screen. // This isn't how we will do this in the final version of the code, but will do // for this example. object.commit(); loop {} }
If you run this you should now see the ball for this pong game somewhere in the top left of the screen.
Making the sprite move
The GBA renders to the screen one pixel at a time a line at a time from left to right.
After it has finished rendering to each pixel of the screen, it briefly pauses rendering before starting again.
This period of no drawing is called vblank
, which stands for the 'vertical blanking interval'.
There is also a 'horizontal blanking interval', but that is outside of the scope of this book.
You should .commit()
your sprites only during this vblank
phase, because otherwise you may end up moving a sprite during the rendering which could cause tearing of your objects1.
agb
provides a convenience function for waiting until the right moment called agb::display::busy_wait_for_vblank()
.
You shouldn't use this is a real game (we'll do it properly later on), but for now we can use this to wait for the correct time to commit
our sprites to memory.
Making the sprite move 1 pixel every frame (so 60 pixels per second) can be done as follows:
#![allow(unused)] fn main() { // replace the call to object.commit() with the following: let mut ball_x = 50; let mut ball_y = 50; let mut x_velocity = 1; let mut y_velocity = 1; loop { // This will calculate the new position and enforce the position // of the ball remains within the screen ball_x = (ball_x + x_velocity).clamp(0, agb::display::WIDTH - 16); ball_y = (ball_y + y_velocity).clamp(0, agb::display::HEIGHT - 16); // We check if the ball reaches the edge of the screen and reverse it's direction if ball_x == 0 || ball_x == agb::display::WIDTH - 16 { x_velocity = -x_velocity; } if ball_y == 0 || ball_y == agb::display::HEIGHT - 16 { y_velocity = -y_velocity; } // Set the position of the ball to match our new calculated position ball.set_x(ball_x as u16).set_y(ball_y as u16); // Wait for vblank, then commit the objects to the screen agb::display::busy_wait_for_vblank(); object.commit(); } }
What we did
In this section, we covered why sprites are important, how to create and manage them using the ObjectController
in agb
and make a ball bounce around the screen.
Timing this can give you some really cool effects allowing you to push the hardware.
However, agb
does not by default provide the timing accuracy needed to fully take advantage of this, erring on the side of making it easier to make games rather than squeezing every last drop of performance from the console.
Controls
In this section, we'll make the ball that we displayed in the last section move by pressing the D-Pad.
The GBA controls
The GBA has 10 buttons we can read the state of, and this is the only way a player can directly control the game. They are the 4 directions on the D-Pad, A, B, Start, Select, and the L and R triggers.
Reading the button state
There are two ways to capture the button state in agb, interrupts and polling. In most games, you will want to use polling, so that is what we will use now. Interrupts will be covered in a later chapter.
To add button control to our game, we will need a ButtonController. Add this near the top of your main function:
#![allow(unused)] fn main() { let mut input = agb::input::ButtonController::new(); }
The button controller is not part of the Gba
struct because it only allows for reading and not writing so does not need to be controlled by the borrow checker.
Replace the inner loop with the following:
#![allow(unused)] fn main() { let mut ball_x = 50; let mut ball_y = 50; // now we initialise the x and y velocities to 0 rather than 1 let mut x_velocity = 0; let mut y_velocity = 0; loop { ball_x = (ball_x + x_velocity).clamp(0, agb::display::WIDTH - 16); ball_y = (ball_y + y_velocity).clamp(0, agb::display::HEIGHT - 16); // x_tri and y_tri describe with -1, 0 and 1 which way the d-pad // buttons are being pressed x_velocity = input.x_tri() as i32; y_velocity = input.y_tri() as i32; ball.set_x(ball_x as u16).set_y(ball_y as u16); agb::display::busy_wait_for_vblank(); object.commit(); // We must call input.update() every frame otherwise it won't update based // on the actual button press state. input.update(); } }
Here we use the x_tri()
and y_tri()
methods.
They return instances of the Tri
enum which describes which buttons are being pressed, and are very helpful in situations like these where you want to move something in a cardinal direction based on which buttons are pressed.
Detecting individual button presses
If you want to detect if any button is pressed, you can use the is_pressed
method on ButtonController
.
For example, we can do the following:
#![allow(unused)] fn main() { use agb::input::Button; if input.is_pressed(Button::A) { // the A button is pressed } }
ButtonController
also provides the is_just_pressed
method.
This will return true for 1 frame, the one where the player actually pressed the button.
From that point on, it'll return false again until the player presses it again.
What we did
We added very basic button control to our bouncing ball example. In the next step, we'll cover meta-sprites and actually add a bat to our game of pong.
Exercise
Make it so the ball moves twice as fast if you're pressing the A
button while moving it around.
Meta Sprites
In this section we'll discuss how the GBA's concept on sprites and objects doesn't need to correspond to your game's concept of objects and we will make the paddle display on screen.
What is a meta sprite?
Imagine all you had were 8x8 pixel sprites, but you wanted an enemy to be 16x16 pixels. You could use 4 sprites in a square arrangement to achieve this. Using multiple of these GBA objects to form one of your game objects is what we call a meta sprite.
Making the paddle
In the paddle sprite we gave you a "Paddle End" and a "Paddle Mid". Therefore in order to show a full paddle we will need 2 paddle ends with a paddle mid between them.
Let's just write that and we'll get to neatening it up later.
#![allow(unused)] fn main() { // outside the game loop let mut paddle_start = object.object_sprite(PADDLE_END.sprite(0)); let mut paddle_mid = object.object_sprite(PADDLE_MID.sprite(0)); let mut paddle_end = object.object_sprite(PADDLE_END.sprite(0)); paddle_start.set_x(20).set_y(20).show(); paddle_mid.set_x(20).set_y(20 + 16).show(); paddle_end.set_x(20).set_y(20 + 16 * 2).show(); }
If you add this to your program, you'll see the paddle. But wait! The bottom of the paddle is the wrong way around! Fortunately, the GBA can horizontally and vertically flip sprites.
#![allow(unused)] fn main() { paddle_end.set_vflip(true); }
Now the paddle will display correctly. It's rather awkward to use, however, having to set all these positions correctly. Therefore we should encapsulate the logic of this object.
#![allow(unused)] fn main() { // change our imports to include what we will use use agb::{ display::object::{Graphics, Object, OamManaged, Tag}, include_aseprite, }; struct Paddle<'obj> { start: Object<'obj>, mid: Object<'obj>, end: Object<'obj>, } impl<'obj> Paddle<'obj> { fn new(object: &'obj OamManaged<'_>, start_x: i32, start_y: i32) -> Self { let mut paddle_start = object.object_sprite(PADDLE_END.sprite(0)); let mut paddle_mid = object.object_sprite(PADDLE_MID.sprite(0)); let mut paddle_end = object.object_sprite(PADDLE_END.sprite(0)); paddle_start.show(); paddle_mid.show(); paddle_end.set_vflip(true).show(); let mut paddle = Self { start: paddle_start, mid: paddle_mid, end: paddle_end, }; paddle.set_position(start_x, start_y); paddle } fn set_position(&mut self, x: i32, y: i32) { // new! use of the `set_position` method. This is a helper feature using // agb's vector types. For now we can just use it to avoid adding them // separately self.start.set_position((x, y)); self.mid.set_position((x, y + 16)); self.end.set_position((x, y + 32)); } } }
Here we've made a struct to hold our paddle objects and added a convenient new
and set_position
function and methods to help us use it. Now we can easily create two paddles (one on each side of the screen).
#![allow(unused)] fn main() { // outside the loop let mut paddle_a = Paddle::new(&object, 8, 8); // the left paddle let mut paddle_b = Paddle::new(&object, 240 - 16 - 8, 8); // the right paddle }
What we did
We used multiple sprites to form one game object of a paddle. We also added convenience around the use of the paddle to make creating a paddle and setting its position easy.
In the next step we will cover adding collision with the ball and the paddles.
Exercise
The paddle on the right is facing the wrong way, it needs to be horizontally
flipped! Given that the method is called set_hflip
, can you modify the code
such that both paddles face the correct direction.