In the previous post of the #DIYConsole series, we started to lay the foundations for making a simple shoot-em-up game. Our goal is to make a game where the player must destroy/dodge asteroids with his starship to get points; during the game, the speed and number of asteroids will increase to rise the level of the challenge.

We have already wrote the code to draw and move the starship. Also, we added an animated background to make the game graphically more pleasing. In this post we will build upon the previously written code and gradually add everything we need to complete the game.

Structure of the game loop

Before starting to write code, let’s see what the structure of our game loop will be:

  1. Frame setup and input reading: at the beginning of the frame, we make some necessary calculations, such as the delta time from the previous frame and score updating. In addition, the touch screen input is read.
  2. Erase game screen: all game objects are erased, overwriting them with the background color. We don’t erase the whole screen because the graphics library is optimized to have the best performance when the modified screen area is minimal.
  3. Process input: here the touch screen input is processed. During the actual game, the input is used to calculate the new position for the starship.
  4. Update objects and check collisions: in this section we calculate the new position of all game objects and check if there are collisions between them (for example if the starship hits an asteroid)
  5. Redraw game screen: all objects are redrawn in their updated position.

Whenever I will add a new piece of code, I will point out in which section you will have to insert it.

Initial code refactoring

In what we have developed so far, we have two types of objects: starship and stars. For each of them, we have declared a structure to store their data:

  • for the starship, we store its position;
  • for a star, we store its position and its color (used only for single pixel stars).

Going forward in development, we will add other types of objects to the game:

  • asteroids: we need to store their position and if they are still valid objects (they may have been destroyed or be off screen);
  • bullets: same data as asteroids;
  • explosions: if the asteroids or the starship are destroyed, an explosion appears on the screen; in addition to position and validity, we will need to store other data (that we will se later).

To avoid declaring a structure for each type of object, we will use a single structure for all types:

struct GameObject {
  float x;
  float y;
  bool valid;
  uint8_t color;
};

So let’s redefine the variables starship, farStar and nearStar as GameObjects:

GameObject starship;
GameObject farStar[FARSTAR_COUNT];
GameObject nearStar[NEARSTAR_COUNT];

Game states

Our game can be in three states:

  • Start Screen: the initial state of the game; in this state, when the player touches the screen, the game goes into Running mode;
  • Running: the actual game;
  • Game Over: when the starship is destroyed, the game shows a screen with the player’s final score and a button to start a new game.

We will store the game state in an enumeration:

enum GameState {
  StartScreen,
  Running,
  GameOver
};

...
	
GameState gameState = StartScreen;

We want to draw the starship only when the game is in the Running state, so we must remove this code from the setup function:

// Draw starship
gfx.drawTransparentBitmap(starshipBitmap, starship.x-16, starship.y-16, 32, 32, 15);

and still in setup, add at the beginning:

starship.valid = false;

To comply with the game loop structure that we saw above, the current input process code must be moved between the Erase game screen section and the Update objects section. Also, what to do with the input depends on the game state, so let’s rewrite the code like this:

if(screenTouched) {
  switch(gameState) {
    case StartScreen:
    {
      // Start a new game
      gameState = Running;
      starship.valid = true;
      break;
    }

    case Running:
    {
      // Map from touchscreen coordinates to screen coordinates
      int tx = map(touchPoint.x, TS_MINX, TS_MAXX, 0, 320);
      int ty = map(touchPoint.y, TS_MINY, TS_MAXY, 0, 480);
      // Ignore touches outside input area
      if(ty > 320) {
        // Set target point for starship movement
        int xTarget = map(tx, 30, 280, 16, 304);
        int yTarget = map(ty, 320, 460, 144, 304);
        xTarget = min(max(xTarget, 16), 304);
        yTarget = min(max(yTarget, 144), 304);
        // Move starship a step towards the target
        starship.x += 24.0 * ((float)xTarget - starship.x) * deltaTime;
        starship.y += 24.0 * ((float)yTarget - starship.y) * deltaTime;
      }
      break;
    }

    case GameOver:
    {
      //TODO
      break;
    }
  }
}

In the Redraw game screen section of the game loop we only want to draw the starship if it is a valid object, so we modify the code as follows:

// Draw starship
if(starship.valid)
  gfx.drawTransparentBitmap(starshipBitmap, starship.x-16, starship.y-16, 32, 32, 15);

Similarly, in the Erase game screen section we must erase the starship only if it’s a valid object:

// Erase starship
if(starship.valid)
  gfx.drawFilledRectangle(starship.x-16, starship.y-16, 32, 32, 15);

If you try now to compile and upload the app, you will see that at first the starship does not appear. But as soon as you touch any point on the screen, the game goes into the Running state and the starship is drawn on the screen.

Game text and UI

To make it clear that the player must touch the screen to start the game, let’s add to the initial screen the string “TOUCH TO START”. First of all, we need to copy the DefaultFont.h file we created in post #DIYConsole – Part 6 and include it in our project:

#include "DefaultFont.h"

Then in the setup function add, after gfx.begin():

gfx.setFont(&defaultFont);

Then, in the Redraw game screen section, right after the code that redraws the starship, we must add the code that will draw of all texts in the game and of the user interface:

// Draw strings and buttons
switch (gameState) {
  case StartScreen:
    gfx.drawString2x(48, 144, "TOUCH TO START", 0);
    break;
		
  case Running:
    //TODO
    break;

  case GameOver:
    //TODO
    break;
}

When the game starts we have to delete this string. So, in the Input process section, when we process the input for the StartScreen state, we need to add the line:

// Erase start string
gfx.drawFilledRectangle(48, 144, 224, 32, 15);

Asteroids!

Next step is adding the asteroids to the game. They will appear at the top of the screen and move downward to simulate the forward movement of the starship.

To store asteroids position and status, we need to create an array of GameObjects:

#define MAX_ASTEROIDS   20
GameObject asteroid[MAX_ASTEROIDS];

We want to make asteroids appear at regular intervals, so we need a variable that stores the moment when the last asteroid was created. In addition, we will use two other variables:

  • a variable for storing the period between the creation of an asteroid and the next one;
  • a variable for storing the current speed of asteroids.
unsigned long lastAsteroidTime = 0;
float asteroidPeriod = 500000;
float asteroidSpeed = 70;

By progressively decreasing the creation period and increasing the speed of the asteroids, we will raise the difficulty level of the game over time.

Now let’s write the function that creates a new asteroid inside the array:

bool createAsteroid(float x, float y) {
  for(int i=0; i<MAX_ASTEROIDS; i++) {
    if(!asteroid[i].valid) {
      asteroid[i].valid = true;
      asteroid[i].x = x;
      asteroid[i].y = y;
      return true;
    }
  }
  return false;
}

In the game loop, in the Frame setup and input reading section, we will add the code that manages the creation of asteroids. The asteroids will be positioned just above the screen, in a random x position and only if the game is in the Running state:

// Things to do only when game is running
if(gameState == Running) {
  // Create new asteroid?
  if((now - lastAsteroidTime) >= asteroidPeriod) {
    if(createAsteroid(esp_random() % 320, -32))
    lastAsteroidTime = now;
  }
}

In the Update objects and check collisions section we must add the code that recalculates the position of the asteroids at each frame:

// Update asteroids position
for(int i=0; i<MAX_ASTEROIDS; i++) {
  if(asteroid[i].valid) {
    asteroid[i].y += asteroidSpeed * deltaTime;
    if(asteroid[i].y > 336) {
      asteroid[i].valid = false;
    }
  }
}

You can see that if an asteroid ends up beyond the game screen, it is set as invalid and so its element in the array is available again for later use.

At this point, to complete the asteroid code, all we need to do is to draw asteroids on screen. First of all, we need a bitmap. In the bitmaps.h file, add the following code:

// Asteroid 32x32 pixels
uint8_t asteroidBitmap[1024] = {
	15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
	15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 13, 13, 13, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
	15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 13, 13, 13, 13, 13, 13, 13, 13, 13, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
	15, 15, 15, 15, 15, 15, 15, 15, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
	15, 15, 15, 15, 15, 15, 15, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 13, 13, 13, 15, 15, 15, 15, 15, 15, 15, 15,
	15, 15, 15, 15, 15, 15, 15, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 13, 13, 13, 15, 15, 15, 15, 15, 15, 15,
	15, 15, 15, 15, 15, 15, 13, 13, 14, 14, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 13, 13, 13, 13, 15, 15, 15, 15, 15, 15,
	15, 15, 15, 15, 15, 13, 13, 14, 14, 14, 14, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 13, 13, 13, 13, 15, 15, 15, 15, 15, 15,
	15, 15, 15, 15, 13, 13, 13, 14, 14, 14, 14, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 13, 13, 13, 13, 13, 13, 15, 15, 15, 15, 15,
	15, 15, 15, 15, 13, 13, 13, 13, 14, 14, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 13, 13, 15, 15, 15, 15,
	15, 15, 15, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 13, 13, 15, 15, 15, 15,
	15, 15, 15, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 13, 13, 13, 15, 15, 15, 15,
	15, 15, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 15, 15, 15,
	15, 15, 13, 13, 13, 13, 13, 13, 13, 14, 14, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 13, 13, 13, 13, 13, 13, 15, 15, 15,
	15, 15, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 13, 13, 13, 13, 13, 13, 15, 15,
	15, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 13, 13, 13, 13, 13, 13, 13, 15,
	15, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 14, 13, 13, 13, 13, 13, 13, 15,
	15, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 14, 13, 13, 13, 13, 13, 13, 15,
	15, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 13, 13, 13, 13, 13, 13, 15, 15,
	15, 15, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 15, 15,
	15, 15, 13, 13, 13, 13, 13, 14, 14, 14, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 15, 15, 15,
	15, 15, 13, 13, 13, 13, 14, 14, 14, 14, 14, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 15, 15, 15,
	15, 15, 15, 13, 13, 13, 14, 14, 14, 14, 14, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 15, 15, 15, 15,
	15, 15, 15, 13, 13, 13, 13, 14, 14, 14, 14, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 15, 15, 15, 15, 15,
	15, 15, 15, 15, 13, 13, 13, 14, 14, 14, 13, 13, 13, 13, 13, 14, 14, 13, 13, 13, 13, 13, 13, 13, 13, 13, 15, 15, 15, 15, 15, 15,
	15, 15, 15, 15, 15, 13, 13, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 13, 13, 13, 13, 13, 13, 13, 13, 15, 15, 15, 15, 15, 15,
	15, 15, 15, 15, 15, 15, 15, 13, 13, 13, 13, 13, 13, 13, 14, 14, 14, 14, 13, 13, 13, 13, 13, 13, 13, 15, 15, 15, 15, 15, 15, 15,
	15, 15, 15, 15, 15, 15, 15, 15, 13, 13, 13, 13, 13, 13, 13, 14, 14, 13, 13, 13, 13, 13, 13, 13, 15, 15, 15, 15, 15, 15, 15, 15,
	15, 15, 15, 15, 15, 15, 15, 15, 15, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 15, 15, 15, 15, 15, 15, 15, 15, 15,
	15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 13, 13, 13, 13, 13, 13, 13, 13, 13, 13, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
	15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 13, 13, 13, 13, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15,
	15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15, 15
};

Back in main.cpp, in the Erase game screen section, we need to add the code to erase asteroids:

// Erase asteroids
for(int i=0; i<MAX_ASTEROIDS; i++) {
  if(asteroid[i].valid) {
    int height = 336 - asteroid[i].y;
    if(height > 32)
      height = 32;
    gfx.drawFilledRectangle(asteroid[i].x-16, asteroid[i].y-16, 32, height, 15);
  }
}

and in the Redraw game screen section, immediately after drawing the stars, the code to draw asteroids:

// Draw asteroids
for(int i=0; i<MAX_ASTEROIDS; i++) {
  if(asteroid[i].valid) {
    int height = 336 - asteroid[i].y;
    if(height > 32)
      height = 32;
    gfx.drawTransparentBitmap(asteroidBitmap, asteroid[i].x-16, asteroid[i].y-16, 32, height, 0);
  }
}

Starship and asteroids collision

At this point, you will see asteroids appearing from the top of the screen and moving downwards until they disappear. However, you can still move through the asteroids without any consequences.

We must therefore implement collisions between the starship and the asteroids. When the starship hits an asteroid, both are destroyed and the game ends, switching to GameOver state.

A simple way to calculate collisions is to use the distance between the two objects: if this distance is less than a certain threshold, the two objects are considered in contact.

To save the square root calculation time, we will use a function that calculates the square of the distance:

float distanceSquared(float x1, float y1, float x2, float y2) {
  return (x1-x2)*(x1-x2)+(y1-y2)*(y1-y2);
}

To implement starship-to-asteroid collision check, add this code to the Update objects and check collisions section:

// Check collision of starship with asteroids
if(starship.valid) {
  for(int i=0; i<MAX_ASTEROIDS; i++) {
    if(asteroid[i].valid) {
      if(distanceSquared(starship.x, starship.y, asteroid[i].x, asteroid[i].y) < 900) {
        asteroid[i].valid = false;
        gameState = GameOver;
        starship.valid = false;
	break;
      }
    }
  }
}

Game over

When the player’s starship is destroyed, the game ends and the Game over screen is shown. This screen displays the player’s final score and a button to start a new game.

To show the text “GAME OVER” and the player’s score, change the UI code as follows:

// Draw strings and buttons
switch (gameState) {
  case StartScreen:
    gfx.drawString2x(48, 144, "TOUCH TO START", 0);
    break;
    
  case Running:
    //TODO
    break;

  case GameOver:
    gfx.drawString2x(88, 80, "GAME OVER", 0);
    gfx.drawFilledRectangle(50, 170, 220, 64, 9);
    gfx.drawRectangle(50, 170, 220, 64, 8);
    gfx.drawString2x(80, 186, "PLAY AGAIN", 0);
    break;
}

In the Process input section we must respond to the click of the “play again” button:

...

case GameOver:
{
  // Map from touchscreen coordinates to screen coordinates
  int tx = map(touchPoint.x, TS_MINX, TS_MAXX, 0, 320);
  int ty = map(touchPoint.y, TS_MINY, TS_MAXY, 0, 480);
  if((tx >= 50) && (tx <= 270) && (ty >= 170) && (ty <= 234)) {
    // Play again clicked, restore initial value
    starship.x = 160;
    starship.y = 230;
    starship.valid = true;
    gameState = Running;

    // Erase play again button and game over string
    gfx.drawFilledRectangle(50, 60, 240, 174, 15);
  }
  break;
}

...

Bullets

Since we want our starship to be able to destroy the asteroids, we will equip it with a cannon that automatically fires bullets at regular intervals. This solution is due to the fact that this screen has a resistive touch, so we can only detect one touch at a time. Since we already use it for movement, unfortunately we cannot add a manual fire button.

The behavior of bullets is very similar to that of asteroids and as a result the code is also very similar. First, create the bullets array and the function to create a new bullet:

#define MAX_BULLETS   5
GameObject bullet[MAX_BULLETS];
unsigned long lastFireTime = 0;
unsigned long firePeriod = 1000000;

...

bool createBullet(float x, float y) {
  for(int i=0; i<MAX_BULLETS; i++) {
    if(!bullet[i].valid) {
      bullet[i].valid = true;
      bullet[i].x = x;
      bullet[i].y = y;
      return true;
    }
  }
  return false;
}

Then in the game loop, in the Frame setup and input reading section, next to the asteroid creation code, add:

// Fire a new bullet?
if((now - lastFireTime) >= firePeriod) {
  if(createBullet(starship.x, starship.y - 16))
    lastFireTime = now;
}

then add the update bullets position code:

// Update bullets position
for(int i=0; i<MAX_BULLETS; i++) {
  if(bullet[i].valid) {
    bullet[i].y -= 200 * deltaTime;
    if(bullet[i].y < 0)
      bullet[i].valid = false;
  }
}

To draw a bullet, we need a bitmap. In the bitmaps.h file add this code:

// Bullet 5x5 pixels
uint8_t bulletBitmap[25] = {
  0, 9, 9, 9, 0,
  9, 8, 8, 8, 9,
  9, 8, 1, 8, 9,
  9, 8, 8, 8, 9,
  0, 9, 9, 9, 0
};

Finally add the code to erase and redraw bullets:

// Erase bullets
for(int i=0; i<MAX_BULLETS; i++) {
  if(bullet[i].valid)
    gfx.drawFilledRectangle(bullet[i].x-2, bullet[i].y-2, 5, 5, 15);
}

...

// Draw bullets
for(int i=0; i<MAX_BULLETS; i++) {
  if(bullet[i].valid)
    gfx.drawTransparentBitmap(bulletBitmap, bullet[i].x-2, bullet[i].y-2, 5, 5, 0);
}

Bullets and asteroids collisions

Now we must implement collisions between bullets and the asteroids. When a bullet hits an asteroid, both are destroyed. As before, we will use the square of the distance to determine if there is a collision:

// Check collision of bullets and asteroids
for(int i=0; i<MAX_ASTEROIDS; i++) {
  if(!asteroid[i].valid)
    continue;
  for(int j=0; j<MAX_BULLETS; j++) {
    if(bullet[j].valid) {
      if(distanceSquared(bullet[j].x, bullet[j].y, asteroid[i].x, asteroid[i].y) < 290) {
        asteroid[i].valid = false;
        bullet[j].valid = false;
        break;
      }
    }
  }
}

Explosions

Currently, when an asteroid or your starship are destroyed, they simply disappear. To make the game visually more enjoyable, we will add explosions.

We will not use sprites for explosions, but a series of animated circles. In addition to the variables already in the GameObject structure, for an explosion we need to store:

  • speed: the explosion of the starship remains still, while those of an asteroid moves at the same speed at which the asteroid previously moved;
  • creation time: the elapsed time from the creation of an explosion object is used to animate its color and size;
  • circles size and position: for each circle forming the explosion we store its radius and position relative to the center of the explosion.

So, we need to add these structures:

struct ExplosionCircle {
  float deltaX;
  float deltaY;
  float radius;
};

struct Explosion : GameObject {
  float speed;
  unsigned long startTime;
  ExplosionCircle circles[6];
};

...
	
#define MAX_EXPLOSIONS  5
Explosion explosion[MAX_EXPLOSIONS];

As for the other objects, we define a function for creating explosions:

void createExplosion(float x, float y, float speed) {
  for(int i=0; i<MAX_EXPLOSIONS; i++) {
    if(!explosion[i].valid) {
      explosion[i].valid = true;
      explosion[i].startTime = micros();
      explosion[i].x = x;
      explosion[i].y = y;
      explosion[i].speed = speed;
      for(int j=0; j<6; j++) {
        explosion[i].circles[j].deltaX = random(-10, 10);
        explosion[i].circles[j].deltaY = random(-10, 10);
        explosion[i].circles[j].radius = random(10, 20);
      }
      break;
    }
  }
}

In the Update section, add the code to update the position of the explosion (including the positions of each circles):

// Update explosions position
for(int i=0; i<MAX_EXPLOSIONS; i++) {
  if(explosion[i].valid) {
    explosion[i].y += explosion[i].speed * deltaTime;
    for(int j=0; j<6; j++) {
      explosion[i].circles[j].deltaX *= 1.1;
      explosion[i].circles[j].deltaY *= 1.1;
      explosion[i].circles[j].radius *= 0.8;
  }
  if(explosion[i].y >= 320) {
    explosion[i].valid = false;
  }
  if((now - explosion[i].startTime) > 400000)
    explosion[i].valid = false;
  }
}

When we want to create an explosion? On two different occasions:

  • When the starship collide with an asteroid. In this case we need to create two explosion, one for the starship and one for the asteroid. Inside the starship-to-asteroid collision test, add this code:
// Create one explosion for the starship (with speed 0), and
// an explosione for the asteroid (with current asteroid speed)
createExplosion(starship.x, starship.y, 0);
createExplosion(asteroid[i].x, asteroid[i].y, asteroidSpeed);
  • When a bullet collide with an asteroid. Inside the bullet-to-asteroid collision test, add this code:
createExplosion(asteroid[i].x, asteroid[i].y, asteroidSpeed);

Now, we must add the code to draw the explosions. First, add this code in the Frame setup section:

// We need to redraw input area?
bool redrawInputArea = false;

We need this variable for a little hack: when an explosion is drawn near the input area, it’s possible that some circle will overlap with the input area. This variable signals that the input area must be redrawn.

The code to erase and redraw explosions is the following:

// Erase explosions
for(int i=0; i<MAX_EXPLOSIONS; i++) {
  if(explosion[i].valid) {
    int x = explosion[i].x;
    int y = explosion[i].y;
    for(int j=0; j<6; j++) {
      if(y + explosion[i].circles[j].deltaY >= (320 - explosion[i].circles[j].radius))
        redrawInputArea = true;
        gfx.drawFilledCircle(x + explosion[i].circles[j].deltaX, y + explosion[i].circles[j].deltaY, explosion[i].circles[j].radius+2, 15);
    }
  }
}
	  
// Draw explosions
for(int i=0; i<MAX_EXPLOSIONS; i++) {
  if(explosion[i].valid) {
    float x = explosion[i].x;
    float y = explosion[i].y;
    for(int j=0; j<6; j++) {
      unsigned long timeFromStart = now - explosion[i].startTime;
      uint8_t color = 0;
      if(timeFromStart > 280000)
        color = 3;
      else if(timeFromStart > 180000)
        color = 2;
      else if(timeFromStart > 40000)
        color = 1;
      gfx.drawFilledCircle(x + explosion[i].circles[j].deltaX, y + explosion[i].circles[j].deltaY, explosion[i].circles[j].radius, color);
    }
  }
}

...

// Redraw top of the input area
if(redrawInputArea) {
  gfx.drawFilledRectangle(0, 320, 320, 80, 13);
  gfx.drawFilledRectangle(2, 322, 316, 78, 14);
}

Score and difficulty

The game is almost ready! We only have to add score tracking and gradually raising the game difficulty as the time pass.

First, add a variable to hold the score:

float score;

In the Frame setup section, add this code:

// Things to do only when game is running
if(gameState == Running) {
  // Update score
  score += deltaTime * asteroidSpeed;

  // Update asteroid speed and spawn period
  asteroidSpeed += 2 * deltaTime;
  if(asteroidPeriod > 10000)
    asteroidPeriod -= 3000 * deltaTime;

  // Update bullet fire period
  if(firePeriod > 10000)
    firePeriod -= 3000 * deltaTime;
	
  ...
  
  // Erase score
  gfx.drawFilledRectangle(2, 2, 160, 16, 15);
}

At each frame we add a score proportional to the delta time passed times the asteroid speed. Furthermore, we increase the speed of asteroids and reduce their creation period to increase the difficulty over time. We also decrease the period between one bullet and another to slightly increase the chance of hitting asteroids when they move faster.

The last line erase the upper left corner of the screen, where we will write the score. In the Redraw section, where we draw strings, add this code for the Running state:

char buffer[32];
switch (gameState) {
  ...
	  
  case Running:
    sprintf(buffer, "%d", (int)roundf(score));
    gfx.drawString(2, 2, buffer, 0);
    break;
		
  ...
}

We obviously want to assign points every time the player destroys an asteroid. Within the bullet-to-asteroid collision test, add this code:

if(gameState == Running)
  score += 200;

In the Process input section, when we start a new game from the game over screen, we must reset the variables to their start value:

if(screenTouched) {
  switch(gameState) {
    ...

    case GameOver:
    {
      // Map from touchscreen coordinates to screen coordinates
      int tx = map(touchPoint.x, TS_MINX, TS_MAXX, 0, 320);
      int ty = map(touchPoint.y, TS_MINY, TS_MAXY, 0, 480);
      if((tx >= 50) && (tx <= 270) && (ty >= 170) && (ty <= 234)) {
        // Play again clicked, restore initial value
        firePeriod = 1000000;
        asteroidPeriod = 500000;
        asteroidSpeed = 70;
        score = 0;
        starship.x = 160;
        starship.y = 230;
        starship.valid = true;
        gameState = Running;

        // Erase play again button and game over string
        gfx.drawFilledRectangle(50, 60, 240, 174, 15);
      }
      break;
    }
  }
}

The last thig to do is showing the final score in the game over screen:

// Draw strings and buttons
char buffer[32];
switch (gameState) {
  ...

  case GameOver:
    sprintf(buffer, "SCORE: %d", (int)roundf(score));
    gfx.drawString2x(88, 80, "GAME OVER", 0);
    gfx.drawString2x(160-8*strlen(buffer), 116, buffer, 0);
    gfx.drawFilledRectangle(50, 170, 220, 64, 9);
    gfx.drawRectangle(50, 170, 220, 64, 8);
    gfx.drawString2x(80, 186, "PLAY AGAIN", 0);
    break;
}

Final result

The game is finally complete! This is the final result:

Music:
Planet Hell by Serge Narcissoff | https://soundcloud.com/sergenarcissoff
Music promoted by https://www.free-stock-music.com
Creative Commons Attribution-ShareAlike 3.0 Unported
https://creativecommons.org/licenses/by-sa/3.0/deed.en_US

What’s your highest score? 😃

You can find the complete code of this post here.


0 Comments

Leave a Reply

Your email address will not be published. Required fields are marked *

By continuing to browse this Website, you consent to the use of cookies. More information

This Website uses:

By continuing to browse this Website without changing the cookies settings of your browser or by clicking "Accept" below, you consent to the use of cookies.

Close