|
Chapter 5 – Processing User Input
In this chapter, we will extend our program to read the input buttons.
We'll use this information to drive the planes, making them accelerate
and decelerate, climb and dive. We'll also add bombs that can be dropped
onto the canyon.
What you need for this chapter
1. The project created in Chapter 4
2. The bitmap containing the bomb images
The portion of the SGADE code that we'll be focusing on in this chapter
is the SoKeys module. SoKeys is used to read all of the buttons on the
GBA. In addition to providing their current up/down state, it can also
identify state transitions. There are only four functions provided by
the SoKeys module, so I'll list them all here:
SoKeysUpdate
- reads the current key state
SoKeysDown
- returns true if any of the requested keys are currently down
SoKeysPressed
- returns true if any of the requested keys were pressed (went from up
to down)
SoKeysReleased
- returns true if any of the requested keys were released (went from down
to up)
Using the SoKeys module is actually quite simple. You just call SoKeysUpdate
once per frame and then call the SoKeysDown, SoKeysPressed, or SoKeysReleased
wherever you need to read user input.
Vertical plane control (climb/dive)
Since there's a lot of code to be added in this chapter, we're going
to do it in phases. First, we'll add the input handler to drive the plane.
Once this is complete and is working, we'll add the bomb data and code.
The first part of the plane control is the use of the up and down arrow
keys to drive its altitude.
Add the following configuration #defines to the beginning of the bbadvance.c
file. The HEADING defines are used to limit the rotation of the plane
image and are given in 256ths of a circle.
// rotation values for sprite image
#define PLAYER_HEADING_UP30 -21
#define PLAYER_HEADING_LEVEL 0
#define PLAYER_HEADING_DOWN30 21
#define PLAYER_HEADING_INCREMENT 3
Add the following member fields to the playerInfo structure. The vOffset
field is used to keep track of how far away the plane is from its 'ideal'
position. The iHeading field keeps track of the current plane rotation
and will be used to to update the vertical portion of the offset value.
SoVector2 vOffset; // NOTE: this is in 28.4 fixed point
int iHeading;
With the defines and data values in place, we move on to the driving
code. Our first step is to modify the setupSprites function.
Insert a new line of code in the for(...MAX_PLAYERS...) loop right
after the position setting to initialize the plane's heading to a level
(non-rotated) value.
g_player[ii].iHeading = PLAYER_HEADING_LEVEL;
Next, we modify the update function to utilize the heading value to drive
the plane's vertical offset and to display an appropriately rotated image
for the plane.
Insert new code at the top of the for(...MAX_PLAYERS...) loop to update
the vertical offset based on the plane's heading. The extra code is
there to prevent the plane from climbing off the top of the screen or
diving into the canyon.
g_player[ii].vOffset.m_Y += g_player[ii].iHeading;
if(g_player[0].vOffset.m_Y <= -14<<4)
g_player[0].vOffset.m_Y = -14<<4;
if(g_player[0].vOffset.m_Y >= 37<<4)
g_player[0].vOffset.m_Y = 37<<4;
Modify the SoSpriteSetTranslate call to use the (fixed-point) offset
value. Then add a call to SoSpriteManagerSetRotationAndScale to set
the rotation to the current heading. Note that since we're treating
the heading as an absolute up/down value, we must negate it when our
plane is moving from right-to-left.
SoSpriteSetTranslate(g_player[ii].sprite,
g_player[ii].vPosition.m_X + (g_player[ii].vOffset.m_X>>4),
g_player[ii].vPosition.m_Y + (g_player[ii].vOffset.m_Y>>4));
SoSpriteManagerSetRotationAndScale(ii,
(g_player[ii].iDirection == DIRECTION_RIGHT) ?
g_player[ii].iHeading : -g_player[ii].iHeading,
SO_FIXED_FROM_WHOLE(1), SO_FIXED_FROM_WHOLE(1));
Finally, we add the actual code to read the input and update the heading
value.
Insert a new function called handleInput. This function will do all
of our input handling. It reads the input keys and then updates the
status of the plane based on the keys that are pressed.
void handleInput(void)
{
static int cntFrame = 0;
cntFrame += 1;
// update the GBA button status variables
SoKeysUpdate();
// ascend or descend - - - - - - - - - - - - - - - - - - - - - - - -
if(!(cntFrame & 0x3))
{
if(SoKeysDown(SO_KEY_UP) &&
(g_player[0].iHeading > PLAYER_HEADING_UP30))
g_player[0].iHeading -= PLAYER_HEADING_INCREMENT;
else if(SoKeysDown(SO_KEY_DOWN) &&
(g_player[0].iHeading < PLAYER_HEADING_DOWN30))
g_player[0].iHeading += PLAYER_HEADING_INCREMENT;
else
{
if(g_player[0].iHeading > 0)
g_player[0].iHeading -= PLAYER_HEADING_INCREMENT;
else if(g_player[0].iHeading < 0)
g_player[0].iHeading += PLAYER_HEADING_INCREMENT;
}
}
}
Add a call to the new handleInput function just before the call to
update in the AgbMain function.
handleInput();
Compile and test the program.
If you fly entirely within the altitude limits, the plane looks pretty
good. Unfortunately, when you hit the top and bottom limits, it looks
pretty bad. As soon as the plane hits the top/bottom limit it simply stops,
remaining fully banked but flying straight ahead. Our next block of code
will address this.
Add the following configuration #defines to the beginning of the bbadvance.c
file. These defines are used to determine the vertical range of the
plane flight.
#define PLAYER_Y_MAX 64
#define PLAYER_Y_MIN 16
Add the following code to the handleInput function immediately following
the code that sets the plane's iHeading value. I generally frown on
using magic numbers like this, but here we're more interested in results
than in having pretty code.
// smooth out top/bottom of range (so we don't "hit a wall")
// warning: magic numbers ahead!
{
int iPos = g_player[0].vPosition.m_Y +
(g_player[0].vOffset.m_Y>>4);
int iMin= PLAYER_HEADING_UP30;
int iMax= PLAYER_HEADING_DOWN30;
if(iPos <= PLAYER_Y_MIN) iMin = 0;
else if(iPos <= PLAYER_Y_MIN+2) iMin = -3;
else if(iPos <= PLAYER_Y_MIN+6) iMin = -6;
else if(iPos <= PLAYER_Y_MIN+10) iMin = -9;
else if(iPos <= PLAYER_Y_MIN+14) iMin = -12;
else if(iPos <= PLAYER_Y_MIN+19) iMin = -18;
if(iPos >= PLAYER_Y_MAX) iMax = 0;
else if(iPos >= PLAYER_Y_MAX-1) iMax = 3;
else if(iPos >= PLAYER_Y_MAX-5) iMax = 6;
else if(iPos >= PLAYER_Y_MAX-9) iMax = 9;
else if(iPos >= PLAYER_Y_MAX-13) iMax = 12;
else if(iPos >= PLAYER_Y_MAX-18) iMax = 18;
if(g_player[0].iHeading < iMin)
g_player[0].iHeading = iMin;
else if(g_player[0].iHeading > iMax)
g_player[0].iHeading = iMax;
}
Since the above code handily manages vertical limits, remove the following
code from update() that previously served this purpose:
g_player[ii].vOffset.m_Y += g_player[ii].iHeading;
if(g_player[0].vOffset.m_Y <= -14<<4)
g_player[0].vOffset.m_Y = -14<<4;
if(g_player[0].vOffset.m_Y >= 37<<4)
g_player[0].vOffset.m_Y = 37<<4;
Compile and test the program.
The plane's vertical movement at its extremes is now just as nice as
it is in the middle of the range. There is still one problem, however.
If you dive to the lower limit of the range while going right, you will
reappear within the canyon when you exit and reenter the screen going
left. Likewise, if you climb to the top when going left, you will reappear
off the top of the screen when you reenter going right. We'll fix this
with a simple hack. We just force the plane offset to 0 whenever it exits
the screen.
Add the following line of code in the update function just after the
line that initializes the plane's position when reentering the screen
(hint: the position code contains a reference to startPosition[]):
g_player[ii].vOffset.m_X = g_player[ii].vOffset.m_Y = 0;
Compile and test the program. The plane's vertical movement handling
is now complete.
Horizontal plane control (accelerate/decelerate)
The next part of the plane control is the use of the left and right arrow
keys to allow the plane to move across the screen a little faster or a
little slower.
Add the following configuration #defines to the beginning of the bbadvance.c
file. The OFFSET_X defines are used to limit how far away from the 'ideal'
position the plane is allowed to get.
#define PLAYER_OFFSET_X_MAX (16*16)
#define PLAYER_OFFSET_X_MIN (-16*16)
Add the following member field to the playerInfo structure. The iSpeed
field tracks how quickly the horizontal portion of the offset value
is changing.
int iSpeed; // NOTE: this is in 28.4 fixed point
Once again, we modify the update function, this time to utilize the new
iSpeed value to drive the plane's horizontal offset.
Insert new code at the top of the for(...MAX_PLAYERS...) loop to update
the horizontal offset based on the plane's speed. The extra code is
there to prevent the player from constantly breaking to get an aiming
advantage.
g_player[ii].vOffset.m_X += g_player[ii].iSpeed;
if(g_player[ii].vOffset.m_X < PLAYER_OFFSET_X_MIN)
g_player[ii].vOffset.m_X = PLAYER_OFFSET_X_MIN;
if(g_player[ii].vOffset.m_X > PLAYER_OFFSET_X_MAX)
g_player[ii].vOffset.m_X = PLAYER_OFFSET_X_MAX;
Modify the off-screen test to take the horizontal offset into account.
This 'if' test replaces the old code that just compared the plane position
against the PLAYER_POSITION_MAX and PLAYER_POSITION_MIN #defines. Without
this code in place, there could either be large delays after the plane
goes off the edge or the plane may even vanish before it hits the edge.
if(((g_player[ii].vPosition.m_X+(g_player[ii].vOffset.m_X>>4))
< PLAYER_POSITION_MIN) || ((g_player[ii].vPosition.m_X+
(g_player[ii].vOffset.m_X>>4)) > PLAYER_POSITION_MAX))
Finally, add we add the code to actually read the left/right input keys
and update the speed value.
Insert the following code in the handleInput function after the call
to SoKeysUpdate.
// advance or retreat - - - - - - - - - - - - - - - - - - - - - - - -
if(SoKeysDown(SO_KEY_LEFT))
{
if(g_player[0].iSpeed > -8)
g_player[0].iSpeed -= 2;
}
else if(SoKeysDown(SO_KEY_RIGHT))
{
if(g_player[0].iSpeed < 8)
g_player[0].iSpeed += 2;
}
else
{
// force gradual return to center
if(g_player[0].vOffset.m_X > 0)
g_player[0].iSpeed = -2;
else if(g_player[0].vOffset.m_X < 0)
g_player[0].iSpeed = 2;
else
g_player[0].iSpeed = 0; // idle state (centered)
}
Compile and test the program.
When you push on the left and right arrow keys you should see a short
acceleration or deceleration of your plane. If you accelerate across multiple
passes, you will eventually catch up with the other plane.

Bomb activation and control
The
final control we'll add in this chapter is the ability to drop bombs and
super bombs. For the bomb images, we'll be using the last of the "Programmer-Art"
from the PC version of Boulder Bombers. The bomb graphic can be seen on
the right edge of this page. The top four images represent a 'normal'
bomb, while the bottom four represent a 'super' bomb.
Before we can use the image in the project, we'll once again we have
to convert it into code. We'll use the same trick we used last chapter
to get an eight frame animation.
Put the bombs.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. Since the graphic was created
using the same palette as the planes in the last chapter, there is no
need to generate another palette.
% SoConverter.exe -file bombs.bmp -converter SoSpriteAnimation
Invalid value for the -converter argument
Please wait while converting....
Done
Press almost any key to continue...
Now, edit the bombsSpriteAnimation.dat file to correct the size and
number of frames. In this case, there are 8 frames, each of which is
8 pixels wide by 8 pixels tall.
const SoSpriteAnimation bombsSpriteAnimation = { false, 8, 8, 8,
(u8*) bombsSpriteAnimationData };
Now that our bomb graphics are ready, we'll incorporate it into bbadvance.c
and add the #defines, structures and variables that reference it. Open
the bbadvance.c file in your editor and make the following changes:
Insert the following #include statement to the "constant data"
section:
#include "bombsSpriteAnimation.dat"
Add the following lines to "Configuration #defines" section
at the top of the source file. MAX_BOMBS specifies the maximum number
of bombs we're going to track. 32 is really overkill for this, but we
don't want to accidently run out. The BOMB_TYPEs are used together with
the TYPE_OFFSET as indexes into the bomb graphic above.
#define MAX_BOMBS 32
#define BOMB_TYPE_NORMAL 0
#define BOMB_TYPE_SUPER 1
#define BOMB_SPRITE_TYPE_OFFSET 4
Add the following structure definition to the "Data Structures"
section. This defines the data necessary to represent a single bomb
sprite.
struct bombInfo {
SoSprite *sprite;
int iType;
int iPlayer;
int iDirection;
SoVector2 vPosition; // NOTE: this is in 28.4 fixed point
SoVector2 vSpeed; // NOTE: this is in 28.4 fixed point
bool bActive;
int iAnimFrame;
};
Add the following global variable declarations to the "Global
Variables section.
struct bombInfo g_bombs[MAX_BOMBS];
u32 g_iBombBaseSpr;
Now, we add the actual code to drive the bombs.
Add the following initialization code to the end of the setupSprites
function, just before the call to SoSpriteManagerEnableSprites. This
code is very similar to the code we already have to initialize the plane
sprites, but with one notable difference. This one doesn't enable the
sprite by calling SoSpriteSetRotationAndScaleEnable. Since the bombs
are usually not visible, the job of enabling them is left to the bomb
drop subroutine.
// load bomb sprite data
g_iBombBaseSpr = SoSpriteMemManagerLoad(&bombsSpriteAnimation);
for(ii = 0; ii < MAX_BOMBS; ++ii)
{
g_bombs[ii].sprite = SoSpriteManagerRequestSprite();
SoSpriteCopyPropertiesFromAnimation(g_bombs[ii].sprite,
&bombsSpriteAnimation);
// initialize all bombs offscreen
killBomb(ii);
}
You'll note that the previous block of code ended with a call to killBomb.
Add this function right before the setupSprites function. The SoSpriteDisable
function informs the GBA hardware to stop display of the sprite.
// remove bomb (for any reason)
void killBomb(int iBomb)
{
g_bombs[iBomb].bActive = false;
SoSpriteDisable(g_bombs[iBomb].sprite);
}
Next, add the companion function dropBomb. The dropBomb function locates
an available bomb entry in the global g_bombs array, initializes all
of the entry contents, and then enables the bomb sprite. This last item
is done using a SGADE function you might not have expected: SoSpriteSetSizeDoubleEnable.
The reason that this works (and why there isn't a SoSpriteEnable function)
is because the GBA hardware is built so that a sprite with disabled
rotation/scaling and enabled double-size is itself disabled. Thus, a
sprite may be enabled by either enabling rotating/scaling or by disabling
double-size. Since the bombs won't be doing any rotating, we opt for
the latter solution.
void dropBomb(int iPlayer, int iType)
{
int ii;
// find an empty slot
for(ii = 0; ii < MAX_BOMBS; ++ii)
{
if(!g_bombs[ii].bActive)
break;
}
if(ii >= MAX_BOMBS)
{
SO_ASSERT(0, "No bombs available!");
return;
}
// set initial bomb position & speed
g_bombs[ii].iType = iType;
g_bombs[ii].iPlayer = iPlayer;
g_bombs[ii].iDirection = g_player[iPlayer].iDirection;
// start bomb at front end of plane
g_bombs[ii].vPosition.m_X = ((g_player[iPlayer].vPosition.m_X +
((g_player[iPlayer].iDirection == DIRECTION_LEFT) ? 1 :
SoSpriteGetWidth(g_player[iPlayer].sprite) -
SoSpriteGetWidth(g_bombs[ii].sprite))) << 4) +
g_player[iPlayer].vOffset.m_X;
g_bombs[ii].vPosition.m_Y = ((g_player[iPlayer].vPosition.m_Y + 10)
<< 4) + g_player[iPlayer].vOffset.m_Y;
// start with same speed as plane
g_bombs[ii].vSpeed.m_X = g_player[0].iSpeed +
((g_player[0].iDirection == DIRECTION_RIGHT) ? 16 : -16);
// start drop speed with extra 1/2 pixel per frame
g_bombs[ii].vSpeed.m_Y = 8+g_player[0].iHeading;
g_bombs[ii].iAnimFrame = 0;
g_bombs[ii].bActive = true;
SoSpriteSetSizeDoubleEnable(g_bombs[ii].sprite, false);
}
We're getting close now. Next we add the code to handle the bomb movement.
Add the following code to the end of the update function, right before
the SoSpriteManagerUpdate call.
// update bomb positions
for(ii = 0; ii < MAX_BOMBS; ++ii)
{
if(!g_bombs[ii].bActive)
continue;
g_bombs[ii].vPosition.m_X += g_bombs[ii].vSpeed.m_X;
g_bombs[ii].vPosition.m_Y += g_bombs[ii].vSpeed.m_Y;
// apply accel/decel
if(!(animFrame & 0x03))
{
if(g_bombs[ii].vSpeed.m_X > 0)
--g_bombs[ii].vSpeed.m_X;
else if(g_bombs[ii].vSpeed.m_X < 0)
++g_bombs[ii].vSpeed.m_X;
}
g_bombs[ii].vSpeed.m_Y += 1;
// check if bomb went off screen
if((g_bombs[ii].vPosition.m_X < 0) ||
(g_bombs[ii].vPosition.m_X >= (SO_SCREEN_WIDTH<<4)) ||
(g_bombs[ii].vPosition.m_Y >= (SO_SCREEN_HEIGHT<<4)))
{
killBomb(ii);
continue;
}
g_bombs[ii].iAnimFrame += cntFrame&1;
if(g_bombs[ii].iAnimFrame >= BOMB_SPRITE_TYPE_OFFSET)
g_bombs[ii].iAnimFrame = 0;
// move bomb sprite
SoSpriteSetTranslate(g_bombs[ii].sprite,
g_bombs[ii].vPosition.m_X>>4, g_bombs[ii].vPosition.m_Y>>4);
SoSpriteSetAnimationIndex(g_bombs[ii].sprite,
g_iBombBaseSpr + (g_bombs[ii].iAnimFrame +
(g_bombs[ii].iType*BOMB_SPRITE_TYPE_OFFSET))
*SoSpriteAnimationGetNumIndicesPerFrame(&bombsSpriteAnimation));
}
Finally, we add the code that drops the bombs in response to the user
pressing a key. Add the following code to the bottom of the handleInput
function.
// drop bombs - - - - - - - - - - - - - - - - - - - - - - - - - - - -
if(SoKeysPressed(SO_KEY_A)) // drop a bomb (if available)
dropBomb(0, BOMB_TYPE_NORMAL);
if(SoKeysPressed(SO_KEY_B)) // drop a super-bomb (if available)
dropBomb(0, BOMB_TYPE_SUPER);
That's it! Now just save the file.
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. Try out all of the input features together.
click here to download the solution
to this chapter
|