Now, we have all of our raw data to work with, think of these as the "ingredients" to the recipe. Now we need the actual code that tells our application what to do. And to help this, we will declare several defines that are automatically converted to numbers at compile. You could simply leave these parts out and write the numbers yourself, but this sometimes makes code management easier. This also makes it easier to change a define later on.
#define NBDFRAMES 0x18 /* Nb frames for the door */
#define NBSFRAMES 0x07 /* Nb frames for the sprite */
#define WINSZX 0x80 /* Size of the picture in the window */
#define WINSZY 0x50
#define MINWINX (MAXWNDPOSX-WINSZX+1) /* Bounds of the window origin */
#define MINWINY (MAXWNDPOSY-WINSZY+1)
#define MAXWINX MAXWNDPOSX
#define MAXWINY MAXWNDPOSY
#define FADESTEP 0x10 /* Nb steps for the fading effect */
#define STARTFADE (0x06*FADESTEP) /* Initial value for the fading effect */
#define CLOSED 0x00
#define OPENING 0x01
#define OPENED 0x02
#define CLOSING 0x03
Next, declare some global variables that will be used throughout the program, including positions and state numbers.
UBYTE time; /* Global "time" value (counter) */
UBYTE doorstate; /* State of the door (OPENED, CLOSED...) */
UBYTE doorpos; /* Current position in the door animation */
UBYTE color; /* Current color for fading effect */
UBYTE sframe; /* Current frame of the sprite */
fixed bposx, bposy; /* Background position (fixed point) */
fixed bspx, bspy; /* Background speed (fixed point) */
fixed wposx, wposy; /* Window position (fixed point) */
fixed wspx, wspy; /* Window speed (fixed point) */
fixed sposx, sposy; /* Sprite position (fixed point) */
fixed sspx, sspy; /* Sprite speed (fixed point) */
Next, declare our functions for good practice.
void fade();
void scroll();
void door();
void animate_sprite();
void tile_sprite();
void place_sprite();
Next, we have the first function with a little meat to it. Fade is a function that changes the screen register to modify the palette. The gameboy allows 4 colors to be in the palette at all times, and by giving the programmer access to this palette, we can do fun little tricks like slowly darkening each color until the entire screen is black. Fade does just this by settings 4 particular numbers, F9, FE, FF, E4, each of these are a step along fading to black.
void fade()
{
if(color == 0)
return;
switch(color)
{
case STARTFADE:
case STARTFADE-4*FADESTEP:
BGP_REG = 0xF9U;
break;
case STARTFADE-FADESTEP:
case STARTFADE-3*FADESTEP:
BGP_REG = 0xFEU;
break;
case STARTFADE-2*FADESTEP:
BGP_REG = 0xFFU;
break;
case STARTFADE-5*FADESTEP:
BGP_REG = 0xE4U;
break;
}
color--;
}
So, we have a fading function, next up we want a scrolling function that makes the stars in the backdrop move. We accomplish this by modifying two registers inside of the Gameboy, SCX_REG, and SCY_REG. By modifying these numbers, the background will "scroll" or move around the background tile (check out the VRAM in BGB). There are also two registers for the window layer, WX_REG and WY_REG, which we will also modify to move around the scrolling window. Finally, the sprite is updated by the global variable we declared earlier, sposx and sposy. Note: the place_sprite() function is being declared at the end, because the OAM must be written to in order to move the globe properly.
void scroll()
{
/* Update background */
bposx.w += bspx.w;
bposy.w += bspy.w;
SCX_REG = bposx.b.h;
SCY_REG = bposy.b.h;
/* Update window */
wposx.w += wspx.w ;
wposy.w += wspy.w ;
/* X position */
if(wposx.b.h >= MAXWINX) {
wposx.b.h = MAXWINX;
/* Invert speed */
wspx.w = -(WORD)wspx.w;
} else if(wposx.b.h <= MINWINX) {
wposx.b.h = MINWINX;
/* Invert speed */
wspx.w = -(WORD)wspx.w;
}
WX_REG = wposx.b.h;
/* Y position */
if(wposy.b.h >= MAXWINY) {
wposy.b.h = MAXWINY;
/* Invert speed */
wspy.w = -(WORD)wspy.w;
} else if(wposy.b.h <= MINWINY) {
wposy.b.h = MINWINY;
/* Invert speed */
wspy.w = -(WORD)wspy.w;
}
WY_REG = wposy.b.h;
/* Update sprite */
sposx.w += sspx.w;
sposy.w += sspy.w;
place_sprite();
}
Next up, we have a door function, which actually draws the door frames opening and closing. Remember our doorstate is a global variable, so when it is set in main, this function will catch it, increment the door position (which is its current frame), and set the window layer tiles based on our film[] data pointer array. Same goes for when you close the door.
void door()
{
if(doorstate == OPENING) {
doorpos++;
/* Draw the door in the window */
set_win_tiles(2, 2, 12, 6, film[doorpos]);
if(doorpos == NBDFRAMES)
doorstate = OPENED;
} else if(doorstate == CLOSING) {
doorpos--;
/* Draw the door in the window */
set_win_tiles(2, 2, 12, 6, film[doorpos]);
if(doorpos == 0)
doorstate = CLOSED;
}
}
To actually give the globe a rotation, we have a nice little function that looks at the time keeper, recorded further down. Whenever this number is &7 (which is basically a super fast mod
, the frame is incremented, and the sprite is updated with tile_sprite()
void animate_sprite()
{
if((time&0x07) == 0) {
sframe++;
if(sframe == NBSFRAMES)
sframe = 0;
tile_sprite();
}
}
And to complement the animate sprite declaration, we have our tile_sprite function, which sets the sprite tile. set_sprite_tile takes the ID of the sprite you want to animate, and the data pointer to the tile data you're animating from.
void tile_sprite()
{
UBYTE s;
s = sframe<<1;
set_sprite_tile(0, earth_tiles[s]);
set_sprite_tile(1, earth_tiles[s+1]);
}
Place sprite calls a GBDK function, which essentially writes a quick OAM vram write to give the sprite its proper X/Y value.
void place_sprite()
{
move_sprite(0, sposx.b.h, sposy.b.h);
move_sprite(1, sposx.b.h+8, sposy.b.h);
}
And finally, the entry point to the application, main. We start off main by setting up our screen properly, disabling all interrupts and turning off the display. This is done so an interrupt such as a VBlank (vertical blank) is not triggered, or something such as a button being pressed by the user.
void main()
{
UBYTE i, j;
disable_interrupts();
DISPLAY_OFF;
LCDC_REG = 0x67;
/*
* LCD = Off
* WindowBank = 0x9C00
* Window = On
* BG Chr = 0x8800
* BG Bank = 0x9800
* OBJ = 8x16
* OBJ = On
* BG = On
*/
Take note how LCDC_REG was written. 0x67 might just look like an arbitrary number, but because we're dealing with such low level systems, you have to think in binary here. Each binary number in LCDC_REG deals with a part of the Gameboy. So in this case:
0x67 = 01100111 = [LCD Off][WindowBank at 0x9c00][Window Layer ON][BG Chr at 0x8800][BG Bank at 0x9800][OBJ size is 8x16][Sprites are ON][BG is ON]
Now you might be wondering, why do we even set 0 or 1 for a bank such as 0x9800. This is because we can share banks between the window and background, or we can do quick bank swaps by setting the various LCD registers to 0 or 1.
Next, we want to setup a few more vital bits of data, including the palette.
doorstate = CLOSED;
/* Set palettes */
BGP_REG = OBP0_REG = OBP1_REG = 0xE4U;
Notice the palettes are all set to E4, this is simply because in binary, this looks like 11100100. Or divide that up, [11][10][01][00] [4th color][3rd color][2nd color][1st color] which is our entire palette. Notice in that function we had way up there, Fade(), we were modifying this. This simply messed with each color.
Next, we want to write some background data, GBDK supplies us with some nice functions to do this.
/* Initialize the background */
set_bkg_data(0xFC, 0x04, std_data);
set_bkg_data(0x00, 0x2D, bkg_data);
/*
* Draw the background
*
* Width = 0x100 = 0x20 * 8
* Height = 0x100 = 0x20 * 8
*/
for(i = 0; i < 32; i+=8)
for(j = 0; j < 32; j+=8)
set_bkg_tiles(i, j, 8, 8, bkg_tiles);
bposx.w = 0;
SCX_REG = 0;
bposy.w = 0;
SCY_REG = 0;
bspx.w = 0xFF00UL;
bspy.w = 0x0080UL;
Set the windows data the same way.
/* Initialize the window */
set_win_data(0x80, 0x21, frame_data);
/*
* Draw the frame in the window
*
* Width = 0x80 = 0x10 * 8
* Height = 0x50 = 0x0A * 8
*/
set_win_tiles(0, 0, 16, 10, frame_tiles);
/*
* Draw the door in the window
*
* Width = 0x60 = 0x20 * 12
* Height = 0x30 = 0x20 * 6
*/
set_win_tiles(2, 2, 12, 6, door1_tiles);
wposx.b.h = MAXWNDPOSX;
wposx.b.l = 0;
WX_REG = MAXWNDPOSX;
wposy.b.h = MAXWNDPOSY;
wposy.b.l = 0;
WY_REG = MAXWNDPOSY;
wspx.w = 0xFF80UL;
wspy.w = 0xFFC0UL;
Initialize the sprite, call our functions to tile and place the sprite.
/* Initialize the sprite */
set_sprite_data(0x00, 0x1C, earth_data);
set_sprite_prop(0, 0x00);
set_sprite_prop(1, 0x00);
sframe = 0;
sposx.w = 0x1000U;
sposy.w = 0x1000U;
sspx.w = 0x0040UL;
sspy.w = 0x0040UL;
tile_sprite();
place_sprite();
And we're back and ready to start rolling. We want to turn the display back on (register bit flip), enable our interrupts so everythign is firing again, and jump on into our infinite while loop.
DISPLAY_ON;
enable_interrupts();
while(1) {
Now, the contents of this while loop should look fairly familiar. We have a fade(), door(), scroll(), and animate_sprite() right off the bat. We also see a wait_vbl_done() which is essentially a freebie timer we can use from the hardware of the Gameboy. This will only trigger when the display is ready to update, and by delaying 4 times, we slow down the entire program. You could very easily get rid of the for portion of this, and the program would be 4 times faster
/* Skip four VBLs (slow down animation) */
for(i = 0; i < 4; i++)
wait_vbl_done();
time++;
fade();
door();
scroll();
animate_sprite();
Next, we notice a joypad() call, which will set the binary bits inside of i based on joystick input. We can simply AND this with various defines such as joystick A, joystick B, etc. to get the desired results. In this example, we have many options including moving the window layer, settings the color to start a fade, or changing the door state.
i = joypad();
if(i & J_B) {
if(i & J_UP)
bspy.w -= 0x0010UL;
if(i & J_DOWN)
bspy.w += 0x0010UL;
if(i & J_LEFT)
bspx.w -= 0x0010UL;
if(i & J_RIGHT)
bspx.w += 0x0010UL;
} else if(i & J_A) {
if(i & J_UP)
wspy.w -= 0x0010UL;
if(i & J_DOWN)
wspy.w += 0x0010UL;
if(i & J_LEFT)
wspx.w -= 0x0010UL;
if(i & J_RIGHT)
wspx.w += 0x0010UL;
} else {
if(i & J_SELECT)
color = STARTFADE;
if(i & J_START)
if(doorstate == CLOSED) {
doorstate = OPENING;
doorpos = 0;
} else if(doorstate == OPENED) {
doorstate = CLOSING;
doorpos = NBDFRAMES;
}
if(i & J_UP)
sspy.w -= 0x0010UL;
if(i & J_DOWN)
sspy.w += 0x0010UL;
if(i & J_LEFT)
sspx.w -= 0x0010UL;
if(i & J_RIGHT)
sspx.w += 0x0010UL;
}
}
}
And that about does it for galaxy.c. It is really a simple program once you get the hang of it, and you can see it introduces all layers of drawing we have available on the gameboy. I would HIGHLY suggest compiling this yourself, popping it into BGB, and taking a look at the VRAM viewer. By experimenting with this, and trying to come up with other ways to mess with the sprites, you should have a very good understanding of everything that goes into making a sprite and manipulating it, along with manipulating the background.
Let me know if you need anything clarified or corrected. I'm hoping to maybe do a tools tutorial, then break away and get into some C vs. ASM comparisons of why we need to write the rest of our programs in ASM.