NDS/Tutorials Animation
From Dev-Scene
Contents |
[edit] Animated sprites for embedded devices
OK. We've seen simple rectangles and circles drifting across the screen, but it's time for something more dynamic. We are going to learn how to program animated sprites. Instead of a grid of static pixels floating around, we can effectively have small movies all over the screen. Consider the possibilities:
- a bouncy ball that deforms when it hits the screen edge;
- spaceship explosions;
- the heroic samurai brandishing his deadly blade; or
- an evil alien overlord, waxing eloquent over his plot to explode the solar system into the 13th dimension... who is cleverly disguised as your pet bunny rabbit, complete with cute ear twitches and nose wiggles.
Unfortunately, libnds doesn't give you any convenience functions for doing animated sprites (unlike SDL, DirectX and Cocoa in desktop programming environments). This means we are going to learn how to code animated sprites using "manual transmission", creating our own animation functions and understanding what is actually happening in memory.
But don't worry, it's really not that hard.
Animated sprites are made simply by swapping out images in sequence and blitting them to the screen.
That is this whole article in a nutshell. Now, let's go look at some code.
[edit] Getting your feet wet...
Let's start off with looking in the examples folder to see some demo code. Navigate to nds/Graphics/Sprite/animate_simple/, then run make and open the executable .nds file in an emulator to see what it does.
It's pretty simple. There are two different sprites that can face four directions each, and there is an animation for walking in each direction, which is triggered whenever you press a button.
If you look in animate_simple/sprites/, you'll see two sprite sheets and a grit file. Often, programmers put the frames of an animated sprite in a single file as an organizational aid. Trust me, it simplifies things a lot. For more info about grit files, see NDS/Tutorials Day 5.
There is just one source file, template.c. I've divided it up into sections for you here.
↓template.c
, lines 45-50#include <nds.h> #include <man.h> #include <woman.h> #define FRAMES_PER_ANIMATION 3
After including the libnds library, we add in the header files generated by grit, man.h
and woman.h
. These contain pointers to the bitmap data we need for our sprites. The #define
just holds how many frames there are for each direction of sprite movement.
Since the man and the woman sprites use different animation techniques, I'll document them in separate sections.
[edit] The Manly Way
The man sprite works by copying the needed section of man.png
over to VRAM. Then, the sprite is blitted (drawn) to the screen. That is, each frame is copied from RAM to VRAM just before it is blitted.
↓template.c
, lines 54-64typedef struct { int x; int y; u16* sprite_gfx_mem; u8* frame_gfx; int state; int anim_frame; }Man;
This struct
has some interesting variables in it:
sprite_gfx_mem
— points to a space in VRAM that the blitter will look at when drawing your spriteframe_gfx
— a repository in normal RAM for the entire Man bitmapstate
— what direction the sprite is facing. The possible values are in thisenum
statement:
↓template.c
, line 89enum SpriteState {W_UP = 0, W_RIGHT = 1, W_DOWN = 2, W_LEFT = 3};
anim_frame
— how many frames we are into the directional animation. This will range from0
toFRAMES_PER_ANIMATION - 1
. The value be used later to calculate where insideframe_gfx
the next animation frame begins.
Moving on...
↓template.c
, lines 112-117void initMan(Man *sprite, u8* gfx) { sprite->sprite_gfx_mem = oamAllocateGfx(&oamMain, SpriteSize_32x32, SpriteColorFormat_256Color); sprite->frame_gfx = (u8*)gfx; }
This code isn't anything new; it just allocates some space in VRAM for a single frame of a 32x32 sprite, and stores a pointer to where the bitmap is in normal RAM. Since we've allocated graphics using &oamMain
the man sprite will be on the top screen.
Now, let's take a look at main()
. (Don't worry, we'll get to animateMan()
in a bit.)
↓template.c
, line 150Man man = {0,0};
We start man
at x=0, y=0 (the top left corner of the screen).
↓template.c
, line 168initMan(&man, (u8*)manTiles);
After setting up the graphics subsystem, we initialize our man with the bitmap data pointed to by manTiles
. This pointer will be created by grit and defined in man.h
just before compile time.
↓template.c
, line 171dmaCopy(manPal, SPRITE_PALETTE, 512);
Copy the 512-byte color palette over to VRAM. We use dmaCopy()
since it uses dedicated hardware and is faster than a for()
loop (see the main Wikipedia article about DMA). By the way, for this section of code, I would have used the #define manPalLen 512
inside man.h
that grit defined for me, just in case I changed the color depth of my source images later on.
[edit] Animating: The Manly Way
Inside the while()
loop in main()
, there are first a whole bunch of simple if()
statements that just change the coordinates of our sprites depending on what D-Pad button is pressed. I won't bother documenting that, because that's easy. After the if()
statements about the buttons is the interesting stuff:
↓template.c
, line 214man.anim_frame++;
Move on to the next frame, but...
↓template.c
, line 217if(man.anim_frame >= FRAMES_PER_ANIMATION) man.anim_frame = 0;
...if we've moved past the last frame in that direction, restart from frame 0.
Now, we call animateMan(&man);
in line 222. Let's take a look at that function definition.
↓template.c
, lines 99-106void animateMan(Man *sprite) { int frame = sprite->anim_frame + sprite->state * FRAMES_PER_ANIMATION; u8* offset = sprite->frame_gfx + frame * 32*32; dmaCopy(offset, sprite->sprite_gfx_mem, 32*32); }
OK. Now, as you know, computers can't really store 2-D data. So what they really do is they store the 2-D grid of pixels as a 1-D list of numbers. So for an 8x8 bit sprite, it will turn
0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63
into a 1-D list of pixels in the order indicated.
In a similar way, grit took our sprite frames, which were laid out like so:
0 1 2 3 4 5 6 7 8 9 10 11
and flattened them into a 1-D string of bytes in memory laid out from frame 0 through frame 11.
Because of the strategic way in which the SpriteState enum
was defined, our sprite frame number is sprite->anim_frame + sprite->state * FRAMES_PER_ANIMATION
, as set in the frame
variable at the beginning of animateMan()
above.
Also, since each pixel (assuming 256-bit color) is 1 byte, and each frame is 32*32 pixels in size, our offset relative to the beginning of the sprite sheet bitmap is:
u8* relativeOffset = 32*32*frame;
Because sprite->frame_gfx
is the address of the start of our sprite sheet bitmap, our total offset is:
u8* offset = sprite->frame_gfx + relativeOffset;
Compare this to the actual source code above, and you'll see that our code is equivalent to theirs.
Now that we have the correct memory address, we just use dmaCopy()
to move the correct frame into VRAM.
Now we jump back into main()
:
↓template.c
, lines 230-31oamSet(&oamMain, 0, man.x, man.y, 0, 0, SpriteSize_32x32, SpriteColorFormat_256Color, man.sprite_gfx_mem, -1, false, false, false, false, false);
And we're back in easy territory. We just set the sprite position and other attributes...
↓template.c
, lines 236-38swiWaitForVBlank(); oamUpdate(&oamMain);
...wait for the screen, and then blit. That's the Manly Way.
[edit] The Womanly Way
The Womanly Way differs from the Manly Way mainly in that the Womanly Way loads the entire sprite sheet into VRAM and just changes the graphics pointer to advance to the next frame. As you can imagine, this is much faster than the Manly Way, where you have to actually copy over the next graphic at every single frame change. The drawback is that the Womanly Way (in this example case) takes 12x the VRAM as the Manly Way, and VRAM is relatively scarce. More on this and related topics later.
↓template.c
, lines 72-84typedef struct { int x; int y; u16* sprite_gfx_mem[12]; int gfx_frame; int state; int anim_frame; }Woman;
The difference between the man's struct
and the woman's struct
is in the middle two lines of the struct
. Since the woman sprite is going to be storing all of her data in VRAM, we don't need to keep a pointer to a repository of frames in regular RAM like for the Man. The Man, as you recall, needed this to copy the frame to VRAM, and then the graphics subsystem blitted him to the screen. Since the Woman will already be in VRAM, sprite_gfx_mem[]
and gfx_frame
will tell the graphics subsystem where to blit from.
↓template.c
, lines 133-143void initWoman(Woman *sprite, u8* gfx) { int i; for(i = 0; i < 12; i++) { sprite->sprite_gfx_mem[i] = oamAllocateGfx(&oamSub, SpriteSize_32x32, SpriteColorFormat_256Color); dmaCopy(gfx, sprite->sprite_gfx_mem[i], 32*32); gfx += 32*32; } }
Note that the Woman's initializer is slower than the Man's, because of the series of calls to oamAllocateGfx()
and dmaCopy()
. (On a picky side point: even though DMA doesn't usually take cycles away from the processor, there are only 4 DMA channels available to the arm9. Especially on larger sprite frames, DMA hardware calls after the fourth channel is taken will have to wait on a previous DMA call to complete before it can get to work itself.) However, the extra time taken is less critical here at load-time than in-game.
[edit] Animating: The Womanly Way
Since much of the Womanly code in main()
is basically the same as for Man, I'll skip the boring stuff.
↓template.c
, lines 124-127void animateWoman(Woman *sprite) { sprite->gfx_frame = sprite->anim_frame + sprite->state * FRAMES_PER_ANIMATION; }
The animation code just changes the gfx_frame
index, which is faster than the Manly just-in-time call to dmaCopy()
. Eventually, at the end of the loop in main()
, oamSet
is called like so:
↓template.c
, lines 233-234oamSet(&oamSub, 0, woman.x, woman.y, 0, 0, SpriteSize_32x32, SpriteColorFormat_256Color, woman.sprite_gfx_mem[woman.gfx_frame], -1, false, false, false, false, false);
The index that was recalculated in animateWoman()
is used here to point to where the graphics subsystem needs to blit from.
That's it for the Womanly Way.
[edit] Conclusions: Which One, Which One?
By no means are these two methods the only ways to animate sprites. Inside the comments in template.c
we find:
A more advanced approach is to keep track of which frames of which sprites are loaded into memory during the animation stage, load new graphics frames into memory overwriting the currently unused frames only when sprite memory is full. Decide which frame to unload by keeping track of how often they are being used and be sure to mark all a sprites frames as unused when it is "killed"
However, Rogers also noted in the same comment block that CPU cycles, not to mention DMA, can handle the Manly Way quite ably, and said that he would generally choose the Manly Way over the Womanly Way because of scarce VRAM, compared to main RAM.
That is not to say that Womanly Way should never be used at all. If you would like to free up as many DMA channels as possible and reduce the load on the processor while some complicated, cycle-hogging process is completing between animation frames, the Womanly Way could be better than the Manly Way. For example, if you want to load and initialize the next game map as your hero approaches a boundary, you may want to avoid using DMA for the Man at every single frame, and opt for the Womanly Way instead.
[edit] Notes
Sprites larger than 8x8 pixels are not stored linearly in RAM/VRAM, row by row. Instead, they are broken down into 8x8 sections, and these sections are then laid out in memory left-to-right, top-to-bottom. This means that the pixels that belong in the same row are not contiguous! There will be 8 pixels together here, then 64 locations later, the next 8 pixels in the row will appear. I have written a few functions that will return the position of a pixel inside the sprite data array, given the coordinates of the desired pixel (top-left being the origin). I only wrote functions for square sprites, but the rest can be extrapolated:
inline unsigned int sprite8XY(unsigned int x, unsigned int y) { return x + (y<<3); } inline unsigned int sprite16XY(unsigned int x, unsigned int y) { return sprite8XY(x & 0x07, y & 0x07) + (x & 0x08)<<3 + (y & 0x08)<<4; } inline unsigned int sprite32XY(unsigned int x, unsigned int y) { return sprite8XY(x & 0x07, y & 0x07) + (x & 0x18)<<3 + (y & 0x18)<<5; } inline unsigned int sprite64XY(unsigned int x, unsigned int y) { return sprite8XY(x&0x07,y&0x07) + (x&0x38)<<3 + (y&0x38)<<6; }
For an interesting discussion of various oddities an NDS developer may encounter while taking advantage of DMA, see Vijn's article about cache and RAM synchronization at http://www.coranac.com/2009/05/dma-vs-arm9-fight/.