Chapter 7

Basic Game Mechanics Applied to the Space Shooter Genre

The Importance of Object Oriented Programming

Object oriented programming (OOP) is essential to making good games on a modern system. Although it is very much possible without object oriented programming, OOP is an incredible tool that greatly improves code reusability, readability, modularization, and abstraction. It makes the programmer's job a lot easier. Also, due to modularization, collaborating on projects with your friends or coworkers is easily ten fold easier.

The Ship Class

The first thing we'll make is a Ship class. This class will encapsulate all the properties and functionality of any ship in an easy to use and understand format. Think of things a ship can do, on a high level. What should come to mind is the ability to turn both ways, shoot weapons, accelerate, move at a given velocity (coasting), and maybe some more things if you are creative enough. What properties of a ship can you come up with? Perhaps turning speed, thrust, mass, maximum speed, velocity, position, shields? Well, after you are done brainstorming, the next step is to write out the functionality and properties we need to put into our Ship class. You could make a table, as in Table 7.1, “Table of Ship properties and functionality”, or draw some diagrams on a piece of paper. Either way, you want to make sure your ideas all get onto some physical paper before you begin coding.

Table of Ship properties and functionality

PropertiesFunctionality
shipHeightaccelerate
shipWidthmoveShip
positionturnClockwise
velocityturnCounterClockwise
anglegetPosition
turnSpeedreverseTurn
thrustgetAngle
maxSpeed
mass

Making the Ship Class

I have provided a skeleton framework file for you to write your class in. It is all set and ready for you to implement in the ship.cpp file. The header file, ship.h is also included. On your own, with your own classes in the future, you should always make a skeleton framework class to work from. It makes implementation straightforward and you do not have to worry about the semantics of setting up a class so much.

The Constructor

I have provided you with a simple constructor and private initialization method method. These are often mundane things to make. In fact, the compiler will automatically make the constructor, copy constructor, and operator= methods for you if you don't explicitly make them. Feel free to modify the default values in the initializers to try out different effects of changing the ship properties.

Acceleration

Acceleration is probably one of the most important things your ships can do. To accelerate, we simply increase our velocity by a certain increment, that being the thrust capability of the ship, in the angle we are headed. Here is where some simple trigonometry comes into play. Since our velocity is stored as a two dimensional vector (x and y component), we have to shadow our thrust vector onto each direction. We do this we multiply the thrust by sin(angle) for our x component, and by -cos(angle) for the y direction. Next, after we have computed the increment for both x and y, we add them onto our current velocity, making sure we don’t go over the ship's maximum speed.

void Ship::accelerate() {
  float incX = thrust * sin(angle);
  float incY = -(thrust * cos(angle));

  // the following method of speed limitation is not accurate, traveling
  // diagonally is faster than straight, which is not the desired limitation
  // a more accurate method is needed at a later time

  velocity.x += incX;
  // make sure can't go too fast in x direction
  if (velocity.x > maxSpeed) {
    velocity.x = maxSpeed;
  }
  if (velocity.x < -maxSpeed) {
    velocity.x = -maxSpeed;
  }

  velocity.y += incY;
  // make sure can't go too fast in y direction
  if (velocity.y > maxSpeed) {
    velocity.y = maxSpeed;
  }
  if (velocity.y < -maxSpeed) {
    velocity.y = -maxSpeed;
  }
}

Moving the Ship

This one is incredibly easy thanks to the Nintendo DS hardware. All we have to do is increment our position by our velocity. The hardware takes care of any wrapping or offscreen issues.

void Ship::moveShip() {
  // move the ship
  position.x += velocity.x;
  position.y += velocity.y;

  // hw does wrap around for us, so we don't have to have any of that sort of
  // logic in here
}

Reversing the Ship's Direction

This one took me a while to figure out, even though it's just one line, but it's very useful. We can turn the ship around, not a 180 per se, but simply pointing into the opposite direction of our current velocity. This will get the angle of our velocity with respect to 0 degrees, and then will do a 180 from that angle.

void Ship::reverseTurn() { angle = (2 * PI) - atan2(velocity.x, velocity.y); }

Rotating the Ship

Rotating the ship is also quite simple. We just increment or by ship’s turning speed depending on which direction we wish to turn. Note that we are storing the angle of the ship as a clockwise rotation. This will be important to remember for later when we update the ship sprite.

void Ship::turnClockwise() { angle += turnSpeed; }
void Ship::turnCounterClockwise() { angle -= turnSpeed; }

Getting the Ship's Position

Return the ship’s position.

MathVector2D<float> Ship::getPosition() { return position; }

Getting the Ship's Angle

This one is a bit more tricky and involved. I suppose I should start by explaining that a Nintendo DS circle has 32768 degrees. It doesn't actually have 32768 degrees, nor does a Nintendo DS even know what a circle is, but it is easy to understand the hardware a bit better when we think of it this way. I will say, however, that the reason for the 32768 degrees is due to libnds's built-in look up tables for the sin and cos functions. Having only 360 entries in your lookup table would be a waste of space when it takes just as many bits to index into a 360 entry table as it does a 512 entry one. libnds gives us more accuracy by presenting a 32768 entry one. More entries allow finer accuracy. In order for the Nintendo DS to know how to rotate our sprites, we have to convert the internally stored radian angle value to a value within the 32768 degree system. This is an easy conversion.

The first step is to convert to a 360 degree system, as you must have learned in junior high school. This is done by multiplying the radian value by 180/PI. The 180 part is half the number of degrees in a circle. So, in a 32768 degree system we can convert by multiplying the radian value by DEGREES_IN_A_CIRCLE/(2 * PI). Lastly, just return that value as an integer. (The hardware does not have any floating point, so when rotating our sprites, we use a fixed point value disguised as an ordinary integer.)

Then, we make a function to return a converted angle value, for whenever we need it.

int Ship::radToDeg(float rad) {
  return (int)(rad * (DEGREES_IN_A_CIRCLE / (2 * PI)));
}
int Ship::getAngleDeg() { return radToDeg(angle); }

Linking the Ship into our Program

We now need to create an instance of the ship in our main function. Creating an instance of a class, known as an object, is quite simple, as you can see below. We just have to create the Ship object and then assign a SpriteEntry to it.

We should also do something nifty with our new class so that we can verify that what we wrote is working. Let's make the ship move around on its own by telling the ship to trust ten times.

int main() {
  /*  Turn on the 2D graphics core. */
  powerOn(POWER_ALL_2D);

  /*
   *  Configure the VRAM and background control registers.
   *
   *  Place the main screen on the bottom physical screen. Then arrange the
   *  VRAM banks. Next, confiure the background control registers.
   */
  lcdMainOnBottom();
  initVideo();
  initBackgrounds();

  /* Set up a few sprites. */
  SpriteInfo spriteInfo[SPRITE_COUNT];
  OAMTable *oam = new OAMTable();
  iniOAMTable(oam);
  initSprites(oam, spriteInfo);

  /* Display the backgrounds. */
  displayStarField();
  displayPlanet();
  displaySplash();

  /* Make the ship object */
  static const int SHUTTLE_OAM_ID = 0;
  SpriteEntry *shipEntry = &oam->oamBuffer[SHUTTLE_OAM_ID];
  SpriteRotation *shipRotation = &oam->matrixBuffer[SHUTTLE_OAM_ID];
  Ship *ship = new Ship(&spriteInfo[SHUTTLE_OAM_ID]);

  /* Accelerate the ship for a little while to make it move. */
  for (int i = 0; i < 10; i++) {
    ship->accelerate();
  }

  /*
   *  Update the OAM.
   *
   *  We have to copy our copy of OAM data into the actual
   *  OAM during VBlank (writes to it are locked during
   *  other times).
   */
  swiWaitForVBlank();
  updateOAM(oam);

  /* Loop forever so that the Nintendo DS doesn't reboot upon program
   * completion. */
  for (;;)
    ;

  return 0;
}

Creating the Main Game Loop

The previous code isn't very exciting, since we never update the OAM more than once. We need to begin the creation of what is referred to as the game loop. We won't be fully implementing it in this chapter, since a major component of it will be missing until we discover input on the Nintendo DS.

The game loop has at least three major components. The first thing any game loop should do is to collect input from the outside world. We won't be doing that in this chapter, however. The next component of the game loop is updating the game state. Based on inputs the game received in the previous frame (to the one we'll render next) and the passing of time, the game state will change (if anything interesting is happening). The final component of the game loop is the rendering component. In our case, we have to update the OAM to let it know of the changes that occurred in the game state and that it needs to reflect those changes.

Now that we know what a game loop is, it's time for us to start creating one to run our program. The first thing we want to happen in our game loop is for the game state to be updated. This is because we don't have any input to collect yet. We tell our ship to move at it's current velocity. This will change the ship’s position. Then we update the sprite attributes with new information about our ship, as some properties of the ship have now changed (i.e. its angle and position). Note that the rotateSprite() function performs a counter-clockwise rotation and our ship tracks its angle as a clockwise rotation; this is resolved by simply negating the angle to rotate by. Finally, we call a function that will make sure our program does not exceed 60fps (speed of the graphics on the Nintendo DS) by waiting for VBlank, and then we update the OAM, telling it that we changed some attributes on the sprites and it needs to handle that.

  for (;;) {
    /* Update the game state. */
    ship->moveShip();

    /* Update sprite attributes. */
    MathVector2D<float> position = ship->getPosition();
    shipEntry->x = (int)position.x;
    shipEntry->y = (int)position.y;
    rotateSprite(shipRotation, -ship->getAngleDeg());

    /*
     *  Update the OAM.
     *
     *  We have to copy our copy of OAM data into the actual OAM during
     *  VBlank (writes to it are locked during other times).
     */
    swiWaitForVBlank();
    updateOAM(oam);
  }

  return 0;
}

The OAM really shines through here. The all powerful Nintendo DS hardware, an incredible masterpiece, will rotate and move our ship with very little effort on our part. In hindsight, all we have done is flip a few bits in a few registers in a structured manner, and our ship comes to life. Incredible.

Compiling

Verify that you are including all the files you need to include now, before compiling.

#include <assert.h>
#include <nds.h>

#include "ship.h"
#include "sprites.h"

/* Backgrounds */
#include "planet.h"
#include "splash.h"
#include "starField.h"
/* Sprites */
#include "moon.h"
#include "orangeShuttle.h"

Everything should compile for you fine at this point if you wish to play around with your new class. However, in the next chapter we will cover how to get Nintendo DS input to affect the Ship. Be ready for it, we're going to have some major fun.

Orange Shuttle