A "Sprite" is a two dimensional image that can be placed on screen, and moved independently from everything else on screen. The GBA has hardware support for up to 128 sprites. This means that we can simply tell the GBA what a sprite looks like, and where it should appear on screen, and the hardware will draw it there automatically (and quickly).
At a high level, we need to do the following things to use sprites:
There is only one palette that all sprites must share (just as there is only one palette that all backgrounds must share), and one image memory, so it is much easier to have all sprites share one image.
Additionally, sprites work in 8 by 8 tiles, just like tiled backgrounds do. For our example, we will use the following koopa image:
Here is the same image blown up 400%:
This image contains two "frames" which are intended to be used in an animation. Not all sprites are animated, some just contain one frame. Each frame of the animation is two tiles wide by four tiles high:
One important caveat to this is that the different frames of all characters have to be laid out vertically like this. The reason is that the tiles for each frame must be laid out sequentially in the image. If two frames or two characters were laid out side by side, then their tiles would interfere with each other.
We could create the GBA image file with png2gba as:
png2gba -p -t koopa.png
Once we have an image in the GBA format, we could then load the palette into the sprite palette, and the image data into the sprite image memory:
/* the address of the color palettes used for backgrounds and sprites */
volatile unsigned short* bg_palette = (volatile unsigned short*) 0x5000000;
volatile unsigned short* sprite_palette = (volatile unsigned short*) 0x5000200;
/* the memory location which stores sprite image data */
volatile unsigned short* sprite_image_memory = (volatile unsigned short*) 0x6010000;
/* setup the sprite image and palette */
void setup_sprite_image() {
/* load the palette from the image into palette memory*/
memcpy16_dma((unsigned short*) sprite_palette, (unsigned short*) koopa_palette, PALETTE_SIZE);
/* load the image into sprite image memory */
memcpy16_dma((unsigned short*) sprite_image_memory, (unsigned short*) koopa_data, (koopa_width * koopa_height) / 2);
}
We use the DMA transfer for copying the palette and image data to the right locations.
Note that the sprite_image_memory location is past the end of the character blocks used for storing tiled backgrounds, so it won't overwrite our background data.
Once we have usable image data for our sprites, we can begin to define them. The GBA has 128 sprites available and stores their attributes in an array of memory beginning at 0x7000000. Each of the 128 sprites has four 16-bit values which each pack a number of values together.
The four attributes are as follows:
Attribute 0
Field | Shape | Color Mode | Mosaic | Effect | Affine | Y |
Bits | 15 14 | 13 | 12 | 11 10 | 9 8 | 7 6 5 4 3 2 1 0 |
Attribute 1
Field | Size | V Flip | H Flip | Unused | X |
Bits | 15 14 | 13 | 12 | 11 10 9 | 8 7 6 5 4 3 2 1 0 |
Attribute 2
Field | Palette Bank | Priority | Tile Index |
Bits | 15 14 13 12 | 11 10 | 9 8 7 6 5 4 3 2 1 0 |
Attribute 3
There is an attribute 3, but it is not used by us. When doing affine transformations, the GBA stores things for the sprites here.
Some notes on the individual attributes:
X and Y
These refer to the on screen position of the sprite. The coordinates specify the upper left hand corner of the sprite.
Shape and Size
The shape and size together describe the actual size of the sprite.
Size | |||||
0 | 1 | 2 | 3 | ||
Shape | 0 | 8x8 | 16x16 | 32x32 | 64x64 |
1 | 16x8 | 32x8 | 32x16 | 64x32 | |
2 | 8x16 | 8x32 | 16x32 | 32x64 |
So for our 16x32 Koopa sprite, we would want size and shape to both be 2.
Color Mode
0 for 16-color mode, 1 for 256-color mode. We will use 256-color mode.
Mosaic
Enables the "Mosaic effect".
Effect
Used to turn on alpha blending or masking.
Affine
Used for specifying affine transformations.
V Flip and H Flip
Specify whether the sprite should be flipped in the vertical or horizontal directions. Notice that our Koopa is only facing right. The GBA can flip it horizontally so that we get the left-facing Koopa for free.
Palette Bank
Only used for 16-bit color sprites.
Priority
As with backgrounds, the priority is used for layering sprites. Low priorities appear over higher ones. Sprites appear over backgrounds of the same priority.
Tile Index
The Tile index is the index of the first tile in the sprite. This is what causes the sprite to use a particular image. In our Koopa, we can use 0 to cause the first image to appear for the Koopa. To switch to the other frame, we can use 16. This is the tile number multiplied by two. The tile indices allow you to start half way into a tile, so to start on a whole tile, we multiply by two.
To set these up in code, we first make a struct for storing the four attributes:
/* a sprite is a moveable image on the screen */
struct Sprite {
unsigned short attribute0;
unsigned short attribute1;
unsigned short attribute2;
unsigned short attribute3;
};
Then we make an array of 128 of them to store all of the sprites we can use:
/* array of all the sprites available on the GBA */
struct Sprite sprites[NUM_SPRITES];
int next_sprite_index = 0;
We can make an enumeration for all of the different sprite sizes available to us, which will be easier than looking up the shape/size values we need:
/* the different sizes of sprites which are possible */
enum SpriteSize {
SIZE_8_8,
SIZE_16_16,
SIZE_32_32,
SIZE_64_64,
SIZE_16_8,
SIZE_32_8,
SIZE_32_16,
SIZE_64_32,
SIZE_8_16,
SIZE_8_32,
SIZE_16_32,
SIZE_32_64
};
We can then write a function that creates a sprite and fills in the properties we might want to change, and defaults for the ones we won't. This function sets the attribute bits in the next available sprite and returns a pointer to it.
/* function to initialize a sprite with its properties, and return a pointer */
struct Sprite* sprite_init(int x, int y, enum SpriteSize size,
int horizontal_flip, int vertical_flip, int tile_index, int priority) {
/* grab the next index */
int index = next_sprite_index++;
/* setup the bits used for each shape/size possible */
int size_bits, shape_bits;
switch (size) {
case SIZE_8_8: size_bits = 0; shape_bits = 0; break;
case SIZE_16_16: size_bits = 1; shape_bits = 0; break;
case SIZE_32_32: size_bits = 2; shape_bits = 0; break;
case SIZE_64_64: size_bits = 3; shape_bits = 0; break;
case SIZE_16_8: size_bits = 0; shape_bits = 1; break;
case SIZE_32_8: size_bits = 1; shape_bits = 1; break;
case SIZE_32_16: size_bits = 2; shape_bits = 1; break;
case SIZE_64_32: size_bits = 3; shape_bits = 1; break;
case SIZE_8_16: size_bits = 0; shape_bits = 2; break;
case SIZE_8_32: size_bits = 1; shape_bits = 2; break;
case SIZE_16_32: size_bits = 2; shape_bits = 2; break;
case SIZE_32_64: size_bits = 3; shape_bits = 2; break;
}
int h = horizontal_flip ? 1 : 0;
int v = vertical_flip ? 1 : 0;
/* set up the first attribute */
sprites[index].attribute0 = y | /* y coordinate */
(0 << 8) | /* rendering mode */
(0 << 10) | /* gfx mode */
(0 << 12) | /* mosaic */
(1 << 13) | /* color mode, 0:16, 1:256 */
(shape_bits << 14); /* shape */
/* set up the second attribute */
sprites[index].attribute1 = x | /* x coordinate */
(0 << 9) | /* affine flag */
(h << 12) | /* horizontal flip flag */
(v << 13) | /* vertical flip flag */
(size_bits << 14); /* size */
/* setup the second attribute */
sprites[index].attribute2 = tile_index | /* tile index */
(priority << 10) | /* priority */
(0 << 12); /* palette bank (only 16 color)*/
/* return pointer to this sprite */
return &sprites[index];
}
The code above does not in fact store the sprites in the sprite attribute memory. The reason for this is that we should only update the sprites in memory during the V blank period. In order to ensure this is the case, we store the sprite attributes in a regular variable, then copy them all into sprite attributes memory during V blank.
We can then change the attributes in memory whenever we want. For instance the following functions change different attributes:
/* set a sprite position */
void sprite_position(struct Sprite* sprite, int x, int y) {
/* clear out the y coordinate */
sprite->attribute0 &= 0xff00;
/* set the new y coordinate */
sprite->attribute0 |= (y & 0xff);
/* clear out the x coordinate */
sprite->attribute1 &= 0xfe00;
/* set the new x coordinate */
sprite->attribute1 |= (x & 0x1ff);
}
/* move a sprite in a direction */
void sprite_move(struct Sprite* sprite, int dx, int dy) {
/* get the current y coordinate */
int y = sprite->attribute0 & 0xff;
/* get the current x coordinate */
int x = sprite->attribute1 & 0x1ff;
/* move to the new location */
sprite_position(sprite, x + dx, y + dy);
}
/* change the vertical flip flag */
void sprite_set_vertical_flip(struct Sprite* sprite, int vertical_flip) {
if (vertical_flip) {
/* set the bit */
sprite->attribute1 |= 0x2000;
} else {
/* clear the bit */
sprite->attribute1 &= 0xdfff;
}
}
/* change the vertical flip flag */
void sprite_set_horizontal_flip(struct Sprite* sprite, int horizontal_flip) {
if (horizontal_flip) {
/* set the bit */
sprite->attribute1 |= 0x1000;
} else {
/* clear the bit */
sprite->attribute1 &= 0xefff;
}
}
/* change the tile offset of a sprite */
void sprite_set_offset(struct Sprite* sprite, int offset) {
/* clear the old offset */
sprite->attribute2 &= 0xfc00;
/* apply the new one */
sprite->attribute2 |= (offset & 0x03ff);
}
Then we can have them all copied into object attribute memory:
/* update all of the spries on the screen */
void sprite_update_all() {
/* copy them all over */
memcpy16_dma((unsigned short*) sprite_attribute_memory, (unsigned short*) sprites, NUM_SPRITES * 4);
}
The code above is all we need to interact with the GBA hardware sprites. We can create them, change their properties and then write them into sprite memory so they will be drawn on screen.
However, we need a little bit more logic to easily animate the sprite. We need to keep track of Koopa's position, which frame of the animation he is on, as well as how long until the animation should flip to the other frame. We can create a struct to hold all of this information:
/* a struct for the koopa's logic and behavior */
struct Koopa {
/* the actual sprite attribute info */
struct Sprite* sprite;
/* the x and y position */
int x, y;
/* which frame of the animation he is on */
int frame;
/* the number of frames to wait before flipping */
int animation_delay;
/* the animation counter counts how many frames until we flip */
int counter;
/* whether the koopa is moving right now or not */
int move;
/* the number of pixels away from the edge of the screen the koopa stays */
int border;
};
We can write a function to create a Koopa with default values:
/* initialize the koopa */
void koopa_init(struct Koopa* koopa) {
koopa->x = 100;
koopa->y = 113;
koopa->border = 40;
koopa->frame = 0;
koopa->move = 0;
koopa->counter = 0;
koopa->animation_delay = 8;
koopa->sprite = sprite_init(koopa->x, koopa->y, SIZE_16_32, 0, 0, koopa->frame, 0);
}
When the Koopa moves left, it will check if it is near the left side of the screen. If so, it will not actually move left, we will just move the tile map to the right. This will make it look like the Koopa is walking left while keeping him on screen:
/* move the koopa left or right returns if it is at edge of the screen */
int koopa_left(struct Koopa* koopa) {
/* face left */
sprite_set_horizontal_flip(koopa->sprite, 1);
koopa->move = 1;
/* if we are at the left end, just scroll the screen */
if (koopa->x < koopa->border) {
return 1;
} else {
/* else move left */
koopa->x--;
return 0;
}
}
Same thing for moving to the right:
int koopa_right(struct Koopa* koopa) {
/* face right */
sprite_set_horizontal_flip(koopa->sprite, 0);
koopa->move = 1;
/* if we are at the right end, just scroll the screen */
if (koopa->x > (SCREEN_WIDTH - 16 - koopa->border)) {
return 1;
} else {
/* else move right */
koopa->x++;
return 0;
}
}
When the Koopa is not moving left or right, we set its "move" variable to 0 so that he does not keep animating while standing still. We also set his counter to 7 so he takes a step as soon as he begins walking again:
void koopa_stop(struct Koopa* koopa) {
koopa->move = 0;
koopa->frame = 0;
koopa->counter = 7;
sprite_set_offset(koopa->sprite, koopa->frame);
}
Lastly, we write a function to update the Koopa's animation and position every frame. This would be called inside of the main loop:
/* update the koopa */
void koopa_update(struct Koopa* koopa) {
if (koopa->move) {
koopa->counter++;
if (koopa->counter >= koopa->animation_delay) {
koopa->frame = koopa->frame + 16;
if (koopa->frame > 16) {
koopa->frame = 0;
}
sprite_set_offset(koopa->sprite, koopa->frame);
koopa->counter = 0;
}
}
sprite_position(koopa->sprite, koopa->x, koopa->y);
}
There are only a few extra things we need for a complete example. First we must turn on sprites in the display control register, and set the mapping mode to 1D:
/* flags to set sprite handling in display control register */
#define SPRITE_MAP_2D 0x0
#define SPRITE_MAP_1D 0x40
#define SPRITE_ENABLE 0x1000
/* in main */
*display_control = MODE0 | BG0_ENABLE | SPRITE_ENABLE | SPRITE_MAP_1D;
We also have to somehow initialize all 128 sprites. If we don't do something with them, all of the sprites may have random attributes and draw tiles any place. To avoid this, we place them all off screen to begin with:
/* setup all sprites */
void sprite_clear() {
/* clear the index counter */
next_sprite_index = 0;
/* move all sprites offscreen to hide them */
for(int i = 0; i < NUM_SPRITES; i++) {
sprites[i].attribute0 = SCREEN_HEIGHT;
sprites[i].attribute1 = SCREEN_WIDTH;
}
}
We will then create the Koopa and set the background scroll to 0:
/* create the koopa */
struct Koopa koopa;
koopa_init(&koopa);
/* set initial scroll to 0 */
int xscroll = 0;
Our main loop will then update the koopa, check for left and right arrow keys, and adjust the background as needed:
/* loop forever */
while (1) {
/* update the koopa */
koopa_update(&koopa);
/* now the arrow keys move the koopa */
if (button_pressed(BUTTON_RIGHT)) {
if (koopa_right(&koopa)) {
xscroll++;
}
} else if (button_pressed(BUTTON_LEFT)) {
if (koopa_left(&koopa)) {
xscroll--;
}
} else {
koopa_stop(&koopa);
}
/* wait for vblank before scrolling and moving sprites */
wait_vblank();
*bg0_x_scroll = xscroll;
sprite_update_all();
/* delay some */
delay(300);
}
The complete code listing can be seen in the files here.
If we want to add jumping to our animated koopa, we could do so using simple physics calculations. We can add a variable yvel to represent the koopa's y velocity. When the koopa jumps, we set yvel to a negative number, so that its y position will decrease, and the koopa will go up.
In order to have the koopa fall again, we can decrease the yvel value each frame as a result of gravity. This way the koopa will begin to slow down, and eventually fall back down the screen. When the koopa has hit the grown we will stop it from falling:
To jump, we can use the following function:
/* start the koopa jumping, unless already falling */
void koopa_jump(struct Koopa* koopa) {
if (!koopa->falling) {
koopa->yvel = -1500;
koopa->falling = 1;
}
}
Adding the following into the koopa's update logic will make it actually fall:
/* update y position and speed if falling */
if (koopa->falling) {
koopa->y += koopa->yvel;
koopa->yvel += koopa->gravity;
}
There is one issue with the above approach which is that it doesn't work so well with integer numbers. The reason is that the minimum value of gravity would be one. This means the yvel will increase at least one pixel per frame, per frame. This is much too fast most of the time.
We could use floating point numbers instead, and use a fractional value, but the processor of the GBA does not have hardware support for working with floating point numbers. Adding two floating point numbers is done with a software routine making it very slow.
An alternative is to use fixed point numbers instead. Fixed point numbers are still regular integers, but we use them to store the number of fractions. For instance, we could store monetary values with integers representing the number of cents which are $\frac{1}{100}$ of a dollar, instead of using a float or double to store the number of dollars.
We could use the same approach for storing the koopa's speed, and gravity. We will change the units from a single pixel to $\frac{1}{256}$ of a pixel. Using a power of 2 means that we can back and forth to whole pixels with bit shifts. For instance, to convert to actual coordinates:
/* convert speed from pixels to 1/256 of pixels */
int pix_per_second = speed >> 8;
This gives us much more granularity with regards to gravity and velocity values without sacrificing performance.
The last thing we will discuss is how to know when to stop the koopa from falling. A simple approach is to stop it when its position reaches a hard-coded "floor level". However, for many games, we would want the ground to depend on the tiles in the background.
To do this, we need some way of figuring out which tile a particular screen coordinate is in. The following function does just this:
/* finds which tile a screen coordinate maps to, taking scroll into acco unt */
unsigned short tile_lookup(int x, int y, int xscroll, int yscroll,
const unsigned short* tilemap, int tilemap_w, int tilemap_h) {
/* adjust for the scroll */
x += xscroll;
y += yscroll;
/* convert from screen coordinates to tile coordinates */
x >>= 3;
y >>= 3;
/* account for wraparound */
while (x >= tilemap_w) {
x -= tilemap_w;
}
while (y >= tilemap_h) {
y -= tilemap_h;
}
while (x < 0) {
x += tilemap_w;
}
while (y < 0) {
y += tilemap_h;
}
/* the larger screen maps (bigger than 32x32) are made of multiple s titched
* together - the offset is used for finding which screen block we a re in
* for these cases */
int offset = 0;
/* if the width is 64, add 0x400 offset to get to tile maps on right */
if (tilemap_w == 64 && x >= 32) {
x -= 32;
offset += 0x400;
}
/* if height is 64 and were down there */
if (tilemap_h == 64 && y >= 32) {
y -= 32;
/* if width is also 64 add 0x800, else just 0x400 */
if (tilemap_w == 64) {
offset += 0x800;
} else {
offset += 0x400;
}
}
/* find the index in this tile map */
int index = y * 32 + x;
/* return the tile */
return tilemap[index + offset];
}
It works by first adjusting for any scrolling the tile map has done. If we have scrolled the background behind the sprite, we must add that to the sprites position. Next, we divide the position by 8 which converts from pixels to 8x8 tiles. Lastly, we adjust for wrap-around, by ensuring that the coordinates are within range of the background.
Lastly, we just use these coordinates to index into the tile map and return the tile which is there.
We can use this to detect when our koopa hits the ground by testing which tile is directly under his feet. If it is a floor type tile, then we stop him:
/* check which tile the koopa's feet are over */
unsigned short tile = tile_lookup(koopa->x + 8, koopa->y + 32, xscroll, 0, map,
map_width, map_height);
/* if it's block tile
* these numbers refer to the tile indices of the blocks the koopa can walk on */
if ((tile >= 1 && tile <= 6) ||
(tile >= 12 && tile <= 17)) {
/* stop the fall! */
koopa->falling = 0;
koopa->yvel = 0;
/* make him line up with the top of a block
* works by clearing out the lower bits to 0 */
koopa->y &= ~0x7ff;
/* move him down one because there is a one pixel gap in the image */
koopa->y++;
} else {
/* he is falling now */
koopa->falling = 1;
}
The complete code including jumping and collisions is available here.
GBA sprites offer a way to create images which can be drawn, moved and animated quickly using hardware. Setting them up requires putting a tiled sprite image into memory, and setting up the appropriate attribute bits for the sprite.
Building movement and animation logic requires just a little bit more work on top of that.
Copyright © 2024 Ian Finlayson | Licensed under a Creative Commons BY-NC-SA 4.0 License.