GB101:Scrolling Backgrounds

From NYC Resistor Wiki
Jump to navigation Jump to search
GB101 Class Notes
Sections
  1. Home
  2. Installing Gameboy Development Tools
  3. Introduction to the Gameboy Advance
  4. Hello Background
  5. Working with Palettes
  6. Tiles and Background Maps
  7. Sprites
  8. Scrolling Backgrounds
  9. Collision Detection
  10. Fixed point math
  11. Affine Sprites
  12. Sound
  13. Where do I go from here?

Introduction[edit]

What fun is a background if you can't scroll it. Fortunately the GBA has plenty of hardware to do the scroll for us. The only trick is arranging the tilemaps in memory so that the GBA will handle things correctly. Once we have that figured out it's just a matter of changing a couple of registers.

Screenbg.jpg

Remember that the standard background size is 32x32 tiles. Since the screen will only display 30x20 tiles, that already gives us a little room to scroll. When we're using more backgrounds the GBA arranges things a little funny.

32x32 64x32 32x64 64x64
1  
   
1 2
   
1  
2  
1 2
3 4

So the key is just loading our 32x32 tilemaps into the appropriate screenblocks to get the scrolling we want.

A Problem[edit]

Our favorite tile editor, TileMax doesn't seem to understand this concept. When it exports a tilemap it will run everything together. What should be

const u16 LEVEL1[64*32] = {
0x0081,..30 more map entries..,0x0082,
0x0081,..30 more map entries..,0x0082,

..60x30 more map entries ..

0x0061,..30 more map entries..,0x0061,
0x0061,..30 more map entries..,0x0061};

ends up

const u16 LEVEL1[64*32] = {
0x0081,..62 more map entries..,0x0082,
0x0081,..62 more map entries..,0x0082,

..28x64 more map entries ..

0x0061,..62 more map entries..,0x0061,
0x0061,..62 more map entries..,0x0061};

So what we need to do is take the each rows right-most 32 map entries and cut them, and then paste them to the bottom of the data. This is easy to do for the 64x32 maps, especially if your editor can do block selection. As an alternative you could handle this in your game, but it would make loading screen entry maps a little slower.

Implementation[edit]

Now then, let's do some scrolling.

#include <tonc.h>
#include "data.h"

void loop() {
	int x = 0;
	int y = 0;

	while(1) {
		// Wait for VBlank
		vid_vsync();
		
		// Get the state of the input controls
		key_poll();
		
		// Get the state of the D-pad, adjust our x/y accordingly
		x += 2 * key_tri_horz();
		y += 2 * key_tri_vert();
		
		// Set the window offset of BG2
		REG_BG2HOFS = x;
		REG_BG2VOFS = y; 
	}
}

int main()
{
	// Load the toiles into page 0
	memcpy(&tile_mem[0][0], (u8*)TS_PORTAL, 256*128);
	// Load the palette
	memcpy(pal_bg_mem, (u16*)PAL_PORTAL, 256*2);
	// Load the sceen entries into page 30
	memcpy(&se_mem[30], (u16*)LEVEL1, 64*32*2);

	// Set mode 0, bg 2,
	REG_DISPCNT = DCNT_MODE(0) | DCNT_BG2;

	// Setup BG2 to use 256 colors, with tiles on page 0, 
	// screen entries on page 30 and the size of 64x32t
	REG_BG2CNT = BG_8BPP | BG_CBB(0) | BG_SBB(30) | BG_REG_64x32;
	
	loop();

	return 0;
}

Looping[edit]

As you may notice from this demo, the map loops around and you can see it starting over. This is probably not ideal in many cases. So we'll need to bound the movement so we can't scroll the window past the edge of the screen. Another problem we haven't displayed here is that a sprite is moved independently of the background, so we need to figure out where to place the sprite within the screen whenever we move the background window. Most of the time this is easy, just place the character in the center since the background window will likely follow them. It gets tricky when you get to the edges.

Scrollbgsprite.jpg

In this example we'll want to lock the background and move the sprite in the yellow areas, and lock the sprite and move the background in the green area. The illustration is a little misleading because it only shows the scroll/no-scroll areas for the x-axis, we'll need to do the same thing for the y-axis as well.

Implementation[edit]

#include <tonc.h>
#include "data.h"
#include "sprites.h"

#define MAP_WIDTH 64*8
#define MAP_HEIGHT 32*8

// The buffer to store the OAM entries between vblanks
OBJ_ATTR obj_buffer[128];

void loop() {
	int x = 0;
	int y = 0;
	int window_x = 0, window_y = 0;
	int player_x = 0, player_y = 0;
	
	// Get a pointer to the first OAM entry
	OBJ_ATTR *sprites= &obj_buffer[0];
	obj_set_attr(sprites, 
		ATTR0_TALL | ATTR0_8BPP,  // Tall 64x64 = 32x64				
		ATTR1_SIZE_64,			  // 64x64
		ATTR2_PALBANK(0) | 0);    // pal0, tid0

	while(1) {
		// Wait for VBlank
		vid_vsync();
		
		// Get the state of the input controls
		key_poll();
		
		// Get the state of the D-pad, adjust our x/y accordingly
		x += 2 * key_tri_horz();
		y += 2 * key_tri_vert();

		// Bound x and y by the size of the map.
		if (x < 0) 
			x = 0;
		else if (x > MAP_WIDTH) 
			x = MAP_WIDTH;
		
		if (y < 0) 
			y = 0;
		else if (y > MAP_HEIGHT) 
			y = MAP_HEIGHT;
		
		// If we're in the 1st quarter, lock the window on 0 and move the sprite
		if (x < VID_WIDTH/2) {
			window_x = 0;
			player_x = x;
		}
		// If we're in the 4th quarter, lock the window as far as possible 
		// without looping and move the sprite
		else if (x > MAP_WIDTH-VID_WIDTH/2) {
			window_x = MAP_WIDTH-VID_WIDTH;
			player_x = x - window_x;
		}
		// In the 2nd and 3rd quarter center the sprite and move the screen
		else {
			window_x = x - VID_WIDTH/2;
			player_x = VID_WIDTH/2;
		}

		// Same thing as before, but vertically.  If we're too close to the top
		// lock the screen and move the sprite.
		if (y < VID_HEIGHT/2) {
			window_y = 0;
			player_y = y;
		}
		// If we're too close to the bottom, lock the screen and move the sprite
		else if (y > MAP_HEIGHT-VID_HEIGHT/2) {
			window_y = MAP_HEIGHT-VID_HEIGHT;
			player_y = y - window_y;
		}
		// Lock the sprite and move the screen otherwise
		else {
			window_y = y - VID_HEIGHT/2;
			player_y = VID_HEIGHT/2;
		}

		// Set the window offset of BG2
		REG_BG2HOFS = window_x;
		REG_BG2VOFS = window_y; 
		
		// Change the sprite position
		obj_set_pos(sprites, player_x, player_y);

		// Copy the bufferred OAM entries to the actual OAM memory
		oam_copy(oam_mem, obj_buffer, 1);	// only need to update one
	}
}

int main()
{
	// Load the toiles into page 0
	memcpy(&tile_mem[0][0], (u8*)TS_PORTAL, 256*128);
	// Load the palette
	memcpy(pal_bg_mem, (u16*)PAL_PORTAL, 256*2);
	// Load the sceen entries into page 30
	memcpy(&se_mem[30], (u16*)LEVEL1, 64*32*2);
	// Load the sprites sprite into tile memory vram page 4
	memcpy(&tile_mem[4][0], spritesTiles, spritesTilesLen);
	// Load the palette memory into palette memory
	memcpy(pal_obj_mem, spritesPal, spritesPalLen);
	
	// Set mode 0, bg 2, sprites on, 1D sprites
	REG_DISPCNT = DCNT_MODE(0) | DCNT_BG2 | DCNT_OBJ | DCNT_OBJ_1D;

	// Setup BG2 to use 256 colors, with tiles on page 0, 
	// screen entries on page 30 and the size of 64x32t
	REG_BG2CNT = BG_8BPP | BG_CBB(0) | BG_SBB(30) | BG_REG_64x32;
	
	loop();

	return 0;
}

Fun Idea[edit]

What if we created a huge font using as many tiles as possible, then filled a simple 32x32 map with some text? Now let's start scrolling to the right. Every eight pixels we scroll, change the map just right of the window to be the next column of our text. Now we've got the workings of a full screen text scroller. Combine this with another background using the raster bar effects we've already worked with and we've got quite a nice demo routine!