roguelike-tutorial

Roguelike Tutorial Revised ported to C

View on GitHub

Chapter 01: Back @ You

We’ve done the setup, we’ve got a nice little Hello World done, now it’s time to actually start making our game!

Let’s begin by looking at the code in main.c. Here it is again for you:

#include "BearLibTerminal.h"

int main(int argc, char** argv)
{
    terminal_open();

    // Printing text
    terminal_print(1, 1, "Hello, world!");
    terminal_refresh();

    // Wait until user closes the window
    while (terminal_read() != TK_CLOSE) { }

    terminal_close();
    return 0;
}

Let’s step through this line by line.

#include "BearLibTerminal.h"

Like we discussed in Chapter 0, we’re including BearLibTerminal’s header file in main.c so that we can use BLT’s functions. Remember that we use double quotes instead of angle brackets because BearLibTerminal.h is a local header file in our project, not one provided by our system.

Under The C: The main() Function

int main(int argc, char** argv)

main() is defined by the C standard as the entry point to a program. Every C program must have a main() function (technically there are exceptions but they do not apply here). The parameters passed into main() are for processing command line arguments when you run the program. argc is the number of arguments, argv is an array of strings that made up the command.

For example, if you were to type rtut arg1 arg2 right now into your terminal and press enter, rtut will run with the strings arg1 and arg2 passed in as arguments. argc == 3 because the first argument is the name of the program itself, and argv == "rtut", "arg1", "arg2". Note that the prior statement is pseudocode and not valid C.

Now our game does nothing with command line arguments, but they still need to be part of main()’s definition. Though it is true that if you omit argc and argv from main() your program will compile and work, but this behavior goes against the C standard. Personally I try to stay as close to the standards as possible and as such always include argc and argv, even if I am not using them.

Getting into BearLibTerminal

Remember that you can always check out BLT’s documentation if any of its functions do not make sense. That being said, let’s look at what’s needed to get a basic Hello World going.

terminal_open();

This function must be called before any of BLT’s functionality will work. Behind the scenes it starts up our game window and prepares it for drawing.

terminal_print(1, 1, "Hello, world!");

Now we print our “Hello World” string to the terminal. The first two arguments are the position of the string. BLT’s coordinate system starts with 0, 0 at the top left-hand corner of the window and counts up the first value to move right and the second to move down. With terminal_print(), the position put in is where the first character in the string goes. So the H in “Hello World” is printed starting at 1, 1 then the “e” is printed at 2, 1.

But here’s the thing… if we just froze the program right now, say inside a debugger, our window wouldn’t have Hello World printed to it. But we just called terminal_print(), right?

BearLibTerminal, like most graphics libraries, doesn’t draw to the screen with every call to draw something. It turns out that drawing onto your monitor is an expensive operation, so in order to speed up efficency we’re not actually drawing on the screen itself yet. Instead, a draw call like terminal_print() will instead draw into a buffer. Once all of our printing is done and our buffer is ready to be drawn, we will need to tell BLT that it’s time to draw the contents of the buffer onto our actual screen. Since our Hello World only has one print call, now would be a good time to do so.

terminal_refresh();

Here we’re actually taking the buffer and drawing onto the screen. If we paused the program after terminal_refresh() was called, then we would see our black background and Hello World message. If we had multiple terminal_print() calls (and we will have many), then all the things we put into our buffer get drawn at the same time.

Under The C: Scoping and Code Blocks

Our next line of code uses some syntax that might throw you off, so let’s talk about how C handles statement evaluation in a complex statement.

while (terminal_read() != TK_CLOSE) { }

For now let’s ignore what terminal_read() and TK_CLOSE are… We will get there in a bit. Focus on the syntax of this statement. Confusing, no? A better way to write this might be

while (terminal_read() != TK_CLOSE) {
    /* Infinite loop with nothing in it */
    // You can also write a comment like this!
}

So we’ve now got an infinite while loop with some comments in it for clarity. C will happily run this while loop forever until we kill the program. In C, curly braces define a block of code that goes together, such as what should run inside a while loop or an if statement. However, conditional statements can also be written without a block. Both of the following code snippets perform the same operation;

int i = 2;
if (i == 2) {
    i = 3;
}
int i = 2;
if (i == 2)
    i = 3;

If a conditional statement exists and there are no curly braces afterwards, then the conditional statements affects the next statement only. Therefore, both of the above snippets are the same.

However, this is an extremely hard thing to debug when it doesn’t work as intended. For example, the following two snippets are not the same thing!

int i = 2;
if (i == 2) {
    i = 3;
    printf("i is now 3\n");
}
int i = 2;
if (i == 2)
    i = 3;
    printf("i is now 3\n");

The second snippet of code will instead be evaluated by the compiler as

int i = 2;
if (i == 2) {
    i = 3;
}
printf("i is now 3\n");

So our second snippet will always print “i is now 3” no matter what! This is why I always use braces, even if the conditional only has one line of code after it. I strongly suggest that you adopt this practice as well.

Yes, But What Does The Code Do?

while (terminal_read() != TK_CLOSE) { }

So we know that until whatever is in this while loop is false, that we’re in an infinite loop. Let’s find out what it does.

terminal_read() reads in the next input invent that the player puts in. This can be a keyboard stroke or mouse input. This input is represented by an int that terminal_read() returns. TK_CLOSE is a preprocessor macro that maps this seemingly random int into clicking the close button in your game’s window. If you’re scratching your head and saying “A preprocessor whatsit?”, then read on.

Under The C: Preprocessor Macros and You

Hoo boy, this is a huge subject. We’re going to only skim the surface, but understanding the preprocessor is a huge part of writing C. Most C programmers have a love/hate relationship with the preprocessor, as it can cause mind-breaking bugs when using advanced features. However, it is a very useful tool and a necessary one when writing non-trivial C programs.

So what is this mythical preprocessor? It is a piece of the compiler that modifies your code right before the compiler actually compiles the file into a program. When the compiler firsts reads your .c file, it checks for preprocessor directives, which start with a #. Whenever it sees a line starting with #, such as in #include, this is a signal to the preprocessor to modify this line. This is the most common preproccessor macro and you’ll be using this in every C file you write.

So far the only one we’ve seen is #include, specifically for BLT’s header in main.c. #include directives literally copy and paste the entirety of the included file where that #include directive is. So before the compiler gets to look at main.c, the preprocessor first copies all 766 lines of BearLibTerminal.h into main.c.

Now BearLibTerminal.h is full of preprocessor macros. So after the code gets copied into main.c, the preprocessor does another pass of the file and runs those macros. If those macros generate more code with macros, then another pass handles those, and so on. The preprocessor recursively runs through the file until all macros have been resolved.

Now lets look at a new macro, #define. To do so, let’s look at TK_CLOSE. Open up BearLibTerminal.h and browse to line 189. You’ll see

#define TK_CLOSE 0xE0

The #define directive means “When you see the first thing, swap in the second”. Define directives are a great way to create named aliases. In this case, terminal_read()’s way of saying “Hey you just clicked the close button” is to return the number 0xE0 in hex, or 224 in decimal. BLT’s designer probably doesn’t expect the users of BLT to memorize that 0xE0 means a close event, so instead they defined a macro to map the name TK_CLOSE to 0xE0.

Since we have #include "BearLibTerminal.h" in main.c, this means that all of the header is copied onto main.c before the compiler runs, including the definition of TK_CLOSE. So the preprocessor copies the header, then reads the #define directives and looks for places to swap definitions in. That means that when our compiler finally gets the modified code from the preprocessor, the compiler just sees

while (terminal_read() != 0xE0) { }

There are many more preprocessor directives and you can write some insanely complicated macros. We will not be touching more than just a few basic macros here but if you want to learn more there are whole books on just the C preprocessor you can check out. A great free resource for both this subject and C in general is Jens Gustedt’s Modern C

Finishing Up Hello World

terminal_close();

Now that we know the player has closed the game, we use terminal_close() to clean up the resources our window was using. Remember that C has very little automatic memory management, and in this case we need to help it clean up the memory that the terminal uses by calling terminal_close().

return 0;

main() returns an int as a signal to the OS whether it closed successfully or not. You can return a number other than 0 to let your OS know that the program closed with an error. Since we have no errors, we return 0. As with argc and argv you technically do not have to write this line as the compiler will let the code still compile. However, following the standard means that we need to return a value at the end of execution. Also, it helps you look at your code and say “Here is where a normal execution flow will end”.

So… When Do We Make A Game?

I know, that was a lot of information without really getting anywhere. Now we have the fundamentals and can start working on actually doing something with our game. Let’s start by rewriting main.c, starting at the top.

#include "BearLibTerminal.h"

int main(int argc, char** argv)
{
    terminal_open();

So far still the same. We include our header, startup main() and open our terminal.

    int player_x = 1, player_y = 1, key = 0, exit_game = 1;

We’re going to define some variables here. player_x and player_y hold the player’s X and Y coordinates, key holds the return value of terminal_read(), and exit_game is used as a flag later to let our program know when it’s time to close.

    while (exit_game) {

Games are basically one infinite loop running until we exit. We will get more in-depth into game design in a later chapter, but for now we need to understand that all games have what’s called the “game loop”. This is the infinite loop where we start by updating states for things like the player, enemies, and the world. Then we draw everything to the screen. Finally, we handle player input. After those three steps, we start over again.

So why use an integer to manage our game loop? Without including a header from the standard library, C has no true or false that you may have seen in other languages like C++ or Python. Instead, the C standard says that any non-zero value is considered to be true, and any zero value is considered false. We set exit_game to 1 when we declared it, so as long as exit_game is any value other than 0 the loop will continue.

In the C standard library, there is a header called stdbool.h that adds true and false as keywords to the language. Want to know how that’s implemented?

#define true 1
#define false 0

Use it if you want, but I find it easier to not include stdbool.h and just use an int set to 1 or 0.

        terminal_clear();

        terminal_print(player_x, player_y, "@");

        terminal_refresh();

Here’s our first new BLT function, terminal_clear(). This function clears the drawing buffer that we discussed earlier. The reason we have to clear the buffer is that when a buffer is drawn to the screen, a new buffer is created with the contents of the just-drawn buffer. So if we do not clear our new buffer on each iteration of our while loop, then anything we drew previously will still be onscreen. Instead of a single @, we will have an @ on any spot the player has been.

Next we print the player’s @ symbol, using the values of player_x and player_y instead of hard coded values. Note that we use double quotes even for a single character string.

Lastly, we refresh the screen with our freshly cleared and redrawn buffer.

        key = terminal_read();

Next, we read the value of terminal_read() into key. You may be asking yourself what happens when no input is read by terminal_read(). The answer is that terminal_read() is a blocking call. This means that the program effectively halts until input is read. If you do not press a button, terminal_read() sits and waits forever until input is received. For a turn based roguelike this makes complete sense. For a real-time game a different approach is needed, but we’re not concerned with that.

        switch (key) {
        case TK_UP:
            player_y--;
            break;

        case TK_DOWN:
            player_y++;
            break;

        case TK_LEFT:
            player_x--;
            break;

        case TK_RIGHT:
            player_x++;
            break;

        case TK_CLOSE:
        case TK_ESCAPE:
            exit_game = 0;
            break;
        }
    }

Now we need to figure out what to do with the input we received. If you’re not familiar with switch statements, they’re basically a different way of doing a bunch of “if then else” statements. We switch on the value of key, and each case statement checks to see if it matches key. The break; at the end of each statement causes the switch to exit. Without the break the switch statement continues to execute the next case.

There is inherent danger in using switch statements as a single missing break can cause confusing and hard to debug behavior. The way I handle this is to always follow a case with a break before filling in the actual code.

TK_UP is the define directive that maps to the keyboard’s up arrow. If it is pressed than we decrement player_y by one. Since our the Y axis of our coordinate grid starts at 0 and goes down, subtracting one from player_y will cause our @ to move one cell up on the screen. If key does not equal TK_UP, we move on to see if it matches TK_DOWN and so on.

The last case is special in that there are two case labels next to each other. This means that if either case is true, we should set exit_game to 0. TK_CLOSE is the signal for closing the window, and TK_ESCAPE is the ESC key. I like to bind ESC to closing the program when doing initial development as you have an easy way to quickly close the program. We will remove ESC as a quit button once the project is closer to completion.

The switch requires curly braces to hold its cases, so we close it off when we’re done. We follow up by closing our game loop’s brace as well.

    terminal_close();
    return 0;
}

Any code after our loop is cleanup code, so we use terminal_close() and return 0 to gracefully exit our game.

Here is the complete code for main.c:

#include "BearLibTerminal.h"

int main(int argc, char** argv)
{
    terminal_open();

    int player_x = 1, player_y = 1, key = 0, exit_game = 1;

    while (exit_game) {
        terminal_clear();

        terminal_print(player_x, player_y, "@");

        terminal_refresh();

        key = terminal_read();
        
        switch (key) {
        case TK_UP:
            player_y--;
            break;

        case TK_DOWN:
            player_y++;
            break;

        case TK_LEFT:
            player_x--;
            break;

        case TK_RIGHT:
            player_x++;
            break;

        case TK_CLOSE:
        case TK_ESCAPE:
            exit_game = 0;
            break;
        }
    }

    terminal_close();
    return 0;
}

Go ahead and run make, then try the game. You should have an @ moving around the screen! You’ll notice that you can run your player character right off the edge of the screen, as we have no logic defining what moves are valid. Try running your character off the side of a screen a few steps then bringing it back onto the screen.

In the next chapter, we will enhance our player into its own object and learn about some game design concepts. We will also work on breaking our code up into smaller units to increase readability and separate functionality.

If you’re stuck or getting weird issues, compare your code to the code above. A missing line of code could cause all kinds of weird behavior. You can also visit the git branch for this chapter and compare your code to the tutorial’s.