Write your First Game in C++

By: Abhilekh Gautam

Are you tired of writing console based programs in C++ and want to use C++ for more fun, then this book is for you. At the end of this book you will have a fully fledged working 2D game for you.To write this game we will use olc::PixelGameEngine and some Modern C++.

For more about olc::PixelGameEngine visit here.

The entire source code of the game is available here.

Prerequisites

  • A Simple Understanding of C++ would work. You should be aware of Inheritance in C++. We will try to use as much modern C++ as Possible.

Setup

Since we will be using olc::PixelGameEngine for this purpose we will need to set it up. This won't be covered in this book as everything about that is available in its official docs.

For Linux

To get things done on Linux, Visit: Compiling on Linux

For Windows

To get things done on Windows, Visit: Compiling on Windows

Foreword

Let me tell you here, I am neither a Game Developer nor a C++ Expert. I like writing C++ and I know I am pretty bad at it. If you are here to learn writing production ready code then this is not the right place for you.

But I am sure this won't be a waste of time if you continue till the end. After all we will be writing a 2D game. That should be fun.

Introduction

Welcome to Write Your First Game in C++! The goal of this book is to show that programming with C++ can be fun and engaging. Despite its reputation as a difficult language, C++ is a powerful tool that can be used to create a wide variety of programs. Unfortunately, many universities in Nepal teach outdated versions of C++ (such as C++98/03) and primarily focus on console-based programs, which can give the impression that C++ is not useful. This book aims to change that perception and demonstrate the capabilities of C++ by guiding you through the process of creating your own game. It is also an excellent resource for anyone looking to learn C++ and programming in general.

Who is this book for?

This book is intended for everyone who wish to have fun with C++.

Getting Started

If you have everything setup, Lets begin your Game development journey.

Hello, OLC!

It's time to write your first program using olc::PixelGameEngine. how can we break the tradition, we will start by writing a simple Hello, World program.

Setting up the Project

  • Begin by making a directory to store your Code. I'll name it Space Warrior that will be the name of the game we are going to create
  • Add Header file olcPixelGameEngine.h to our current directory which you can download here.
  • Create a new .cpp file, name it main.cpp in the current direcotry.

Hello, World!

Add following contents to the main.cpp file. We begin by including the header olcPixelGameEngine

#define OLC_PGE_APPLICATION
#include "olcPixelGameEngine.h"

Now let us create a new class for our Game, Game it will inherit olc::PixelGameEngine

  class Game : public olc::PixelGameEngine
  {
    public:
	Game()
	{
		sAppName = "Space Warrior";
	}

	bool OnUserCreate() override
	{
		// Called once at the start, so create things here
		return true;
	}

	bool OnUserUpdate(float fElapsedTime) override
	{
	  Clear(olc::BLACK);
	  DrawString(5,5, "Hello, World");	
	  return true;
	}
};

Any class that inherits from olc::PixeGameEngine should override two functions, OnUserCreate and OnUserUpdate. Don't worry we will get into details later.

Now lets write our main function, main function would be very neat

int main()
{
	Game game;
	if (game.Construct(256, 240, 4, 4))
		game.Start();

	return 0;
}

Compilation and Running the Program

If you have everything setup as mentioned in the chapter Write your First Game in C++, you should be able to issue following commands in the terminal/command prompt.

For Linux

You should move on to the current directory first.

Compiling

$ g++ -o main main.cpp -lX11 -lGL -lpthread -lpng -lstdc++fs -std=c++17

Running

$ ./main

For Windows

You should move on to the current working directory first.

Compiling

$ g++ -o main.exe main.cpp -luser32 -lgdi32 -lopengl32 -lgdiplus -lShlwapi -ldwmapi -lstdc++fs -static -std=c++17

Running

$ main.exe

That's it now if you run this program, you should see a window with string "Hello, World" being displayed like this:

Hello OLC

Getting into Details

As mentioned earlier we inherited our class from olc::PixelGameEngine and then we override two functions,

OnUserCreate()

The function OnUserCreate is called only once by the engine at the start. Let us look at the definition again,

bool OnUserCreate() override
 {
     // Called once at the start, so create things here
     return true;
 }

This function returns a bool value, since the function is called only once, we can use this for the initialization stuff. However at the moment we have nothing to initialize, so we will just return true from the method.

OnUserUpdate()

This function is called repeatedly by the engine per frame. For smooth transition of the game, we need to keep updating the frame and whenever the frame is updated this function is called. Let us check out the code of OnUserUpdate method again,

bool OnUserUpdate(float fElapsedTime) override
 {
   Clear(olc::BLACK);
   DrawString(5,5, "Hello, World");	
   return true;
 }

Let us look at the function signature first, it returns a bool value, recieves a float as a parameter. We call the Clear function inside the function, Clear function clears contents of the previous frame We then called DrawString function and passed in the x, y coordinates where the string would be displayed, and then the third parameter, Hello, World is the string to be displayed in the screen. We exit the function by returning true.

The main function

int main()
{
    Game game;
    if (game.Construct(256, 240, 4, 4))
        game.Start();

    return 0;
}

In the main Function, we first created an object game, of type Game. We then need to tell the engine about the size of window we want to create we do so by using theConstruct method. The Construct method returns true if the construction was successfull and then we call the start method which starts our game engine.

Next up we will look at some useful functions to draw basic shapes using the game engine.

Drawing Basic Shapes

Before we start writing our Game, let us see some useful functions provided by olc pixel game engine that help us in drawing shape.

Note: I won't be providing full working code, just add these codes to the OnUserUpdate function and that would work.

Drawing a Line

To draw a line use the DrawLine Method

DrawLine(5,5, 30, 30);

DrawLine Demo

DrawLine has a signature of:

DrawLine(int32_t x1, int32_t y1, int32_t x2, int32_t y2, Pixel p = olc::WHITE, uint32_t pattern = 0xFFFFFFFF)

It simply draws a line from (x1,y1) to (x2,y2)

Drawing a Circle

To draw a circle use the DrawCircle Method

DrawCircle(50, 50, 5);

DrawCircle

DrawCircle has a signature of

DrawCircle(int32_t x, int32_t y, int32_t radius, Pixel p = olc::WHITE, uint8_t mask = 0xFF)

It simply draws a cirle centered at (x, y) with a radius.

Drawing a Filled Circle

As we might have guessed just call the FillCircle Method

FillCircle(50, 50, 5, olc::RED);

FillCircle

We passed an additional parameter, the color of the pixel to fill with.

Drawing a Rectangle

We use DrawRect method.

DrawRect(5,5, 30, 40);

DrawRect It draws a rectangle at (5,5) with a width of 30 and height of 40.

Drawing a Triangle

We use DrawTriangle method.

DrawTriangle(5,5,20,5, 12,10);

DrawTrianlge DrawTriangle has following signature:

DrawTriangle(int32_t x1, int32_t y1, int32_t x2, int32_t y2, int32_t x3, int32_t y3, Pixel p = olc::WHITE)

It simply draws a triangle between three points (x1, y1), (x2, y2) and (x3, y3);

Adding Our Player

Now on, we will start working on our game, let us start by adding our main player.

We will use olc::sprite to add our player, just think sprite as an imagebut before this let us initialize some member variables for our class.

class Game : public olc::PixelGameEngine
{
public:
    Game()
    {
        sAppName = "Space Warrior";
    }

    bool OnUserCreate() override
    {
        
        return true;
    }

    bool OnUserUpdate(float fElapsedTime) override
    {
        return true;
    }
private:
    float fPlayerPositionX = 185.0f;
    float fPlayerPositionY = 250.0f;
};

Nothing new, just added two variables to denote the player's X and Y coordinates in the screen.

Now download the image provided below (hover and right click), for our player. You can use any image you want

player-image

Loading Sprites

class Game : public olc::PixelGameEngine
{
public:
   bool OnUserCreate() override 
    {
        sprPlayer = std::make_unique<olc::Sprite>("../sprites/player.png");
        return true;
    }

   bool OnUserUpdate(float fElapsedTime) override
    {
        Clear(olc::BLACK);
        DrawSprite(fPlayerPositionX, fPlayerPositionY, sprPlayer.get());
        return true;
    }
private:
    float fPlayerPositionX = 185.0f;
    float fPlayerPositionY = 250.0f;

    std::unique_ptr<olc::Sprite> sprPlayer;
};

Don't get scared, sprPlayer is just a pointer, that is very safe to work with. sprPlayer holds a pointer to object of type olc::Sprite.

We have quite a few changes here, We added,

bool OnUserCreate() override
   {
        // sprites...
        sprPlayer = std::make_unique<olc::Sprite>("../sprites/player.png");
        return true;
    }

We simply initialized sprEnemy by passing filepath to

std::make_unique<olc::Sprite>(file_path);

Similarly, In the OnUserUpdate method, we added

bool OnUserUpdate() override
{
  DrawSprite(fPlayerPositionX, fPlayerPositionY, sprPlayer.get());
  return true;
}

We used DrawSprite method to draw the sprite on the screen, we passed in the position of the player and a pointer contained by sprPlayer, remember I said earlier sprPlayer holds a pointer to object of type olc::sprite, the get method just return that pointer.

To be on the same page, our main function should look like this

int main()
{
    Game game;
    if (game.Construct(450, 340, 4, 4))
        game.Start();

    return 0;
}

Now if you run this program you should see something like this: First look of the game

Don't worry if you see some compiler warnings, we will deal with them later.

Next we will add some life to our player, i.e. we will add some movement to the player.

Adding Motion to Player

Now we are going to add life to our player, we will begin by adding motion to the player. But before we start writing some code let us look at the concept behind moving (translating) the player.

Translating a Point

Moving a player means we want to translate the player's position in the 2D plane. Consider a point in the 2D plane,(x1, y1), now what will you do if you want the point to move by 5 units to the right?

That's simple add 5, to the current position in the X axis, so our new postion will be (x1 + 5, y1).

Similarly to move the player to left we will substract 5, so our new postion will be (x1 - 5, y1).

Notice that we don't change the y coordinate at all because we are dealing with the horizontal motion, so y must be constant here.

Similarly to move the point by 5 units upward, simply subtract 5 to the current position to obtain the new coordinate (x1, y1 - 5).

To move the point 5 units downward, simply add 5 to the current position to obtain the new coordinate (x1, y1 + 5)

To Summarize,

Current PositionTo LeftTo RightUpwardsDownwards
(x1, y1)x1 - 5x1 + 5y1 - 5y1 + 5

If you are confused why we subtracted in case of upward motion, just remember the top left corner is (0,0), so moving downwards requires addtion and moving upwards require subtraction.

Handling User Input

We will only move the player when certain keys are pressed, so we will need to check when certain keys are pressed. That is preety easy, We will use the GetKey function and pass it in the key we are looking for, lets say the left arrow.

if (GetKey(olc::Key::LEFT).bPressed)
{
  std::cout << "Left Arrow was pressed\n";
}

Adding Motion

We need to move the player with some speed, so first we need to create a member variable representing the speed of player.

class Game: public olc::PixelGameEngine{
// same as before
private:
  //same as before
  float fPlayerVel = 0.5;
};

Since we can handle user input, let us update our OnUserUpdate function to:

bool OnUserUpdate(float fElapsedTime) override
{
  Clear(olc::BLACK);
  DrawSprite(fPlayerPositionX, fPlayerPositionY, sprPlayer.get());
  if (GetKey(olc::Key::LEFT).bHeld)
  {
     fPlayerPositionX = fPlayerPositionX - fPlayerVel;
  }
  if (GetKey(olc::Key::RIGHT).bHeld)
  {
     fPlayerPositionX = fPlayerPositionX + fPlayerVel;
  }
  if (GetKey(olc::Key::UP).bHeld)
  {
     fPlayerPositionY = fPlayerPositionY - fPlayerVel;
  }
  if (GetKey(olc::Key::DOWN).bHeld)
  {
     fPlayerPositionY = fPlayerPositionY + fPlayerVel;
  }
 return true;
}

I don't think we need any explanation here, we just applied the same translation technique we learnt earlier. Once you compile and run the code, you should be able to move the player now. Next up we will look at managing time in our game.

Working with Time

Time is an important concept in a game, In a game we give players the illusion of movement, But in fact we are just displaying them the static images, We keep repositioning image throughout the screen which creates the illusion of movement.

We know that,

Speed = Distance travelled / Time Taken

which implies that,

Distance Travelled = Speed * Time Taken

So to calculate the distance travelled by any object we should know about its speed and the time that has passed.

A parameter fElapsedTime is passed as a parameter to OnUserUpdate function which is the previous frame duration in seconds.

So to handle time properly in our game we have to multiply the speed of moving object by fElapsedTime. Let us Quickly bring changes to our code,

bool OnUserUpdate(float fElapsedTime) override
{
  DrawSprite(fPlayerPositionX, fPlayerPositionY, sprPlayer.get());
  if (GetKey(olc::Key::LEFT).bHeld)
  {
     fPlayerPositionX = fPlayerPositionX - fPlayerVel * fElapsedTime;
  }
  if (GetKey(olc::Key::RIGHT).bHeld)
  {
     fPlayerPositionX = fPlayerPositionX + fPlayerVel * fElapsedTime;
  }
  if (GetKey(olc::Key::UP).bHeld) 
  {
     fPlayerPositionY = fPlayerPositionY - fPlayerVel * fElapsedTime;
  }
  if (GetKey(olc::Key::DOWN).bHeld)
  {
     fPlayerPositionY = fPlayerPositionY + fPlayerVel * fElapsedTime;
  }
  return true;
}

You should just remember that, if there is any motion just multiply the speed by fElapsedTime and you should be fine.

The Player Doesn't Move Now

No worries, we have set fPlayerVel = 0.5 and the value of fElapsedTime is very small, so their product would be even smaller.

To solve this simply increase the player's velocity. You just need to update the value of fPlayerVel.

float fPlayerVel = 90.0f;

A value of 90 should work fine, if that didn't worked just try some other values.

Next up we will add enemies to our game. Stay Tuned.

Adding Enemies to Game

Let us now add enemies to our Game. I will use the image below for the enemy, you can download it by right clicking on it, or can choose any other image

Enemy

But before we draw the sprite, let us create a struct that represents Enemy of our game

struct Enemy{
 float x;
 float y;
 bool  alive;
}

Here x and y represent the x and y coordinate of the enemy and alive represents whether the enemy is alive or not.

You can add this Definition right before the class Definition.

We will have many enemies, so we will represent the collection of enemy using a vector. Then let us add a vector to the private field of class, I will name it vEnemy.

std::vector<Enemy> vEnemy;

Here, vEnemy is a vector(collection) of Enemy.

Adding enemy is similar to that of player, we will again use olc::sprite.

Add sprEnemy to the private variable field too,

std::unique_ptr<olc::Sprite> sprEnemy;

Similarly, update the OnUserCreate method to

bool OnUserCreate() override
{
  sprPlayer = std::make_unique<olc::Sprite>("/home/abhilekh/Downloads/player.png");
  sprEnemy = std::make_unique<olc::Sprite>("/home/abhilekh/Downloads/enemy.png");
  return true;
}

Positioning the Enemies

The enemy struct has two fields to represent its coordinate axis, we will simply need to assign them with some values, and draw sprites in those positions.

Let us add a new method to our class, I will name it produceEnemy,

void produceEnemy() {
  for (int i = 0; i < 70; ++i) {
    if (i < 18)
       vEnemy.emplace_back(Enemy{float(ScreenWidth()) / 2 + (float) i * 10 - 100, 40.0f, true});
    else if (i < 36)
       vEnemy.emplace_back(Enemy{float(ScreenWidth()) / 2 + 10.0f * (float) i - 280, 55.0f, true});
    else if (i < 54)
       vEnemy.emplace_back(Enemy{float(ScreenWidth()) / 2 + 10.0f * (float) i - 460, 75.0f, true});
    else
       vEnemy.emplace_back(Enemy{float(ScreenWidth()) / 2 + 10.0f * (float) i - 640, 95.0f, true});
   }
}

Nothing new here, we run the loop for 70 times, meaning we will have total of 70 enemies, then we use emplace_back to add enemy to the vector.

Let me workout this for you when i = 0, i < 0 so the first condition will be satisfied and the following value will be added to the vector

Enemy{ScreenWidth() / 2 + 0 - 100, 40.0f, true}

This means when i = 0 a new Enemy object with,

x = ScreenWidth() / 2 + 0 - 100
y = 40
alive = true

will be added to the vector. In the similar way 70 different Enemy object with different x and y coordinates will be added to the vector.

We will need to create the enemy only once throughout the game so let us call the produceEnemy from the OnUserCreate method as:

bool OnUserCreate() override
{
 produceEnemy();
 sprPlayer = std::make_unique<olc::Sprite>("/home/abhilekh/Downloads/player.png");
 sprEnemy = std::make_unique<olc::Sprite>("/home/abhilekh/Downloads/enemy.png");
 return true;
}

Displaying the Sprite

Displaying the Enemy is similar to displaying the player, the only difference is we have to use a loop to draw enemy because we have 70 enemies in total

for (auto elm: vEnemy) {
  if (elm.alive)
    DrawSprite(elm.x, elm.y, sprEnemy.get());
}

We used a range based for loop and called the DrawSprite function with the enemies position and a pointer to olc::sprite object.

So our OnUserUpdate method should look like this,

bool OnUserUpdate(float fElapsedTime) override
{
  DrawSprite(fPlayerPositionX, fPlayerPositionY, sprPlayer.get());
  if (GetKey(olc::Key::LEFT).bHeld)
  {
    fPlayerPositionX = fPlayerPositionX - fPlayerVel * fElapsedTime;
  }
  if (GetKey(olc::Key::RIGHT).bHeld)
  {
    fPlayerPositionX = fPlayerPositionX + fPlayerVel * fElapsedTime;
  }
  if (GetKey(olc::Key::UP).bHeld) {
    fPlayerPositionY = fPlayerPositionY - fPlayerVel * fElapsedTime;
   }
  if (GetKey(olc::Key::DOWN).bHeld)
  {
    fPlayerPositionY = fPlayerPositionY + fPlayerVel * fElapsedTime;
  }

  for (auto elm: vEnemy)
  {
     if (elm.alive)
       DrawSprite(elm.x, elm.y, sprEnemy.get());
  }
 return true;
}

And now if you compile and run the program you should see something like this, Status After Adding an Enemy

Next up we will implementing bullet shooting for our player.

Shooting the Bullets

We have a player and enemies, whats next? Lets shoot them.

We will need multiple bullets which we will store as a vector, and we will also create a struct Bullet to represent a bullet in our game.

struct Bullet{
  float x;
  float y;
  bool dead;
};

Here x and y represents the position of the bullet and dead represents the current status of the bullet.

Let us then create another variable in our private field to represent the collection of Bullet. I'll name it vBullet, you can name it anything you want.

private:
  //Same as before
  std::vector<Bullet> vBullet;
  // Same as before

We should first decide the key that should be pressed so the player will shoot.

I choose the space key, you can choose any key of your choice. Let us update the OnUserUpdate method so that it listens to the pressing of the space key.

if (GetKey(olc::Key::SPACE).bPressed)
{
  float ftempX = fPlayerPositionX;
  float ftempY = fPlayerPositionY;
  vBullet.emplace_back(Bullet{ftempX + sprPlayer->width / 2, ftempY, false});
}

The main concern for the Bullet is determining their position, where should we shoot the bullets from? It depends on the player's current position, afterall the player is shooting the bullets. If you look at our sprPlayer sprite, it would be better if we could shoot from the center of the sprite. shoot-from-here

Whenever space key is pressed we add a new Bullet to the vector with values set to

x = fPlayerPostionX + sprPlayer->width / 2
y = fPlayerPositionY
dead = false

here fPlayerPositionX is the top left corner of the player's sprite, to move to the center of image we should add up half the width of the image to its top left position.

Drawing the bullets

We will simply use a circle with the radius set to 1, as a bullet. Similar to drawing enemies, we will use a loop here, as we have a collection of bullets.

for (auto &elm: vBullet)
{
  if (elm.y > -1 && !elm.dead) 
  {
    FillCircle(elm.x, elm.y, 1, olc::RED);
  }
}

We don't need to draw bullets that are gone away from the screen display area, so I used the if statement to ensure that.

Moving the Bullets

We need to move the bullets with some speed, so let us declare yet another private member variable that represents the bullet velocity, I will name it fBulletVel, and set it as

float fBulletVel = 180.0f;

Moving the bullet means translating its position, since bullets will be fired upward we will be subtracting the y coordinate of the bullet with its speed.

for (auto &elm: vBullet)
{
  // only take care of bullets which are visible on the screen
   if (elm.y > -1 && !elm.dead) 
   {
     FillCircle(int(elm.x), int(elm.y), 1, olc::RED);
     elm.y = elm.y - fBulletVel * fElapsedTime;
   }
}

Remember that, we multiply fBulletVel with fElapsedTime as we discussed in chapter 4.

You can find all the codes here.

If you have updated the code as I mentioned, you should now be able to shoot bullets. But the bullet doesn't kill any enemy yet.

Next up we will implement collision detection for the Bullet and the enemy. Until then Have Fun!

Collision Detection - I

So far so good, we have a player that can move and shoot bullets. We also have enemy that pose threats to the player, the player needs to kill the enemy with the help of the bullets. So will be implementing this.

To kill the enemy we need to find out whether the bullet collided with the enemy or not. This is gonna be one of the tricky part of the entire program, But no problem I will try to explain everything.

Detecting the collision

It might sound simple but when the coordinate of both bullets and the enemy are same they will collide. That's right but not totally, this is because our enemy has width and touching any part of that enemy should be considered as a collision.

Let us first look at the image below: sprite-demo

Colliding with such enemy means, the x coordinate of the bullet can have the value in the range from, enemy's x to x + sprEnemy->width.

But the bullet can be thought of a circle bounded inside a rectangle, in such way our bullet will also have a width and height.

circle-demo

So For collision,

  • Bullet.y - 1 >= Enemy.y + sprEnemy->height, this means that the top most point of our bullet is in the same level or above y + sprEnemy->height But this is not the only case, as:

no-collision-for-same-y

The above image matches the above condition, but this is not the condition for collision, we have to check for the x coordinate as well.

  • Bullet.x + 1 >= Enemy.x, this means that the extreme right point of the bullet must be greater than Enemy's x coordinate. But this is also not the only case, as:

no-collsion-for-greater-x

For the collision to take place (in X), we have to bound the extreme points of the bullet within Enemy.x to Enemy.x + sprEnemy->width

To do so, we should set the condition for left extreme point as well

  • Bullet.x - 1 <= Enemy.x + sprEnemy->width. But this is not the only condtion.

no-collision-for-smaller-x

If we combine all these three condition, we will be able to detect the collision.

Combining all these three conditions we get

  • Bullet.y - 1 >= Enemy.y + sprEnemy->height
  • Bullet.x + 1 >= Enemy.x
  • Bullet.x - 1 <= Enemy.x + sprEnemy->width

Implementing the Collision

Too much talk, now lets see some code in action, add the following line to the OnUserUpdate method

for (auto &elm: vBullet) {
  for (auto &enemy: vEnemy) {
     if (!elm.dead && enemy.alive && elm.y > enemy.y && elm.x + 1 >= enemy.x &&
         elm.x - 1 <= enemy.x + float(sprEnemy->width) &&
         elm.y - 1 <= enemy.y + float(sprEnemy->height)) {
            // kill both bullet and enemy.
            elm.dead = true;
            enemy.alive = false;
        }
     }
}

We used a nested loop because we have to check the collision for each element against each enemy.

Now you should see the player killing the enemy, but enemies are very weak for us now.

Next up we will give life to our enemy.

Adding Life to the Enemy

Our enemy do nothing as we now, I want the enemies to move towards our player, and when it collides with our player, the life count of the player reduces.

The Pythagoras Theorem

Since the enemy will be moving towards the player, we need to determine the path (or angle) at which enemy move. This is where the Pythagoras theorem comes in.

The Pythagoras Theorem

tempY is the vertical distance between the enemy and the player while tempX is the horizontal distance between the enemy and player.

To calculate the hypo (hypotenuse), we can use pythagoras theorem as,

\(\ hypo = \sqrt{(tempY)^2 + (tempX)^2} \)

tempX and tempY can be calculated as:

tempX = x1 - (x + sprEnemey->width / 2);

tempY = y1 - (y + sprEnemy->height);

Note that the coordinates here are based on the above image.

So we can now calculate the value of sin(theta) and cos(theta). From basic mathematics we have,

sin(theta) = height_of_perpendicular / hypotenuse

which in our case turns out to be,

sin(theta) = tempY / hypo

Similarly,

cos(theta) = base / hypotenuse

which in our case turns out to be,

cos(theta) = tempX / hypotenuse

What to do with the angles?

Remember that, when we moved to some X direction, we simply added the speed to the x coordinate and when we moved to some Y direction, we added the speed to the 'y' coordinate in case of the player.

But things slightly change here, since we are following the player, we have to choose the shortest path. i.e. We won't only move in single direction(either X or either Y), we have to move diagonally which means we have to travel some X and some Y distance at the same time. In fact we will travel with certain angle along the X and Y direction. Let us look at things in some detail, Horizontal Movement

When we are moving horizontally, it looks like we are just adding the x coordinate to the velocity, but in fact, what we really do is,

X = X + Velocity * cos(theta)

Where theta = angle between x-axis and the direction we move in, which is 0.

so, X turns out to be,

X = X + Velocity * cos(0)
  = X + Velocity

Similarly for the vertical movement, Vertical Movement

When we are moving vertically downwards, X coordinate remains constant and Y can be calculated as:

Y = Y + Velocity * Cos(theta)

where theta is the angle between Y-axis and the resultant. So in this case,

Y = Y + Velocity * Cos(0)
  = Y + Velocity

Now let us focus on diagonal movements, diagonal movement

When we move diagonally, there is change in both X and Y coordinate, to calculate them, we will simply use the concept we used earlier,

X = X + Velocity * Cos(theta)
Y = Y + Velocity * Cos(90 - theta)
  = Y + Velocity * Sin(theta)

Note that, When we move diagonally, the angle between Y-axis and the resultant is (90 - theta), and Cos(90 - theta) = Sin(theta), so we multiply by Sin(theta) in case of Y.

Implementation

Too much talk till now, let us write some code then, Update the OnUserUpdate method to have following snippets:

for(auto &elm : vEnemy){
  if(elm.alive)
  {
    float tempX = (fPlayerPositionX + sprPlayer->width / 2
        ) - elm.x + sprEnemy->height + sprEnemy->width / 2
                               );
    float tempY = (fPlayerPositionY - elm.y + sprEnemy->height);

    // simple pythagoras theorem
    float tempHypo = powf(tempX, 2) + powf(tempY, 2);
    float Hypo = sqrtf(tempHypo);

    float sinTheta = (tempY / Hypo);
    float cosTheta = (tempX / Hypo);

    elm.x = elm.x + fEnemyVel * cosTheta * fElapsedTime;
    elm.y = elm.y + fEnemyVel * sinTheta * fElapsedTime;
    break;
  }
}

We iterated through every enemy in the Enemy vector and then we calculated the values of tempX and tempYusing the techniques we discussed earlier, and then calculated the hypotenuse and values of sin(theta) and cos(theta).

And then simply calculated the new coordinates.

elm.x = elm.x + fEnemyVel * cosTheta * fElapsedTime;
elm.y = elm.y + fEnemyVel * sinTheta * fElapsedTime;

Note the inclusion of fElapsedTime, anything in a motion should have its velocity multiplied by fElapsedTime.

There is a break statement at the end of the end block, this is because we only want to move our enemy one at a time.

When you run this program, you should see one of the enemy following the player: enemy following the player

But we will need to kill the enemy when the collsion takes place, that is set for the next chapter.

Collision Detection - II

Earlier we talked about Collision Detection, where we detected the collsion between bullet and the enemy. This time we have to detect collision between the enemy and player.

The concepts and techniques are similar as before, Collision between Enemy and Player

I won't be explaining too much, If you are still confused head back to chapter 7, where I explained in detail about the collision detection.

The conditions

  • x + sprEnemy->width >= x1
  • x <= x1 + sprPlayer->width
  • y + sprEnemy->height >= y1

We check for the range of corners of the enemy, if they satisfy all of the above condition, we can say that they have collided.

Implementation

Update the OnUserUpdate method to contain following snippets,

for(auto &elm : vEnemy){
  if(elm.alive)
  {
   // Same as before..
     if (elm.x + sprEnemy->width >= fPlayerPositionX &&
         elm.x <= fPlayerPositionX + sprPlayer->width &&
         elm.y + sprEnemy->height >= fPlayerPositionY)
        {
          elm.alive = false;
        }
      break;
  }
}

Above code justifies everything we talked earlier, nothing to explain here. We set elm.alive = false to kill the enemy. So now you should see enemies being killed on collision with the player.

All the updated code for OnUserUpdate method can be found here

Next up we will add Scoreboard and implement the concept of life for our Player.

Scoreboard and the Concept of Life

We are nearly at the end of the book now, we can move the player, shoot the threatful enemies, Let us now work out for the Scoreboard and implement the concept of life.

The Rule of the game is simple, you kill a enemy you get 5 points, you collide with the enemy your life reduces by 1, since collision kills the enemy, you will also gain those points.

To store the score and life count, add two new variables to the private member field of the class,

private:
  int score = 0;
  int life_count = 3;

Displaying the Score:

We will use the DrawString function to display the score, Update OnUserUpdate function to,

DrawString(0, 5, "SCORE:" + std::to_string(score));

We also need to update the score, when the enemy is killed, A enemy is killed when

  • The bullet strikes the enemy
  • The enemy collides with the player.

We now just need to add the following line where the above conditions are satisfied.

score = score + 5;

We also need to reduce the life_count by 1 when

  • The enemy collides with the player

So just add the following line where the above condition matches in the OnUserUpdate function.

life_count = life_count - 1;

Drawing the Life Sprite

We will use the sprite below to represent the life count of a player

Life Sprite

First we need to create a private member variable, sprLife to store a pointer to object of type olc::Sprite

std::unique_ptr<olc::Sprite> sprLife;

and then on OnUserUpdate method we assign sprLife as:

bool OnUserCreate() override
{
  produceEnemy();
  sprPlayer = std::make_unique<olc::Sprite>("/home/abhilekh/Downloads/player.png");
  sprEnemy = std::make_unique<olc::Sprite>("/home/abhilekh/Downloads/enemy.png");
  sprLife  = std::make_unique<olc::Sprite>("/home/abhilekh/Downloads/life.png");
  
  return true;
}

To draw this sprite, we will use DrawSprite method, but the number of sprites to be displayed depends on the life_count, so will use a for loop here,

// load life sprite..
for (int i = 0; i < life_count; ++i) {
  DrawSprite(270 + sprLife->width + 25 * i, 5, sprLife.get());
}

We just passed in the x, y coordinate where the sprite is to be drawn and the pointer to the object of type olc::Sprite.

Now if you compile and run the program, you should see something like this Score and Life Demo

All the codes till now, can be found here

Next up we will work on polishing the game.

Congratulations, You have successfully writen your first game.

What's Next?

Writing good code was never the aim of this book, but good code is what we all need. Lets see how we can make our code better.

What can we improve?

The list is pretty long but we will only discuss what I really hate (very much) about this code.

Let us start from our struct definition,

Intoducing a Entity Struct.

We defined two struct one to represent the Enemy

struct Enemy{
    float x;
    float y;
    bool alive;
};

and other to represent Bullets

struct Bullet{
  float x;
  float y;
  bool dead;
};

Look at those fields, aren't they same. Can we not define a single struct for both of them. After all they both represent a Entity in our game, then why not have a single struct for them as:

struct Entity {
   float x;
   float y;
   bool alive;
};

Then how can we distinguish between an enemy and a bullet? Probably you could add a new field in the struct that represents the type of Entity as:

struct Entity{
 //snip
 std::string entity_type;
};

And then our constructor for Entity takes a string as parameter.

Entity::Entity(std::string e_type){
  if (e_type == "Bullet"){
    // some error handling stuff...
    
    // create a bullet
  }
  // similar thing for our enemy.
}

This way we will just need to have a single Vector (Vector of Entity) instead of two and we can check for either enemy or bullet as:

for(auto &elm : vEntity){
   if (elm.entity_type == "Bullet"){
    // do whatever you want to do with bullet

   }
   // similar things ...
}

Improving the produceEnemy function

At the moment our produceEnemy function is:

 void produceEnemy() {
   for (int i = 0; i < 70; ++i) {
    if (i < 18)
        vEnemy.emplace_back(Enemy{float(ScreenWidth()) / 2 + (float) i * 10 - 100, 40.0f, true});
    else if (i < 36)
        vEnemy.emplace_back(Enemy{float(ScreenWidth()) / 2 + 10.0f * (float) i - 280, 55.0f, true});
    else if (i < 54)
        vEnemy.emplace_back(Enemy{float(ScreenWidth()) / 2 + 10.0f * (float) i - 460, 75.0f, true});
    else
        vEnemy.emplace_back(Enemy{float(ScreenWidth()) / 2 + 10.0f * (float) i - 640, 95.0f, true});
    }
}

Do you see all those hard-coded integers there, they are bad, very very bad. Lets see how we can improve this, the integer literals we are using in every if statements actually is ensuring that there are exactly 18 enemies per row being displayed. So what we can do is use create a new variable and use it.

int enemies_per_row = 18;

void produceEnemy(){
  for(//...){
  if (i < enemies_per_row){
   // do sth
   }
  else if (i < 2 * enemies_per_row){
    // do sth
   }
  }

}

furthermore, statements like this are very unreadable

 vEnemy.emplace_back(Enemy{float(ScreenWidth()) / 2 + (float) i * 10 - 100, 40.0f, true});

While we create any Entity either an enemy or a bullet, we need to know the Coordinate for those entities. So why not make our constructor take positions and entity_type as parameter.`

Entity::Entity(float x, float y, std::string e_type){
  // create entity...
}

and insert it into the vector as,

vEntity.emplace_back(Entity(x_position, y_position, "Enemy"));

Somewhat cleaner? I hope you agree.

Cleaning up OnUserUpdate function

Everything inside a single function is a big NOOO but that is what we have done with our OnUserUpdate function. What we can do is, split the logic inside multiple functions, like we can add a new function that only handles user input as :

void handle_input(){
  if (GetKey(olc::Key::LEFT).bHeld) {
      fPlayerPositionX = fPlayerPositionX - fPlayerVel * fElapsedTime;
   }
   if (GetKey(olc::Key::RIGHT).bHeld) {
      fPlayerPositionX = fPlayerPositionX + fPlayerVel * fElapsedTime;
   }
   if (GetKey(olc::Key::UP).bHeld) {
      fPlayerPositionY = fPlayerPositionY - fPlayerVel * fElapsedTime;
    }
   if (GetKey(olc::Key::DOWN).bHeld) {
      fPlayerPositionY = fPlayerPositionY + fPlayerVel * fElapsedTime;
    }
   if (GetKey(olc::Key::SPACE).bPressed) {
       float ftempX = fPlayerPositionX;
       float ftempY = fPlayerPositionY;
       vBullet.emplace_back(Bullet{ftempX + float(sprPlayer->width) / 2, ftempY, false});
   }
}

and then calling it inside our OnUserUpdate function as:

bool OnUserUpdate(float fElapsedTime) override
{
// same as before
   
   handle_input();

// same as before...
}

Similarly you can add a new function that is just dedicated for collision detection and you can just call that inside OnUserUpdate function.

Don't repeat youself

If you didn't follow my earlier suggestion of adding a new function for collision detection then this is a must have for you. If you look at our OnUserUpdate function you can clearly see we are repeating ourself,

This thing is for drawing alive enemy to the screen.

for (auto elm: vEnemy) {
   if (elm.alive)
     DrawSprite(elm.x, elm.y, sprEnemy.get());
}

and another similar construct for moving our enemy towards the player,

for(auto &elm : vEnemy){
    if(elm.alive){
         float tempX = ((fPlayerPositionX + float(sprPlayer->width) / 2
            ) - elm.x + float(sprEnemy->height) + float(sprEnemy->width) / 2
        );
       float tempY = (fPlayerPositionY - elm.y + float(sprEnemy->height));

      // snip
    }
 }

Why not merge these two into one?

for(auto &elm : vEntity){
  if(elm.alive && elm.entity_type == "Enemy"){
    DrawSprite(elm.x, elm.y, sprEnemy.get());
    float tempX = ((fPlayerPositionX + float(sprPlayer->width) / 2
    ) - elm.x + float(sprEnemy->height) + float(sprEnemy->width) / 2
    );
    float tempY = (fPlayerPositionY - elm.y + float(sprEnemy->height));
  }
}

Do you see more of these kind, just merge them.

Stay Consistent

This might sound simple and easy, but you need to be consistent with your naming convention.

  • Do your variable name follow same pattern?
  • Do your function name follow same pattern?
If your answer to any of these is a No, go and improve that.

Splitting into multiple files

Do you feel like this is too much inside a single file, feel free to split the program into multiple files. Everything inside a single file is also a big no and you should try avoiding that. But it is ok for simple projects like this.

That's It

There might still be lots of things to improve but those were the things I really wanted to talk about.

Final Words

C++ is a very challenging language to learn. I always take programming as fun, everybody should enjoy programming. But the problem is most beginner coding projects with C++ won't be interesting, and the interesting ones are too difficult to implement. So, this book aimed to help writing C++ without caring anything about performance and all other issue.

I hope I've helped you moving around with C++.

Hope you had fun.

If you have anything to ask, you can always reach to me on twitter: Abhilekh Gautam