NDS/Tutorials Day 4
From Dev-Scene
Contents |
[edit] Introduction
In yesterday’s chapter we talked at length about how to compose an image on screen by manipulating each pixel until our scene was formed. While this gave us a great deal of control over the final result we quickly realized the DS is not quite a software rendering powerhouse.
To compensate for a relatively slow processor and an even more limiting amount of VRAM the DS includes several hardware features, the result of which is the most advanced dedicated 2D processing systems ever placed in a video game console.
Tile based rendering is the key component of this 2D technology and understanding it will allow you to squeeze enormous, detailed, and fully interactive worlds from the seemingly limited DS resources.
[edit] Tile Modes
What are tile-based graphics? Put simply, it means to describe your scene using a mapping of tile indexes to tile graphics. Instead of describing the screen as a 2D matrix of pixels we are going to describe it as a matrix of tiles, where each tile represents a small bitmap. Let us look at one of the better-known tile based games and get a feel for how it was put together.
Below is all the graphics used to construct the entire overworld of the original Zelda.
You may recognize these little 16x16 chunks as pieces of the Zelda world and perhaps you could imagine that in order to describe the look of the overworld all one would have to do is store which tile goes where. For instance the following familiar scene could be represented by an array of tile numbers.
Such as this:
short map[] = { 64,64,64,64,64,64,64,06,06,64,64,64,64,64,64,64, 64,64,64,64,07,64,62,06,06,64,64,64,64,64,64,64, 64,64,64,62,06,06,06,06,06,64,64,64,64,64,64,64, 64,64,62,06,06,06,06,06,06,64,64,64,64,64,64,64, 64,62,06,06,06,06,06,06,06,63,64,64,64,64,64,64, 06,06,06,06,06,06,06,06,06,06,06,06,06,06,06,06, 64,67,06,06,06,06,06,06,06,06,06,06,06,06,64,64, 64,64,06,06,06,06,06,06,06,06,06,06,06,06,64,64, 64,64,06,06,06,06,06,06,06,06,06,06,06,06,64,64, 64,64,66,66,66,66,66,66,66,66,66,66,66,66,64,64, 64,64,64,64,64,64,64,64,64,64,64,64,64,64,64,64 };
To render the scene all you would have to do is loop through the indexes stored in the map array and use those values to blit each tile to the screen. The entire world map would only end up being a few KBs in size instead of the 10s of Megabytes it would take to store it as a big image. The trade off of course is the increase in the amount of time it takes to render because we have to do a conversion between this map and the final bitmap we want on the screen.
Fortunately (and hopefully obviously at this point) the NDS 2D hardware is built for just this purpose making rendering tile based worlds a snap. All we really need to do is create a map and a tile set, place them into 2D video memory and tell the DS where to find them and it will do the magic for us.
[edit] Background Memory Layout and VRAM Management
In order to meet this first goal of placing tiles and maps into memory we must know where in memory to place them and in what format the NDS expects this data. This brings us back to the seemingly ever-present task of video memory management and memory layout.
The image below will look familiar if you read yesterday’s chapter on bitmap graphics modes.
The above depicts the layout of main background memory as seen by the 2D engine. Background memory is divided into character memory (where you stick the actual tile graphics) and map memory (where you stick the map data).
It turns out you can place maps anywhere in the first 64KB of background memory and you can place tiles anywhere in the first 256KB; the blocks depicted above are only logical offsets and nothing prevents data from crossing these boundaries.
To load a map into memory you pick a block offset and write the map there, then tell the DS were to find it. You then do the same for your tile graphics. Finally you load a palette and call it a day (there may be a few more details).
One thing we need to figure out is the format of tile and map data. It turns out this is rather straight forward.
[edit] Map Entries
There are two forms of maps. Ones with 8 bit entries and ones with 16-bit. The 8-bit flavor are simply an offset into character memory.
For instance if you want the third entry in your map to use tile number 4 you just stick a 4 in that maps entry.
8 bit indexed maps are used only for “Rotation” backgrounds. “Text” and “Extended Rotation” use the more flexible 16 bit indices.
16-bit indexes are broken up into character index and control bits. The low 10 bits represent the index of the character and allow you to address up to 1024 unique characters. The next two bits will cause the character to flip vertically or horizontally. Finally there are 4 bits which let you choose a palette.
Bits: | 15 | 14 | 13 | 12 | 11 | 10 | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | 1 | 0 |
---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
Purpose | Palette | Vertical Flip | Horizontal Flip | Index |
To create a map you fill an array of short ints with these character indexes and you can control not only which character is rendered to the screen but what palette it uses and if it is flipped. If you ignore the flip bits and the palette bits you can treat it as a simple character index (you are still limited to 1024 tiles though).
I think at this point we know just enough about maps to get into trouble so let us see if we can trick the DS into displaying one.
[edit] First Map Demo
This should serve as in introduction to rendering a map. In the next sections we will go into detail about maps and character graphics.
#include <nds.h> //create a 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 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 }; //--------------------------------------------------------------------------------- int main(void) { //--------------------------------------------------------------------------------- int i; //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 and map blocks u8* tileMemory = (u8*)BG_TILE_RAM(1); u16* mapMemory = (u16*)BG_MAP_RAM(0); //tell the DS where we are putting everything and set 256 color mode and that we are using a 32 by 32 tile map. REG_BG0CNT = BG_32x32 | BG_COLOR_256 | BG_MAP_BASE(0) | BG_TILE_BASE(1); //load our palette BG_PALETTE[1] = RGB15(31,0,0); BG_PALETTE[2] = RGB15(0,31,0); //copy the tiles into tile memory one after the other swiCopy(redTile, tileMemory, 32); swiCopy(greenTile, tileMemory + 64, 32); //create a map in map memory for(i = 0; i < 32 * 32; i++) mapMemory[i] = i & 1; while(1) swiWaitForVBlank(); return 0; }
Like other demos we first pick a video mode which does what we want and turn on the things that need turned on. We then map in vram as appropriate.
videoSetMode(MODE_0_2D | DISPLAY_BG0_ACTIVE); vramSetBankA(VRAM_A_MAIN_BG_0x6000000);
As you can see we chose mode 0 and background 0 and if you look on the graphics mode table you will notice this gives us a “Text” background. That means we will use 16 bit indexes.
The next thing we do in this demo is choose a tile block and a map block as the starting points for our data. You can choose any combination of blocks you like as long as your map data and your tile data do not end up in the same place.
A good approach is to stick your maps in the first few map blocks and the tiles starting at tile block 1. This gives you 16K for maps which is normally enough.
u8* tileMemory = (u8*)BG_TILE_RAM(1); u16* mapMemory = (u16*)BG_MAP_RAM(0);
Now that we have made this choice we need to let the DS know about it through the background control register.
BG0_CR = BG_32x32 | BG_COLOR_256 | BG_MAP_BASE(0) | BG_TILE_BASE(1);
We also put the background in 256 color mode. We could have picked 16 color mode but that is a touch harder to create data by hand for. We also opted to go with a 32 by 32 tile map.
If you look towards the top of the file you notice we created two tiles filled with 1 and 2 respectively. We arbitrarily called them red and green so I suppose we should make sure the color in palette entry 1 is infact red and the one in palette entry 2 is green.
BG_PALETTE[1] = RGB15(31,0,0); //red BG_PALETTE[2] = RGB15(0,31,0); //green
Next we use a bios call to copy the tiles into tile memory. First one then the other. The call takes the source of the data, the destination, and the size (in halfwords). The red tile is copied into tile offset 0 and the next 64 bytes later which is tile offset 1.
swiCopy(redTile, tileMemory, 32); swiCopy(greenTile, tileMemory + 64, 32);
Now all we need to do is put a map in map memory. If we set the tile index to 0 a red tile will be drawn. If we set it to 1 a green tile will be drawn. Because creating a map by hand is a pain we use a short loop which alternately sets the index to 1 or 0 (as always if this is unclear be sure to review the bit manipulation techniques we discussed in day 2).
for(i = 0; i < 32 * 32; i++) mapMemory[i] = i & 1;
If you compile and run this demo you will be greeted by your first tile based background…a simple set of vertical strips which alternate red and green.
The next step in our endeavor will be to explore the Text backgrounds fully.
[edit] Text Backgrounds
Please keep on this splendid tutorial! It has begun to be very excited and now which way will I go further? Plz carry it on!