|
Chapter 4 – Displaying “Character” Sprites
In this chapter, we will extend our program to display two planes that
fly left and right over the canyon playfield. The planes will be implemented
as a pair of animated sprites.
What you need for this chapter
1. The project created in Chapter 3
2. The bitmap containing the plane images
To make the planes fit with the canyon from the last chapter, we'll be
using more left-over “Programmer-Art” from the PC version
of Boulder Bombers.
Again we start by converting the images into code that can be included
in the project. Unfortunately if you use the SoConverter to process the
vertical strip image to the right, you will get a sprite animation that
contains just one frame. There are two ways around this. The first is
to use an outside tool to create an eight frame FLI animation file out
of the eight plane images and then use SoConverter to convert that. The
second is to cheat. Since cheating doesn't require any additional tools,
that's what we'll be doing.
Put the planes.bmp file in the data subdirectory of the BBAdvance project.
From a command prompt in the BBAdvance/data directory run the SoConverter.exe
to convert the bitmap to a sprite animation and then again to convert
it to a palette.
% SoConverter.exe -file planes.bmp -converter SoSpriteAnimation
Invalid value for the -converter argument
Please wait while converting....
Done
Press almost any key to continue...
% SoConverter.exe -file planes.bmp -converter SoPalette
Invalid value for the -converter argument
Please wait while converting....
Done
Press almost any key to continue...
Now we've got to fix up the planesSpriteAnimation.dat file to correct
the size and number of frames.
Open the planesSpriteAnimation.dat file using your editor and page
down to the end of the file to the planesSpriteAnimation variable declaration.
Change it from a single frame 16x128 pixel animation to an eight frame
16x16 pixel animation* and save the result.
const SoSpriteAnimation planesSpriteAnimation = { false, 16, 16, 8,
(u8*) planesSpriteAnimationData };
Next we turn our attention to the code needed to make use of the plane
data. Our plane sprites will need some persistent data to keep track of
where they are and where they're going. We'll add this to the top of the
file along with some #defines to make the code more readable.
First, add #include statements to reference the data we generated for
the planes. These should be placed next to the #includes we previously
added for the tile data.
#include "planesPalette.dat"
#include "planesSpriteAnimation.dat"
Now, add the following lines following the #include “Socrates.h”
statement. The PLAYER_SPRITE_* #defines are indexes into our frame animation.
The player offset refers to the number of frames for an individual plane
type (orange or purple in this case) and the direction offset refers
to the number of frames for the plane in a single direction (prop up
and prop down). The Player position #defines give the limit positions
for the sprites going in each direction. They are slightly outside of
the screen coordinates to allow for a short delay when the plane leaves
the playfield.
// =======================================================================
// Configuration #defines
// =======================================================================
#define MAX_PLAYERS 2
#define DIRECTION_RIGHT 0
#define DIRECTION_LEFT 1
#define PLAYER_SPRITE_PLAYER_OFFSET 4
#define PLAYER_SPRITE_DIRECTION_OFFSET 2
#define PLAYER_POSITION_MIN -(16+8)
#define PLAYER_POSITION_MAX (SO_SCREEN_WIDTH+8)
It's generally helpful to declare a structure to contain all of the data specific to a
particular player. This typically includes all of the variables that together make up the
player state. For us, this is currently just a reference to the player sprite, its position
and direction. Add a playerInfo structure to contain this data. It should be placed after
the 'Configuration #defines' section
// =======================================================================
// Data Structures
// =======================================================================
struct playerInfo {
SoSprite *sprite;
int iType;
int iDirection;
SoVector2 vPosition;
};
To make the main code simpler, we'll use a couple of global constants
to specify the starting/ending positions and directions of all players.
Add this code to the end of the 'Constant data' section following the
canyonData declaration.
// starting values ... indexed by player number
const u32 startDirection[] = { DIRECTION_RIGHT, DIRECTION_LEFT };
// starting values ... indexed by DIRECTION_*
const SoVector2 startPosition[] = {
{ PLAYER_POSITION_MIN, 16 },
{ PLAYER_POSITION_MAX, 32 }
};
Finally, we need a couple of global variables to hold the persistent
data about our planes.
// =======================================================================
// Globals
// =======================================================================
struct playerInfo g_player[MAX_PLAYERS];
u32 g_iPlayerBaseSpr;
Now that all of our data is in place, we add the code to use it.
Add a new subroutine to initialize the sprites. It should be located
after our setupPlayfield subroutine. You must call a minimum of nine
functions to set up a rotating/scaling sprite. These calls are:
SoSpriteManagerInitialize
-- called just once
SoSpriteMemManagerInitialize
-- called just once
SoSpriteMemManagerLoad
-- called once per sprite type (e.g. our plane)
SoSpriteManagerRequestSprite
-- called once per sprite
SoSpriteCopyPropertiesFromAnimation
- called once per sprite
SoSpriteManagerSetRotationAndScale
-- called once per unique rotation/scaling configuration
SoSpriteSetRotationAndScaleIndex
- called once per sprite
SoSpriteSetRotationAndScaleEnable
-- called once per sprite
SoSpriteManagerEnableSprites
-- called just once
// Setup Players (Sprites)
void setupSprites(void)
{
int ii;
SoPaletteSetPalette(SO_SPRITE_PALETTE, planesPalette, true);
SoSpriteManagerInitialize();
SoSpriteMemManagerInitialize();
// load sprite data
g_iPlayerBaseSpr = SoSpriteMemManagerLoad(&planesSpriteAnimation);
for(ii = 0; ii < MAX_PLAYERS; ++ii)
{
g_player[ii].iDirection = startDirection[ii];
g_player[ii].vPosition = startPosition[ii];
g_player[ii].iType = ii;
g_player[ii].sprite = SoSpriteManagerRequestSprite();
SoSpriteCopyPropertiesFromAnimation(g_player[ii].sprite,
&planesSpriteAnimation);
SoSpriteManagerSetRotationAndScale(ii, 0,
SO_FIXED_FROM_WHOLE(1), SO_FIXED_FROM_WHOLE(1));
SoSpriteSetRotationAndScaleIndex(g_player[ii].sprite, ii);
SoSpriteSetRotationAndScaleEnable(g_player[ii].sprite, true);
}
SoSpriteManagerEnableSprites();
}
Add a new subroutine to update the sprites each time a new frame is
to be drawn. We'll need to call three SGADE routines to update each
sprite. They are:
SoSpriteSetTranslate
-- called to update/set the sprite's new position
SoSpriteSetAnimationIndex
-- called to update/set the sprite's image
SoSpriteManagerUpdate
-- applies all updates to actual sprites
// ***********************************************************************
// *** CONTROL
// ***********************************************************************
void update(void)
{
int ii;
static int cntFrame = 0;
static int animFrame = 0;
cntFrame += 1;
if(!(cntFrame&1))
animFrame += 1;
// update player positions
for(ii = 0; ii < MAX_PLAYERS; ++ii)
{
if(g_player[ii].iDirection == PLAYER_DIRECTION_RIGHT)
{
g_player[ii].vPosition.m_X += 1;
}
else
{
g_player[ii].vPosition.m_X -= 1;
}
// check if player went off screen
if((g_player[ii].vPosition.m_X < PLAYER_POSITION_MIN) ||
(g_player[ii].vPosition.m_X > PLAYER_POSITION_MAX))
{
if(g_player[ii].iDirection == PLAYER_DIRECTION_RIGHT)
{
g_player[ii].iDirection = PLAYER_DIRECTION_LEFT;
}
else
{
g_player[ii].iDirection = PLAYER_DIRECTION_RIGHT;
}
g_player[ii].vPosition =
startPosition[g_player[ii].iDirection];
}
// move plane
SoSpriteSetTranslate(g_player[ii].sprite,
g_player[ii].vPosition.m_X, g_player[ii].vPosition.m_Y);
SoSpriteSetAnimationIndex(g_player[ii].sprite,
g_iPlayerBaseSpr + (animFrame%PLAYER_SPRITE_DIRECTION_OFFSET +
(g_player[ii].iType*PLAYER_SPRITE_PLAYER_OFFSET) +
(g_player[ii].iDirection*PLAYER_SPRITE_DIRECTION_OFFSET)) *
SoSpriteAnimationGetNumIndicesPerFrame(&planesSpriteAnimation));
}
SoSpriteManagerUpdate();
}
Finally, we modify our AgbMain function to make use of the new sprite
functions.
First, add a call to setupSprites() immediately following the call
to setupPlayfield(). Then, replace the infinite for(;;) loop with this
block of code:
for(;;)
{
// Wait for VBlank;
SoDisplayWaitForVBlankEnd();
SoDisplayWaitForVBlankStart();
update();
}
The SoDisplayWaitForVBlankEnd()
and SoDisplayWaitForVBlankStart()
calls ensure that the update is done just once during each vertical
blank -- about 60 times a second. Since the planes move one pixel per
update, this means that it will take about four seconds to go all the
way across the screen.
Compile your updated project by running ‘make’ from a command
shell in the BBAdvance directory. Once it’s ready, try it out. You
will get a screen like this one (it's much more interesting when it's
moving).
click here to download the solution
to this chapter
Footnotes
* Note that the cheat to create a multiple frame
SoSpriteAnimation using SoConverter.exe on a single bitmap only works
when the frames are consecutively arranged in the bitmap in a vertical
stack. Each frame must also be aligned on an 8-pixel boundary.
|