On this post we are going to start developing our first game. The game we will be writing is going to be an adventure with ASCII graphics, meaning that we will present our world using console output. On the posts that will follow we will expand on this game, divert it to something different and maybe even start using real graphics.
Our goal for this post, is to print a basic map and move our character using console input in the form of north-south-east-west [nsew].
#define
#define is another pre-processor directive. Like we saw before, pre-processor is a program that runs before the compiler and performs text related tasks. #define defines text replaces.
#define SEARCH REPLACE
Every occurrence of SEARCH will be replaced with REPLACE
#define MAX 5
int main(){
int x = MAX;
return 0;
}
On the example above, the compiler ‘sees’:
#define MAX 5
int main(){
int x = 5;
return 0;
}
#defines have a lot of disadvantages but are very easy to use. For small programs they cannot inflict a lot of harm. However, the dumb and simple nature of the pre-processor can lead to some odd error messages, since no type or other checking is performed during that step. There are better alternatives to #defines that we will be seeing later on.
If
When running a program, we need to able to run code only under certain conditions. This is achieved using the if control structure:
if(boolean_expression)
single_statement;
if(boolean_expression)
single_statement;
else
another_statement;
if(boolean_expression){
block_of_code;
}
else {
another_block_of_code;
}
if(boolean_expression){
block_of_code;
}
else if(different_boolean_expression)
single_statement;
else if(different2_boolean_expression)
single_statement;
else {
block_of_code;
}
If boolean_expression evaluates to true, then the code will run, otherwise the code will be skipped.
There are two versions, the one uses a code block ({}) and the other a single statement. Code blocks are very similar to statements, with the difference that they don’t return anything. Ifs also have else sections that run when the condition expression is evaluated to false. Else section is optional and can be omitted.
if(…){} is also a statement. That way you can chain else blocks in order to create an else if structure, that multiple conditions are checked back-to-back. We could argue that all versions are special cases of a general rule.
With some thought, it is easy to come to the conclusion, that if we reach to check a condition, all the previous conditions in the if,else if block have to be false.
The following code checks if the variable x is in (-infinity, -100],(-100,0),[0,infinity) sets and prints an appropriate message
if(x <= -100){
std::cout << "x in (-infinity, -100]"
}
else if(x < 0)
std::cout << "x in (-100, 0)";
else
std::cout << "x in [0, +infinity)";
Notice that, we do not have to check for both the edges of the sets, since the one of the edges was checked on the previous else if section.
while
While is like if, but it runs on a loop. While the condition is true, the block continues to run.
while(boolean_expression)
expression;
Of course, expression can be a single statement, a code block, can be an if, or even another while!
A common use for while, is to iterate over a number sequence like i=0,1,2,3,4,5,6,…,M-1. Code for such iteration given below
int i = 0;
while(i < M)
std::cout << i++;
In C and C++ it is standard practice to start counting from 0 and check with less operator. It will be much clearer why later on, when we take a look on pointers.
Mini-boss
If we wanted to print the numbers i=1,…M instead of i=0,..M-1, what changes we should do on the previous snippet of code?
for
The previous while structure is so common, that there is a loop structure mostly for that! Meet for:
for(initial_expression; check_expression; at_end_of_iteration)
expression;
for(int i = 0;i < M;++i){
std::cout << i << "\n";
}
At the start of the loop, the initial expression is evaluated. Then, before entering the loop the check_expression is evaluated and if it true, then the loop continues, else it stops. At the end of every iteration and before the check, the third part of for is evaluated.
Mini-boss
rewrite the previous for, using while
do … while
Another loop structure is do .. while. I personally don’t use it very much, but here you go:
do {
block_of_code;
} while(boolean_expression);
Block of code is run at least once. Then the boolean_expression is evaluated. If it is true, it continues. Execution of the loop is stopped otherwise.
What you have to keep in mind is that all loop structures can be rewritten to an equivalent one, using a different loop structure.
break, continue
You can ‘escape’ from a loop early using theĀ break; statement. You can also skip an iteration and go to the next using the continue; statement.
int x = 0;
while(true){
if(x++ > 10)
break;
std::cout << x << "\n";
}
for(int i=0; i < 20; ++i){
if(i == 10)
continue;
std::cout << i << "\n";
}
Reading from console
You should have learnt by now that console output uses std::cout and operator <<. Like cout, cin supports multiple types. Just keep in mind, that getting types mixed up (like providing character input when expecting number) can lead to the program breaking at the point of no return.
std::cin >> variable_to_be_read_into;
Game loops
A very important concept of video games and a lot of other programs, is that of the game loop. During every iteration of the game loop, a frame is simulated and drawn on display. The game loop keeps running until we exit the game.
while(true){
check_input;
simulate_world;
update_display;
}
At check_input phase, we process inputs from the player. Then we simulate the “world” of our game. At the end, we update the display. Of course, there are more sophisticated versions of this loop, but the idea remains mostly the same.
modulo
An useful operator is modulo. It is the remainder of the integer division between two numbers and it is the operator %. For example:
std::cout << (10 % 3);//1
std::cout << (11 % 3);//2
std::cout << (12 % 3);//0
As you can see it loops around. In order to produce a sequence of numbers like this: 0,1,2,...,N,0,1,2,...,N,...
You can write code like this:
for(int i=0; i<100000; ++i){
std::cout << (i % (N+1));
}
The first version of the game
Now that we have covered all the base knowledge required, we can move on to the actual game code
#include <iostream>
#define MAP_SIZE_X 5
#define MAP_SIZE_Y 4
#define EMPTY_TILE_SYMBOL '.'
#define DOOR_TILE_SYMBOL '$'
int main()
{
int character_x = 2;
int character_y = 2;
char character_symbol;
int exit_x = 3;
int exit_y = 1;
std::cout << "Welcome to the dungeon\n";
std::cout << "Choose your character:";
std::cin >> character_symbol;
bool done = false;
while(!done){
//print map
for(int j=0;j<MAP_SIZE_Y;++j){
for(int i=0;i<MAP_SIZE_X;++i){
if(character_x == i && character_y == j)
std::cout << character_symbol;
else if(exit_x == i && exit_y == j)
std::cout << DOOR_TILE_SYMBOL;
else
std::cout << EMPTY_TILE_SYMBOL;
}
std::cout << "\n";
}
char dir;
std::cout << "Choose direction [nsew]:";
std::cin >> dir;
if(dir == 'n'){
character_y--;
if(character_y < 0)
character_y = 0;
}
else if(dir == 's'){
character_y = (character_y + 1) % MAP_SIZE_Y;
}
else if(dir == 'w'){
character_x--;
if(character_x < 0)
character_x = 0;
}
else if(dir == 'e'){
character_x = (character_x + 1) % MAP_SIZE_Y;
}
else
std::cout << "Invalid direction\n";
if(character_x == exit_x && character_y == exit_y)
done = true;
}
std::cout << "You exited the dungeon\n";
return 0;
}
Mini-boss
what parts of the code above correspond to the phases of a game loop?
Mini-boss
try giving "ne" as a single input. What happens and why.
Boss
try editing the file to make the map bigger, change symbols and make the game run for maximum of 5 frames; after that it should stop the loop.