Tutorial 1. Dynamic Tile Scrolling
Page - [1] 2 3
Introduction
Every video game lover has played at least one side-scrolling game in their day. The ones that come to mind immediately for
me are Super Mario Bros., Metroid, Metal Storm, Kid Icarus, and Ghouls 'n' Ghosts. This style of game was a huge genre when the
Super Nintendo & Sega Genesis were popular, and can be seen in various newer titles on the Gameboy Advance and the Nintendo DS.
Assuming you've done some experimenting with libnds (www.devkitpro.org), you have probably wondered how to do the same scrolling effects properly on a Nintendo DS Homebrew
application. In this tutorial, I will try to cover all of the questions you have about dynamic tile scrolling, along with how
to setup your own engine. This includes multiple solutions to the same problem, and advanced alternatives used as an exercise
for the readers.
You should also read the Dev-Scene's tutorial on tile mapping if you are unfamiliar with the topic.
Nintendo DS Tutorial Day 4 - Dev-Scene
Simple Dynamic Tile Scrolling
Before we tackle the big problem of filling a screen full of tiles, lets think of a smaller example: dynamically tiling a
screen that has 8 by 6 8 pixel tiles. We also have map data that consists of 16 pixel by 16 pixel tiles, so lets
lay this all out in data.
Screen Data (8 pixel blocks each) Map Data (16 pixel blocks each)
00000000 3313222222
00000000 3311322222
00000000 3331113222
00000000 3333111111
00000000
00000000
If you are completely confused at this point, please take a look at the Dev-Scene tutorial I linked above. If you are following along, lets iterate through several locations:
=================================================================================
Screen @ (0,0) Screen @ (1,0) Screen @ (2,0) Screen @ (3,0) Screen @ (4,0)
33331133 33311332 33113322 31133222 11332222
33331133 33311332 33113322 31133222 11332222
33331111 33311113 33111133 31111332 11113322
33331111 33311113 33111133 31111332 11113322
33333311 33333111 33331111 33311111 33111111
33333311 33333111 33331111 33311111 33111111
Screen @ (0,1) Screen @ (1,1) Screen @ (2,1) Screen @ (3,1) Screen @ (4,1)
33331133 33311333 33113322 31133222 11332222
33331111 33311113 33111133 31111332 11113322
33331111 33311113 33111133 31111332 11113322
33333311 33333111 33331111 33311111 33111111
33333311 33333111 33331111 33311111 33111111
33333333 33333331 33333311 33333111 33331111
=================================================================================
And so on. If you didn't see the pattern, just look at the coordinates we were given, and think of that as an "offset" on
the map data provided. Because each block in the map data is 16x16, and our screen is 8x8, we have to double up all of
the tiles we map onto the screen. So if you were at map coordinate (4,3), and you found a 1 tile, it would take up 4 tiles
on the screen.
So using this fashion of dynamically moving between our map, we can create a very simple DS program to demonstrate this
movement. Use the arrow keys in the following example to move through the map data in a simple tile scroller.
 [ Downloadable Version ]
[ Source Code ]
=================================================================================
#include
//create a red 8x8 tile called redTile
u8 redTile[64] =
{
1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1,
1,1,1,1,1,1,1,1
};
//create a green 8x8 tile called greenTile
u8 greenTile[64] =
{
2,2,2,2,2,2,2,2,
2,2,2,2,2,2,2,2,
2,2,2,2,2,2,2,2,
2,2,2,2,2,2,2,2,
2,2,2,2,2,2,2,2,
2,2,2,2,2,2,2,2,
2,2,2,2,2,2,2,2,
2,2,2,2,2,2,2,2
};
// create a blue 8x8 tile called blueTile
u8 blueTile[64] =
{
3,3,3,3,3,3,3,3,
3,3,3,3,3,3,3,3,
3,3,3,3,3,3,3,3,
3,3,3,3,3,3,3,3,
3,3,3,3,3,3,3,3,
3,3,3,3,3,3,3,3,
3,3,3,3,3,3,3,3,
3,3,3,3,3,3,3,3
};
// Map Data is a (10 by 4) Array full of 16x16 blocks
u16 mapData[40] =
{
3,3,1,3,2,2,2,2,2,2,
3,3,1,1,3,2,2,2,2,2,
3,3,3,1,1,1,3,2,2,2,
3,3,3,3,1,1,1,1,1,1
};
void updateMapMemory(int x, int y)
{
int i,ix,iy;
u16* mapMemory = (u16*)BG_MAP_RAM(0);
for ( i = 0; i < 48; i++) // our screen is 8x6 = 48
{
ix = i % 8;
iy = i / 8;
mapMemory[((iy)*32) + (ix)] = mapData[((y+iy)/2)*10
+ ((x+ix)/2)%10]; //Demystified later on
}
}
int main(void) {
int x,y; // Our current x & y positions in the map data
int pressed; // Used for key input
x = y = 0; // Set both x & y to 0 initially
powerON(POWER_ALL_2D);
// Enable our IRQ & VBlank for updating map data
irqInit();
irqEnable(IRQ_VBLANK);
//set video mode and map vram to the background
videoSetMode(MODE_0_2D | DISPLAY_BG0_ACTIVE);
vramSetBankA(VRAM_A_MAIN_BG_0x06000000);
//get the address of the tile
u8* tileMemory = (u8*)BG_TILE_RAM(1);
BG0_CR = BG_32x32 | BG_COLOR_256 | BG_MAP_BASE(0)
| BG_TILE_BASE(1);
//load our palette
BG_PALETTE[1] = RGB15(31,0,0); // red color
BG_PALETTE[2] = RGB15(0,31,0); // green color
BG_PALETTE[3] = RGB15(0,0,31); // blue color
DC_FlushAll();
//copy the tiles into tile memory one after the other
dmaCopy(redTile, tileMemory + 64, 64);
dmaCopy(greenTile, tileMemory + 128, 64);
dmaCopy(blueTile, tileMemory + 192, 64);
// Init the screen!
updateMapMemory(x,y);
while(1)
{
scanKeys(); // read the button states
pressed = keysDown(); // buttons pressed this loop
if( pressed & KEY_LEFT )
{
if ( x > 0 ){
x--;
updateMapMemory(x,y);
}
}
if( pressed & KEY_RIGHT )
{
if ( x < 12 ){ // 12 is demystified below
x++;
updateMapMemory(x,y);
}
}
if ( pressed & KEY_UP)
{
if ( y > 0 ){
y--;
updateMapMemory(x,y);
}
}
if ( pressed & KEY_DOWN )
{
if ( y < 2 ){ // 2 is demystified below
y++;
updateMapMemory(x,y);
}
}
swiWaitForVBlank();
}
return 0;
}
=================================================================================
Explanation
Notice in the code I have used BG0 for my background in Mode 0 (you could also use Mode 5.) Mode 0 and 5 both provide a text
background consisting of a "Map" of 32 by 32 8x8 tiles, or 1024 tiles total, and we are using the top left corner
of our tile map to dynamically scroll through the map data provided.
So lets demystify the code and try to figure out how this simple example is working, and how we can expand this
to make a solid dynamic tile engine. First, we create 3 tiles, each with their own palette lookup, which we define in our main
loop at red, gree, and blue. Tile data should not be new to you at this point, so if you are having trouble grasping how I am
using these tiles, please refer to the Dev-Scene Tutorial series. Next, we want to make a function that will update our map
memory to match where we currently are in the mapData array declared earlier.
This is probably the most confusing part mathematically of the code. The function updateMapMemory is essentially projecting our
mapData, which is some power of 8 (DS uses 8x8 tiles generally), onto our screen, or the MapMemory we have declared for the Background.
For this example, we have chosen Tile Ram 1, Map Base 0 as seen in the variables u8* tileMemory & u16* mapMemory. So now if you
want to project the ficticious mapData onto our actual map memory, you need to go from a 10 by 4 grid of 16x16 pixel blocks, to a
8 by 6 grid of 8x8 pixel blocks. The way we accomplish the transfer is by looping through each tile we want to write to, and copying
the corresponding mapData number.
Picking apart the code even further, we notice the following constant values:
=================================================================================
ix = i % 8; <-- i % 8 is the x offset of our current tile location
iy = i / 8; <-- i / 8 is the y offset of our current tile location
mapMemory[((iy)*32) + (ix)] <-- we know that the entire screen is currently
32x32 tiles, so for each time we move down a row, we need to
increment 32 times. This is a good trick for keeping your
arrays 1-dimensional while still having a position (x,y). You
can also define a macro for this step, which we will
do later.
= mapData[((y+iy)/2)*10 + ((x+ix)/2)%10]; <--- x & y are both passed in
variables, used for the "user space", or where we want
the person viewing the map to currently see. ix & iy are
the tile offsets used to translate onto the mapMemory, and
finally 10 is the width of the map array we are copying from.
=================================================================================
Furthermore, because we are going from a series of 16x16 blocks to our 8x8 blocks on the screen, we can refer to this as
"metatiles", or projecting tiles larger than 8x8 onto an 8x8 tile set. You could apply this to any size map & screen, and
apply the following formula with this application.
=================================================================================
// The following values apply to our current demo
#define SCREEN_WIDTH 8
#define SCREEN_HEIGHT 6
#define DS_32x32_WIDTH 32
#define MAP_PIXEL 16 // 16x16 square
#define SCREEN_PIXEL 8 // 8x8 square
#define MAP_WIDTH 10
for ( i = 0; i < SCREEN_WIDTH * SCREEN_HEIGHT; i++)
{
ix = i % SCREEN_WIDTH;
iy = i / SCREEN_WIDTH;
mapMemory[((iy)*DS_32x32_WIDTH) + (ix)] =
mapData[((y+iy)/(MAP_PIXEL/SCREEN_PIXEL))*MAP_WIDTH +
((x+ix)/(MAP_PIXEL/SCREEN_PIXEL))%MAP_WIDTH];
}
=================================================================================
You could most certainly increase the screen width & height to their max, 32 by 32, just remember the DS screen only shows
a 256x192 region, so whatever is at the bottom of your map will be cut off. There is not a way to calculate the actual map
width in a 1 dimensional array, so you will have to just define that yourself, or use whatever tool to create the defines
for you (Mappy will do this.)
The last thing to make note of in this demo is the max numbers I check for when pressing right or down. Because you are
translating from one array to the other, you want to make sure you do not overflow the current array. The DS does not
have any way of saying "you are out of bounds", so if you start going over the allocated memory for so and so array, it
will keep on writing.
The proper way to find these numbers is done with the following calculation:
=================================================================================
Array overflow calculations:
MAX_X_VALUE = (MAP_PIXEL/SCREEN_PIXEL) * MAP_WIDTH - SCREEN_WIDTH
MAX_Y_VALUE = (MAP_PIXEL/SCREEN_PIXEL) * MAP_HEIGHT - SCREEN_HEIGHT
=================================================================================
Applying these numbers to our current demo, our max x value = ((16/8) * 10 - 8) = 12, max y value = ((16/8) * 10 - 6) = 2.
This will also work for any screen width, screen height, map width and map height. These are all very useful facts because
the next step we will be taking is expanding our demo to fill the entire 32x32 screen, and using a much larger map array
to fill in the data. We will also look at scrolling the background to make our transition from tile to tile MUCH smoother.
Goto Page 2: Large, Scrolling Dynamic Tiles ====>>
|