Programming the Game Boy : Pong tutorial

Caution

What you must absolutely now before beginning

  • Never access video ram if the screen is ON, or do it in vblank or hblank methods.

  • Game Boy CPU have a bug with asm instruction LDI (HL) in OAM address range ($FE00 to $FE9F).

Download the final version on this link pong.gb

This tutorial covers the installation of the IDE, the creation of the project structure.
The creation of the game explains how to use an image and extract tiles and map files.
A quick tour will be performed for the various notions about programming the Game Boy.

Set proper directives for the game boy header

directive TARGET = "DMG";
directive NAME = "PONG";
directive CARTRIDGETYPE = 0;
directive RAMSIZE = 0;
directive ROMBANKS = 0;
directive LICENSEECODENEW = "00";
directive EMPTYFILL = $FF;
define GAMEPAD_UP = $04;
define GAMEPAD_DOWN = $08;

The keyword directive describes the created rom header.
In order, "DMG" indicates that the game is intended for a GameBoy (gray cartridge with 4 shades of "gray").
Name describes the name as it said.
CARTRIDGETYPE 0 indicates the type of the cartridge : rom only. For more information see this page.
RAMSIZE = 0 : no external memory used.
ROMBANKS = 0 : the size of the rom is 32k
LICENSEECODENEW = "00": we do not have an official Nintendo license.
EMPTYFILL = $FF fills the rom with the given byte.

The define keyword is used to define constants.
We define two constants for this game GAMEPAD_UP = $04 and GAMEPAD_DOWN = $08

Declare variables and constants

// Global variables
racketY = $20;
ballX = $80;
ballY = $80;
speedX = 2;
speedY = 2;

Five variables are defined:
racketY contains the vertical position of the racket
ballX and ballY indicate the position of the ball on the screen
speedX and speedY the horizontal and vertical speed of the ball

Initialize screen and configure the Game Boy™

/**
 * Initialization part
 */
// Switch off the screen
screenOff();
// Switch off sound
set(0,$FF26);
// Load background tiles
loadTiles(end_background-begin_background,begin_background,$8000);
// Load racket and ball tiles
loadTiles(end_ball-begin_ball,begin_ball,$8F00);
// Load background map
loadMapDisplay(begin_map,$9800);
// Clear OAM
clearOAM();
// Set background position
// Scroll Y
// Scroll X
set(0,$FF42);
set(0,$FF43);
// Set OAM attributes for the ball
// OAM ball Y, X, tile, attribut
setOAM(ballY,ballX,$F0,$00,$FE10);
// Set OAM attributes for the racket
// Initial Y position
currentRacketY = racketY;
for (index = 0 ; index <= 12 ; index += 4) {
  // OAM raquette Y, X, tile, attribut
  setOAM(currentRacketY,$10,$F1,$00,$FE00,index);
  currentRacketY += 8;
}

// Palette BG
set(%11100100,$FF47);
// Palette sprite 0
set(%11100100,$FF48);
// Palette sprite 1 (sert pas)
set(%11100100,$FF49);
// Allume l'écran, BG on, tiles à $8000
set(%10010011,$FF40);
// Interruptions VBlank activées
screenOn();
// Interruptions VBlank activées (double activation à la con)
set(%00000001,$FFFF);

// Enable interrupt
asm{
  EI
}

Load tiles and map for background

Let’s go back to the previous source code.
The files used for the background image and the ball are included at the end of the rom.
The tiles files contain the graphics (8x8px character).
The map files contains the number of each character to constitute the image.
You can import these data with the .INCBIN directive

asm{
  begin_tiles:
  begin_background:
  .INCBIN "../../src/bin/wall.tiles.bin"
  end_background:
  begin_ball:
  .INCBIN "../../src/bin/ball.tiles.bin"
  end_ball:
  end_tiles:

  begin_map:
  .INCBIN "../../src/bin/wall.map.bin"
  end_map:
}

Always turn off the screen when you access the video memory!
The tiles are loaded into character video memory at $8000 address.
The map file of the background image is loaded into first Background Map Memory $9800.
The game boy contains two address ranges $8000 and $8800 for the characters. Each can contain 256 characters.
You can use one background layer. There are two address ranges $9800 and $9C00 to define the tile map. Bit 3 of register $FF40 allows to modify it.

// Switch off the screen
screenOff();

// Load background tiles
loadTiles(end_background-begin_background,begin_background,$8000);
// Load racket and ball tiles
loadTiles(end_ball-begin_ball,begin_ball,$8F00);
// Load background map
loadMapDisplay(begin_map,$9800);

// Switch on the screen
screenOff();

Use OAM and vertical blank procedure

In the vblank procedure you can access the video memory. Your code should be as fast as possible.
In general, data copying operations are performed. All calculations are done in the main loop (loop method).
In our method, we change the Y position of each character of the racket : $FE00, $FE04, $FE08, $FE0C.
The Y position of the ball is indicated in $FE10 and X in $FE11

/**
 * Vertical Blank procedure
 */
def VBlank() {

  // Update racket Y position
  currentRacketY = racketY;
  set(currentRacketY,$FE00);
  currentRacketY += 8;
  set(currentRacketY,$FE04);
  currentRacketY += 8;
  set(currentRacketY,$FE08);
  currentRacketY += 8;
  set(currentRacketY,$FE0C);

  // Update ball X and Y position
  set(ballY,$FE10);
  set(ballX,$FE11);
}

Create game loop and computation for ball stuff

The loop method is an infinite loop. We do all the calculations of the game.
At the end of the loop, asm HALT is called to stop the processor and save the batteries.
The processor is awaked by the next interruption for vblank.

/**
 * The Game loop
 */
loop = 1;
while (loop == 1) {
  // game loop
  game();
  // halt
  halt();
}

In the main loop, we follow these steps:
- check the state of the joystick input, if up key is pressed, subtracts 2 to Y racket’s position
- check the state of the joystick input, if down key is pressed, adds 2 to Y racket’s position
- add speedX to the X position of the ball
- add speedY to the Y position of the ball
- check any collision with the wall
- check collision with racket when ball comes from the right.

/**
 * Game procedure
 */
def game() {

  // Get value of gamepad's direction
  joystick = gamepadDirections();

  // Bottom
  joystickValue = joystick & GAMEPAD_UP;
  if (joystickValue == GAMEPAD_UP) {
    // 144+16-32 - Bottom screen border
    if (racketY < 128) {
      racketY += 2;
    }
  }

  // Top
  joystickValue = joystick & GAMEPAD_DOWN;
  if (joystickValue == GAMEPAD_DOWN) {
    // 16 - Top screen border
    if (racketY > 16) {
      racketY -= 2;
    }
  }

  // Add speed value to the current ball x
  ballX += speedX;
  // ballX >= 160: No collision right wall
  if (ballX >= 160) {
    // speedX = -2
    speedX = $FE;
    ballX = 160;
  }
  // ballX < 2: No collision left wall
  if (ballX < 2) {
    // speedX = 2
    speedX = 2;
    ballX = 8;
  }

  // Add speed value to the current ball y
  ballY += speedY;
  // ballY >= 152  (144+8)  No collision bottom wall
  if (ballY >= 152) {
    // speedY = -2
    speedY = $FE;
    //144+8
    ballY = 152;
  }
  // ballY < 16 No collision top wall
  if (ballY < 16) {
    // speedY = 2
    speedY = 2;
    ballY = 16;
  }


  // Collision detection with racket
  // 8+16 : ballX > 8+16: no collision
  // 8+10 : ballX < 8+10: no collision
  if (ballX < 24 && ballX > 18) {
    // Speed > 0 : no racket collision
    if (speedX != 2) {
      paddleY = ballY + 8;
      // paddleY > ballY+8: no collision with the top of the racket
      if (racketY < paddleY) {
        // paddleY+32 < ballY: no collision with the bottom of the racket
        paddleY = racketY + 32;
        if (ballY < paddleY) {
          speedX = 2;
        }
      }
    }
  }
}