Arcade game
Let's create arcade game shall we? I want to create game where player needs to find a treasure before enemies kill him. Levels will be randomly generated. Enemies will slowly hunt the player.
Wall class
I will start with general class Object. Object has position, character and color. I will protect attributes, because later I want to inherit this class. I will add three constructors, one with full customization, second that creates gray wall and third that will create wall on 0, 0 position. I will also add methods show and hide for displaying the object in window. Lastly I added simple collision function that takes position and returns true if given position matches with object position.
class Object
{
protected:
int x, y;
char skin;
Color color;
public:
Object(int xpos, int ypos, char type, Color col) :
x(xpos), y(ypos), skin(type), color(col) { show(); }
Object(int xpos, int ypos) : Object(xpos, ypos, (char) FILL_F, GRAY) { }
Object() : Object(0, 0) { }
int getX()
{ return x; }
int getY()
{ return y; }
void show()
{ Cursor.printChar(x, y, skin, color); }
void hide()
{ Cursor.printBlank(x, y); }
bool collision(int xt, int yt)
{ return x == xt && y == yt; }
};
To check collision with more objects I will create wallCollision function that takes a vector of walls (dynamic array). I need to go through all walls from vector and check the collision. There are two ways how to solve the problem.
// Iterative solution
bool wallCollision(int x, int y, std::vector<Object*>& walls)
{
for(auto wall: walls)
if (wall -> collision(x, y))
return true;
return false;
}
// Functional solution (requires algorithms library)
bool wallCollision(int x, int y, std::vector<Object*>& walls)
{
return std::any_of(walls.begin(), walls.end(),
[&](auto& wall) { return wall -> collision(x, y); });
}
Now I will use this object to populate level with random walls. I will create border of size 18 and 100 random walls inside. Roughly 30% of the level will be walls. In main I will set window size and font size. I will set width and height of font to one value so that font character will fit in square. To have different levels each game, I can use srand and library time.h to set random seed.
void create_walls(std::vector<Object*>& walls)
{
int posx, posy;
for (int i = 0; i < 18; i++)
{
walls.push_back(new Object(i,0));
walls.push_back(new Object(i+1,18));
walls.push_back(new Object(0,i+1));
walls.push_back(new Object(18,i));
}
for (int i = 0; i < 100; i++)
{
posx = 2 + rand() % 15;
posy = 2 + rand() % 15;
if (posx != 1 && posy != 1 && !wallCollision(posx, posy, walls))
{ walls.push_back(new Object(posx, posy)); }
}
}
int main()
{
int fontSize = Window.getScreenHeight() / 36;
Cursor.setFontPixels(fontSize, fontSize);
Window.setSizeChars(20, 20);
srand((int)time(NULL));
std::vector<Object*> walls;
create_walls(walls);
Keyboard.waitUser();
return 0;
}
Player class
We have walls, now we need to add player. To create class Player I will inherit class Object and make move method. When I press 'W' button, I first check if there is a collision and only if there is none I move the player. In main I will add loop that will control the player and Keyboard.wait to slow fast input checks.
class Player : public Object
{
public:
Player(int px, int py) : Object(px, py, '*', LIGHTGREEN) { }
Player() : Player (1, 1) { }
void move(std::vector<Object*>& walls)
{
int nx = x, ny = y;
if (Keyboard.get('W') && !wallCollision(x, y-1, walls)) { ny--; }
if (Keyboard.get('S') && !wallCollision(x, y+1, walls)) { ny++; }
if (Keyboard.get('A') && !wallCollision(x-1, y, walls)) { nx--; }
if (Keyboard.get('D') && !wallCollision(x+1, y, walls)) { nx++; }
if ((nx != x || ny != y) && !wallCollision(nx, ny, walls))
{ hide(); x = nx; y = ny; show(); }
}
};
int main()
{
int fontSize = Window.getScreenHeight() / 36;
Cursor.setFontPixels(fontSize, fontSize);
Window.setSizeChars(20, 20);
Window.hideBlinking();
std::vector<Object*> walls;
create_walls(walls);
Player *player = new Player(1,1);
while(!Keyboard.get(VK_ESCAPE))
{
player->move(walls);
Keyboard.wait(30);
}
return 0;
}
In this gif there can be seen jagged walls, I am not 100% of the cause but
I suspect different font would solve the issue. This code is run from my IDE
but if I run code directly from folder, there are no jagged lines.
Enemy class
In the similar way as I created player I will create enemy. I will change color to red and symbol to X. For move function I will need position of player. Enemies will be quiet dumb, if player is above them they will try to move up, if player is to their left, they will move left. They will also respect wall collisions. I will add create_enemies function that spawns enemies at random positions. Lastly I will add loop that loops through all enemies and slowly moves them towards player.
class Enemy : public Object
{
public:
Enemy(int px, int py) : Object(px, py, (char) 158, LIGHTRED) { cooldown = 0; }
Enemy() : Enemy (1, 1) { }
void move(Player* player, std::vector<Object*>& walls)
{
int nx = x, ny = y;
if (y > player->getY() && !wallCollision(x, y-1, walls)) { ny--; }
else if (y < player->getY() && !wallCollision(x, y+1, walls)) { ny++; }
else if (x > player->getX() && !wallCollision(x-1, y, walls)) { nx--; }
else if (x < player->getX() && !wallCollision(x+1, y, walls)) { nx++; }
if ((nx != x || ny != y) && !wallCollision(nx, ny, walls))
{ hide(); x = nx; y = ny; show(); }
}
};
void create_enemies(std::vector<Enemy*>& enemies, std::vector<Object*>& walls)
{
int posx, posy;
for (int i = 0; i < 1 + rand() % 3; i++)
{
do {
posx = 2 + rand() % 15;
posy = 2 + rand() % 15;
} while (wallCollision(posx, posy, walls));
enemies.push_back(new Enemy(posx, posy));
}
}
int main()
{
int fontSize = Window.getScreenHeight() / 36;
Cursor.setFontPixels(fontSize, fontSize);
Window.setSizeChars(20, 20);
Window.hideBlinking();
std::vector<Object*> walls;
std::vector<Enemy*> enemies;
create_walls(walls);
create_enemies(enemies, walls);
Player *player = new Player(1,1);
int cooldown = 0;
while(!Keyboard.get(VK_ESCAPE))
{
player->move(walls);
for(auto& enemy: enemies)
{
if (cooldown > 1)
{
enemy->move(player, walls);
}
if (enemy -> collision(player -> getX(), player -> getY()))
{
std::cout << "You are dead!";
goto END;
}
}
if (cooldown++ > 1)
{
cooldown = 0;
}
Keyboard.wait(30);
}
END:
walls.clear();
enemies.clear();
return 0;
}
Treasure
Treasure is simple object, could be new class but it is not really necessary. If player collides with treasure, he wins the game. When moving with enemy if I detect collision with treasure, I repaint the treasure (otherwise treasure would remain hidden once enemy moves over it). Choosing treasure position is done simply, however in rare situations treasure will be stuck inside walls and player will be unable to reach it. To solve this I would use path search algorithm, but it is outside the scope of this tutorial.
void Enemy::move(Player* player, Object* treasure, std::vector<Object*>& walls)
{
int nx = x, ny = y; // redraw enemy only if neccessary
if (y > player->getY() && !wallCollision(x, y-1, walls)) { ny--; }
else if (y < player->getY() && !wallCollision(x, y+1, walls)) { ny++; }
else if (x > player->getX() && !wallCollision(x-1, y, walls)) { nx--; }
else if (x < player->getX() && !wallCollision(x+1, y, walls)) { nx++; }
if ((nx != x || ny != y) && !wallCollision(nx, ny, walls))
{
if (!treasure->collision(x, y))
{ hide(); }
else
{ treasure->show(); }
x = nx;
y = ny;
show();
}
}
Object* create_treasure(std::vector<Object*>& walls)
{
int posx, posy;
do {
posx = 8 + rand() % 5;
posy = 8 + rand() % 5;
} while (wallCollision(posx, posy, walls));
return new Object(posx, posy, 'o', LIGHTYELLOW);
}
int main()
{
...
Object* treasure = create_treasure(walls);
while(!Keyboard.get(VK_ESCAPE))
{
player->move(walls);
if (treasure -> collision(player -> getX(), player -> getY()))
{
std::cout << "You got treasure";
goto END;
}
...
}
...
}
Easier main
From the last two paragraphs it is obvious that main is too large. I will add two functions to make it easier to read. Game function will control one game instance and after player wins or dies it ends. Also I will add end function that will print fancy text and handle clearing of memory.
void end(std::string text, Color col, Player *player,
std::vector<Object*>& walls, std::vector<Enemy*>& enemies, Object* treasure)
{
player->show();
Cursor.setPosition(text.length() > 15 ? 1 : 3, 5);
Cursor.setColor(col);
std::cout << text;
Keyboard.waitUser();
Cursor.clearScreen();
walls.clear();
enemies.clear();
delete player;
delete treasure;
}
bool game()
{
...
}
int main()
{
int fontSize = Window.getScreenHeight() / 36;
Cursor.setFontPixels(fontSize, fontSize);
Window.setSizeChars(20, 20);
Window.hideResize();
Window.hideBlinking();
Window.hideScrollbars();
while(game()) { }
return 0;
}
Full code
Code for board game is following. On the github you can find similar code with more comments for this game.
#include <iostream>
#include <vector>
#include <time.h>
#include "swti/swti.hpp"
class Object
{
protected:
int x, y;
char skin;
Color color;
public:
Object(int xpos, int ypos, char type, Color col) :
x(xpos), y(ypos), skin(type), color(col) { show(); }
Object(int xpos, int ypos) : Object(xpos, ypos, (char) FILL_F, GRAY) { }
Object() : Object(0, 0) { }
void show()
{ Cursor.printChar(x, y, skin, color); }
void hide()
{ Cursor.printBlank(x, y); }
bool collision(int xt, int yt)
{ return x == xt && y == yt; }
};
bool wallCollision(int x, int y, std::vector<Object*>& walls)
{
for(auto wall: walls)
if (wall -> collision(x, y))
return true;
return false;
}
void create_walls(std::vector<Object*>& walls)
{
int posx, posy;
for (int i = 0; i < 18; i++)
{
walls.push_back(new Object(i,0));
walls.push_back(new Object(i+1,18));
walls.push_back(new Object(0,i+1));
walls.push_back(new Object(18,i));
}
for (int i = 0; i < 100; i++)
{
posx = 2 + rand() % 15;
posy = 2 + rand() % 15;
if (posx != 1 && posy != 1 && !wallCollision(posx, posy, walls))
{ walls.push_back(new Object(posx, posy)); }
}
}
class Player : public Object
{
public:
Player(int px, int py) : Object(px, py, '*', LIGHTGREEN) { }
Player() : Player (1, 1) { }
int getX() { return x; }
int getY() { return y; }
void move(std::vector<Object*>& walls)
{
int nx = x, ny = y;
if (Keyboard.get('W') && !wallCollision(x, y-1, walls)) { ny--; }
if (Keyboard.get('S') && !wallCollision(x, y+1, walls)) { ny++; }
if (Keyboard.get('A') && !wallCollision(x-1, y, walls)) { nx--; }
if (Keyboard.get('D') && !wallCollision(x+1, y, walls)) { nx++; }
if ((nx != x || ny != y) && !wallCollision(nx, ny, walls))
{ hide(); x = nx; y = ny; show(); }
}
};
class Enemy : public Object
{
public:
Enemy(int px, int py) : Object(px, py, (char) 158, LIGHTRED) { }
Enemy() : Enemy (1, 1) { }
void move(Player* player, Object* treasure, std::vector<Object*>& walls)
{
int nx = x, ny = y; // redraw enemy only if neccessary
if (y > player->getY() && !wallCollision(x, y-1, walls)) { ny--; }
else if (y < player->getY() && !wallCollision(x, y+1, walls)) { ny++; }
else if (x > player->getX() && !wallCollision(x-1, y, walls)) { nx--; }
else if (x < player->getX() && !wallCollision(x+1, y, walls)) { nx++; }
if ((nx != x || ny != y) && !wallCollision(nx, ny, walls))
{
if (!treasure->collision(x, y))
{ hide(); }
else
{ treasure->show(); }
x = nx;
y = ny;
show();
}
}
};
void create_enemies(std::vector<Enemy*>& enemies, std::vector<Object*>& walls)
{
int posx, posy;
for (int i = 0; i < 1 + rand() % 3; i++)
{
do {
posx = 2 + rand() % 15;
posy = 2 + rand() % 15;
} while (wallCollision(posx, posy, walls));
enemies.push_back(new Enemy(posx, posy));
}
}
Object* create_treasure(std::vector<Object*>& walls)
{
int posx, posy;
do {
posx = 8 + rand() % 5;
posy = 8 + rand() % 5;
} while (wallCollision(posx, posy, walls));
return new Object(posx, posy, 'o', LIGHTYELLOW);
}
void end(std::string text, Color col, Player *player,
std::vector<Object*>& walls, std::vector<Enemy*>& enemies, Object* treasure)
{
player->show();
Cursor.setPosition(text.length() > 15 ? 1 : 3, 5);
Cursor.setColor(col);
std::cout << text;
Keyboard.waitUser();
Cursor.clearScreen();
walls.clear();
enemies.clear();
delete player;
delete treasure;
}
bool game()
{
Player *player = new Player(1,1);
std::vector<Object*> walls;
std::vector<Enemy*> enemies;
int cooldown = 0;
srand((int)time(NULL));
create_walls(walls);
create_enemies(enemies, walls);
Object* treasure = create_treasure(walls);
Keyboard.waitUser();
while(!Keyboard.get(VK_ESCAPE))
{
player -> move(walls);
if (treasure -> collision(player -> getX(), player -> getY()))
{
end("You got treasure!", LIGHTYELLOW, player, walls, enemies, treasure);
return true;
}
for(auto& enemy: enemies)
{
if (cooldown > 1)
{
enemy -> move(player, treasure, walls);
}
if (enemy -> collision(player -> getX(), player -> getY()))
{
end("You are dead!", LIGHTRED, player, walls, enemies, treasure);
return true;
}
}
if (cooldown++ > 1)
{
cooldown = 0;
}
Keyboard.wait(30);
}
end("See you soon.", LIGHTYELLOW, player, walls, enemies, treasure);
return false;
}
int main()
{
int fontSize = Window.getScreenHeight() / 36;
Cursor.setFontPixels(fontSize, fontSize);
Window.setSizeChars(20, 20);
Window.hideResize();
Window.hideBlinking();
Window.hideScrollbars();
while(game()) { }
return 0;
}