SGADE Tutorial – Boulder Bombers Advance

by Mark T. Price
Chief Scientist, Sudden Presence

 
 
 
 Previous Chapter Index Next Chapter 

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

 Previous Chapter Index Next Chapter