NDS/Tutorials/Captain Apathy/Tiling
From Dev-Scene
Contents |
[edit] Intro
I'm going to assume you've read the current Day 4 tutorial at least through the first example. There's a bunch of good information there, and I don't want to repeat it if I can avoid it.
[edit] Tiling
[edit] Getting started in non 32x32 arrangements
By default, the DS will be using a 32 by 32 tile map. This is enough for a single screen in the x direction, and some leeway in the y direction. There are, however 3 other options.
32x64(BG_32x64)
64x32(BG_64x32)
64x64(BG_64x64)
When wanting to use one of these other options, when setting the BGn_CR register, include what mode to use in the list. For example:
BG0_CR = BG_64x64 | BG_COLOR_256 | BG_MAP_BASE(0) | BG_TILE_BASE(1);
That tells the DS that we want a map that is 64 tiles wide, and 64 tiles tall.
There is a small caveat; The map is arranged in a series of 2 or 4 32x32 squares. So if using a 64x64 arrangement, to find the location of the tile at x location 3, y location 3, it will be at index 99(3(x)+32*3(y)). To access the tile at location 50,50, the index is 3666(x-32+(y-32)*32+3072).
Confused as to where the 3072 came from? That's from the size of a single map block(32*32 or 1024), and since we are working on the 4th block, we multiple the a map block size by 3.
[edit] So why 64x64?
The reason we have 64x64 at most in hardware is because the screen is 256 pixels wide which means we need at least 32 tiles to fill the entire screen. We double this to have leeway room when scrolling the screen. Once we have reached a certain point on the screen, we can just start uploading data to either a new map area, and just redirect the hardware to render from there, or start loading the data into off screen parts that we've already left. So for hardware, this is basically the minimum size required and be able to scroll.
[edit] Hey, you said you would talk about scrolling!
I will. Right now.
When scrolling, there are two registers to use to move the map. Those registers are BGn_X0 and BGn_Y0. Simple set those values to the new position, and that's where the hardware will render from.
These are not readable registers however. So it is recommended that you use a variable, and change that, and then store the variable into that register.
Example:
//Set up the tiles and the map before this.. //Also, we're assuming we're using BG0. int scrollX = 0; while (1) { scrollX += 1; BG0_X0 = scrollX; swiWaitForVBlank(); }
[edit] Setting Display order
When using multiple BGs, you may want to display them in a certain order. That's done using the BG_PRIORITY() macro when setting the Control Register(BGn_CR). The higher the number, the sooner it gets drawn, so layers that have a higher priority will get overlayed with layers with a lower priority.
[edit] Sprites
Sprites are nice to have, they're very similar to tiles, but you can move them individually rather then in large groups. I have a Sprite demo working, though I'm not 100% clear on all things sprites, but I thought I would go ahead and share what I've learned. If others can fill in this information better, please do.
[edit] Starting out
To enable sprites, when you set the mode, add DISPLAY_SPR_ACTIVE and DISPLAY_SPR_1D (assuming you're using tile mode). Another thing to do, is to set a bank to the sprite display area. I've been using Bank E (vramSetBankE(VRAM_E_MAIN_SPRITE);).
[edit] Where the sprites information is held
The sprites attributes is stored in the Object Attribute Map. This is an area of the graphics system that holds information on the position of sprites, and rotations. There is enough space there to hold 128 entries on the position and various settings of sprites, and 32 entries on their rotations. The data overlaps each other a bit though. This is how I get around it:
SpriteEntry *spriteEntry = new SpriteEntry[128]; SpriteRotation *spriteRotation = (SpriteRotation*)spriteEntry;
Now you are able to access the entries and the rotation information seperatly which is a good thing(tm).
First thing to do is to make sure you initialize the information in the structure.
int i; for (i = 0; i < 128; i++) { spriteEntry[i].attribute[0] = ATTR0_DISABLED; spriteEntry[i].attribute[1] = 0; spriteEntry[i].attribute[2] = 0; } for (i = 0; i < 32; i++) { spriteRotation[i].hdx = 256; spriteRotation[i].hdy = 0; spriteRotation[i].vdx = 0; spriteRotation[i].vdy = 256; }
Whenever you modify either spriteEntry or spriteRotation, and wish for that to be updated in hardware, you need to update the OAM. This is a simple task:
void updateOAM() { DC_FlushAll(); dmaCopy(spriteEntry,OAM, 128*sizeof(SpriteEntry)); }
We now have a place to store the data, and update the data. Now to upload the images. First, to set an entry as active, do this:
spriteEntry[index].attribute[0] = ATTR0_COLOR_256 | ATTR0_ROTSCALE; spriteEntry[index].attribute[1] = ATTR1_ROTDATA(rotindex) | ATTR1_SIZE_64; spriteEntry[index].attribute[2] = id;
Now for what all of that means.
ATTR0_COLOR_256 says we are using 256 colors in this sprite. The other option is ATTR0_COLOR_16 which says we are using 16 colors in tile mode, 16 bit in bitmap mode. ATTR0_ROTSCALE says that this sprite can be rotated and scaled.
ATTR1_ROTDATA indicates what rotation data to use(I haven't confirmed this, but I think that this is accurate...). ATTR1_SIZE_64 says we are using a 64 by 64 sprite.
the id we set in attribute 2 indicates where we are storing the data in ram.
Other options:
- ATTR0_ROTSCALE_DOUBLE: Makes it twice the size, but beyond that, no different then ATTR0_ROTSCALE
- ATTR0_NORMAL: No rotation and scaling for this sprite.
- ATTR0_SQUARE: Square sprite. So if size indicates 64, sprite is 64x64. Default
- ATTR0_WIDE: Sprite is wider then it is tall. So if size indicates 64, sprite is 64x32.
- ATTR0_TALL: Sprite is taller then it is wide. So if size indicates 64, sprite is 32x64
- ATTR0_TYPE_NORMAL: Normal rendering. Pays attention to the alpha bit for if it should be drawn or not. Default
- ATTR0_TYPE_BLENDED: Use alpha blending (for a finer tuning of how things get blended). Recommended reading is here: [1]
- ATTR1_FLIP_X: Flip along the X axis. Only works for ATTR0_NORMAL!
- ATTR1_FLIP_Y: Flip along the Y axis. Only works for ATTR0_NORMAL!
- ATTR1_SIZE_8/16/32: Indicates which size we are using.
- ATTR2_PRIORITY(n): What drawing priority to use. Sprites get drawn on top of layers of the same priority.
- ATTR2_PALETTE(n): Indicates which palette to use(may only work with 16 color sprites)
Ones I haven't played with(I've marked defaults with a '*'):
- ATTR0_TYPE_WINDOWED, ATTR0_BMP (Both of these are with ATTR0_TYPE_NORMAL and ATTR0_TYPE_BLENDED)
- ATTR0_MOSAIC (information on the mosaic effect, and how it's done on the gba can be found here: [2]. The DS probably has a similar layout.
- ATTR2_ALPHA(n)
Now that we have the sprite added, now we need to copy the palette and sprite image up to the proper locations. These would be SPRITE_PALETTE, and SPRITE_GFX.
dmaCopy(palette, SPRITE_PALETTE, palette_size); dmaCopy(image, &SPRITE_GFX[id * 16], image_size);
Once uploaded, just update the OAM.
[edit] Movement and Rotation
Moving a sprite is relatively easy. To move a sprite, do this:
spriteEntry[index].attribute[1] = spriteEntry[index].attribute[1] & 0xFE00 | (x & 0x01FF); spriteEntry[index].attribute[0] = spriteEntry[index].attribute[0] & 0xFF00 | (y & 0x00FF);
Rotations and scaling is done using something called the affine transformation matrix. This gives a lot of ability and power to do many cool translations. But to really make full use, you need to understand linear algebra. I will however opt to simply provide a method to do simple rotations.
s16 s = -SIN[angle & 0x1FF] >> 4; s16 c = COS[angle & 0x1FF] >> 4; spriteRotation[index].hdx = c; spriteRotation[index].hdy = -s; spriteRotation[index].vdx = s; spriteRotation[index].vdy = c;
It should be noted that angle does go from 0 to 512 instead of 0 to 360. To convert from 360 to 512 world, use this: angle * 512 / 360.
For more information on Affine Transformations, I suggest you read [3] as it covers the subject. As a warning, it is written the the gba, however it still applies on the DS.
Don't forget to update the OAM after rotations and translations!
[edit] Animation
There are three good ways to do animation:
- Sprite index changing
- image uploading.
- Sprite buffer swapping.
[edit] Sprite Index Changing
This is a good way to rapidly animate at relatively no cost. Simply modify attribute 2 to what index you wish to use. The problem with this is that you have to keep the different frames in your Sprite Buffer, which can be relatively small. With 64x64 sprites, at 256 bit color, assuming you're using the E bank like I suggested, you have enough space to store 16 sprites. Thats not alot. You can use some of the larger banks to hold 64 sprites, but even that's fairly limited. However, assuming you're using 8x8 sprites, you can hold 1024 sprites with Bank E.
[edit] Image uploading
This method has a higher cost, though you don't have the sprite buffer limitation. It's been reported that there isn't a noticeable slow down at 4 64x64 sprites being uploaded. Also with the dmaCopy, there won't be any tearing effects. Using this technique the animations can be much more advanced, however it's an expensive operation, so it should be avoided for large numbers of objects.
[edit] Sprite Buffer swapping or vram to vram
This method takes up two image banks instead of just one. It's like double buffering for Sprite buffers.
You set one bank to the main sprite, and one to one of the memory locations. The idea is that you upload new sprites to the one in memory(probably using dmaCopy or dmaCopyAsynch if you can do it all at once), while using the sprites in the current buffer. When you need some of the animations from the second buffer, you change buffers to the second one. The disadvantage with this is that it increases the complexity of the system, as you have to make sure that each sprite has the next frame or frames of animation in the new buffer.
[edit] So which one should be used?
They all have their advantages and disadvantages. Myself, I would see a blend of index changing and image uploading would probably be the best because they're both simple to implement, and they each solve a problem the other one has, and if used for their advantage, they can get along no problem.
[edit] Dynamic sized 2d tile systems
The hardware is limited to at most 64x64 tile maps. There are a couple of ways we can get around this.
The first is to upload the visible screen every time we move the map around. This has the disadvantage that we would not be making use of the hardware scrolling functionality of the DS, and we're doing a lot of uploads.
Another option is to only upload every so often. This way we do a lot less uploads to the hardware, but we can do better.
The nice thing is that hardware will only be showing at most 33x25 tiles at any one time. All of the off screen tiles are available for modifications.
The reason why I took awhile in completing this tutorial is that I wanted to implement my own tile system. I've finally managed to complete it, so I'll mainly be talking about what I did.
The primary thing that I did to implement the tile map is when you cross a boundary, you load an off screen section of data with the needed data, then do the move. I placed those boundaries at every 64 pixels(aka, I divided the screen into 8ths).
Whenever the user shifts the tile map, I check to see if they have crossed one of those boundaries. If they have crossed one of those boundaries, I then take the section that is just off screen in that direction, and I upload the data to that section's near visible regions. So if you're moving to the right, I upload 8 tiles above the top left corner of what is going to become visible, to 8 tiles below the bottom section. This way I upload 6 squares of data, 4 of which will become visible, and 2 extra regions above and below.
This places a restriction of shifting at most 8 tiles, or 64 pixels in one call. If I wanted to support larger shifts per call, I could simply double, triple, or quadruple the data uploaded. I don't know how valuable that would be, as moving 255 pixels is a large jump that would probably be hard for a player to keep up with.
That's about all I have to say on it. It's fairly simple to do theoretically, but it does have a number of pitfalls to watch out for. I do plan on releasing the libraries I developed for this tutorial, but I currently have a bug or two to work out before I release them to the public. If you're interested in seeing what I have done, and maybe fixing the bug, show up in the dsdev chatroom, and I may be on there under the nickname of CaptainApathy.