roguelike-tutorial

Roguelike Tutorial Revised ported to C

View on GitHub

Chapter 02: Scuba Diving

Before you start this chapter, be aware that we will spend a significant amount of time discussing language features of C. It’s a lot to learn and I’m only skimming the basics on each subject, so take your time and really try to wrap your head around each concept as it comes up. Most of what we do in this chapter will be learning about C.

Our player is currently a hardcoded @ in a terminal_print() and two integers in main(). It does not make sense to keep these values in separate places, as the data is all related. Since we have data that needs to be structured together, we can use a struct.

Under The C: Structuring Data

If you’re experienced with another programming language, then you’re probably familiar with the concept of classes and object oriented programming. C doesn’t have that, at least within the language. There are OOP libraries out there you can use, but we’re not going to use one for this tutorial. Honestly most of the time OOP is unneccesary unless it’s a main feature of your language, like in C# or Java.

So how do we structure related data together? While C may not have the concept of a class, it does have struct, a user-defined type that’s a collection of other data types. Let’s say we want to represent a point on a two-dimensional grid. Here is how we would do that in C:

struct {
    int x;
    int y;
} Point;

Point.x = 2;
Point.y = 1;

Now we have a struct named Point that can hold 2 integers. If you want to access the x coordinate, you use Point.x. The y coordinate is Point.y.

The big takeaway here is that the above code only defines a single struct named Point. If you want to create an actual type, and then make variables of that type, we need to define the tag of the struct like so:

struct Point {
    int x;
    int y;
};

struct Point p;
p.x = 2;
p.y = 1;

struct Point p2;
p2.x = 3;
p2.y = 4;

Yes, you have to use the struct keyword when talking about the type. No, it’s not the most straightforward syntax. When you define a struct, the pieces look something like this:

struct TAG {
    DATA_MEMBERS
} NAMES;

The TAG section is where we can “tag” the struct with a type name. This lets us make new variables of the type of the struct as long as we put the struct keyword in front of the type name. The following will cause the compiler to throw an error:

struct Point {
    int x;
    int y;
};

Point p; /* ERROR: should have been struct Point p */
p.x = 2;
p.y = 1;

Notice that I did not put the struct keyword when I declared p. This is a syntax error as the compiler needs that struct keyword to know that Point is a tag to a struct.

Now if you have some familiarity with C, you may be raising your hand and saying “ooh ooh what about typedef”. It is possible to use a typedef to define a new type, meaning you do not have to use the struct keyword whenever you reference the type’s tag:

typedef struct Point {
    int x;
    int y;
} Point;

Point p;
p.x = 2;
p.y = 1;

Looks great right? Well, that’s a big point of contention with C developers. Some people love typedef, others abhor it. Personally I do not use typedef for structs. It obfuscates that the type is actually a struct, which leads to programmer errors. Programmers are fallible, and I can’t fathom how saving yourself from writing the word struct now and again is worth opening yourself up to bugs and errors. I’m sure half of you are already disagreeing with me, but such is life with technology holy wars. For this tutorial I will be using struct definitions instead of typedef, but if you disagree feel free to use typedef.

Ready Player One

Anyway, we need to get our player’s data inside a single struct. In order to do so, we first need to define the interface that the rest of our code can use to interact with the player.

Let’s create a new set of files, include/player.h and src/player.c. player.h is where we will declare our interface, and player.c is where we will define the functions. Remember, headers are for saying what the code is and source files are where we define what the code does. Here’s the first pass of player.h:

#ifndef RTUT_PLAYER_H
#define RTUT_PLAYER_H

struct Player {
    int x;
    int y;
    char* symbol;
};

void init_player(struct Player *player);
void move_player(struct Player *player, int x, int y);
void set_pos_player(struct Player *player, int x, int y);
int handle_input_player(struct Player *player, int key);

#endif

Woah, what is all that? We’ve got a few new concepts to talk about. Let’s start with the new preprocessor macro you see at the top.

Under The C: Header Guards

Remember last time when we talked about how #include copies the entire header into your source file before the compiler gets its hands on it? Let’s extrapolate and see what happens when two imports clash:

/* a.h */
int function_a();
/* b.h */
#include "a.h" /* a function in b.h needs dothing() from a.h */

int function_b();
/* main.c */
#include "a.h"
#include "b.h"

int main(int argc, char** argv) {
    function_a();
    function_b();
}

In the above example, main.c includes both a.h and b.h. When the preprocessor runs on main.c it will copy the contents of both header files into main.c, which means it will now look like this:

/* main.c */
int function_a();
#include "a.h"
int function_b();

int main(int argc, char** argv) {
    function_a();
    function_b();
}

Because the preprocessor runs until there are no more macros, it does a second pass on main.c:

/* main.c */
int function_a();
int function_a();
int function_b();

int main(int argc, char** argv) {
    function_a();
    function_b();
}

Now we have two declarations of function_a(). When main() calls function_a(), the compiler will not know which declaration is correct and throws a compile-time error.

So how do we handle this? The same way the C standard library does, by providing header guards. Header guards look like this:

#ifndef HEADER_GUARD_NAME
#define HEADER_GUARD_NAME

/* header code */

#endif

The first new macro, #ifndef, looks if the macro HEADER_GUARD_NAME is not defined. Since it hasn’t been defined yet, the preprocessor ignores the macro and continues. The next line defines HEADER_GUARD_NAME. Notice that there is no second argument to this #define. That is fine, because the preprocessor still recognizes the macro’s name even if it doesn’t get swapped to anything. #ifndef requires an #endif, which we put at the bottom of the header file.

So let’s say our above header gets copied twice into one source file. The first time the preprocessor passes through the code, it sees that HEADER_GUARD_NAME is not defined and keeps going. We define the macro and all our header code beneath it is put into the source file. The #endif is ignored because we ignored the #ifndef.

When the preprocessor hits the second #ifndef and sees that HEADER_GUARD_NAME is defined, even though it’s defined to nothing. This causes the preprocessor to not copy any code between the #ifndef and #endif. This way we do not have two copies of a header in a processed source file.

Therefore, you should always write a header guard as your first step when writing a header. Look at the top of BearLibTerminal.h and you will see a standard header guard as the first line after the opening comments.

And for those of you once again raising your hands and asking about #pragma once, do not use it. If you do not know, putting #pragma once at the beginning of a header is a substitute for a header guard that requires less typing. This is not part of the C standard and is a GNU extension that most major compiler vendors have adopted. Since it’s not part of the C standard, you should not use it!

Continuing Our First Header

Here’s the code for our player.h again:

#ifndef RTUT_PLAYER_H
#define RTUT_PLAYER_H

struct Player {
    int x;
    int y;
    char *symbol;
};

void init_player(struct Player *player);
void move_player(struct Player *player, int x, int y);
void set_pos_player(struct Player *player, int x, int y);
int handle_input_player(struct Player *player, int key);

#endif

Now you can see our header guard clearly wrapping the file. As I said in the last section, always do this first.

Our struct Player is pretty simple right now, it just holds the data we were using previously. As we expand our game we will be coming back often to add more data to the player. The first two variables are the x and y coordinates of where the player is. symbol will hold the @ that represents our player in the game world.

So what is that little * next to symbol? Remember when I said this chapter was full of C concepts? Here’s the biggest one: pointers.

Under the C: Pointers Part One

We’re going to break up pointers into multiple sections as they are a major topic with tons of use cases and nuances. For now, let’s start with the basics. Pointers are variables that instead of holding a value, they hold an address in memory. So a char * is a memory address to a char, or character type. An int * is a variable that holds a memory address, and that address has an int in it.

Because pointers point to an address, you can do something like this:

int i = 10;
int *p = &i;
*p = 20;

After line 2, p now points to the address of i. That’s what the & in front of i means; it says “I do not want the value of 10 that i holds, give me its address in memory instead”.

Next, we dereference p, which means we want to work with the data at the memory address, not the address itself. If we were to type p = 20 instead of *p = 20, then p would be set to the literal memory address 20, which is probably not in the memory our program gets to work with and will cause all kinds of errors. Working with what we have learned so far, you cannot assign memory to a pointer. Later we will learn all about dynamic memory and assigning new memory to a pointer, but that’s for another lesson.

So what does all that mean? Here’s some code that might help explain it:

int i = 2; 

int *p1 = &i; /* p1 is equal to the memory address that i lives at */

*p1 = 5;      /* The value that p1 points to is now 5. That means i is also 5 */

assert(i==5)  /* Will evaluate to true, as dereferencing and modifying p1 will
               * modify the same int in memory that i is set to
               */


p1 = 10;      /* Not an error, but now we've set p1's memory address to 10 instead
               * of changing its underlying value
               */

int *p2 = i;  /* p2 points to the literal address 5 */

int *p3 = 2;  /* p3 points to the literal address 2,
               * not a value with the number 2
               */

As you can see, there is a lot of room for slipping up with pointers. The syntax is admittedly arcane and easy to screw up.

So why even mess with them? Pointers are very useful for manipulating data efficiently. For example, lets say we have a huge struct with hundreds of data members in it. Every time a function that uses our megastruct as an argument is called, we pass the megastruct as the first parameter. Because everything in C is passed by value, this means that every single time we called that function, the program must copy the entire megastruct into the new function. If that struct takes up a few MB, that is a lot of memory copying!

Pointers solve this problem because they’re always the same size, no matter what the data is. Instead of copying our multi-MB struct into each function call, we pass a single 64-bit pointer instead of this huge chunk of data… this is assuming you’re using a 64-bit OS. On a 32-bit OS the addresses are 32 bits.

Once a function has the pointer, it can access all the tasty data inside. This makes it really easy for functions to pass around and modify data efficiently without copying huge chunks of memory.

Another annoying syntax issue with pointers is that when declaring a pointer, the * can go on either the variable name or the type name:

/* These are the same thing */
int* p;
int *p;

/* So are these function definitions */
void func(int* p);
void func(int *p);

Some people prefer the first, some the second. It’s another holy war to argue over with good arguments on both side. When I learned C, the book I used did the int *p syntax, so that’s what I use.

When you dereference a pointer the * must be at the beginning of the name of the variable:

*p = 4; /* valid */
p* = 4 /* invalid */

You will pick up on the nuances of pointer syntax as you learn more about C, it’s just one of those things that takes time and experience to nail down.

I know this is a lot to learn all at once, and it’s something that takes practice to get down. we will have lots of opportunities to use pointers moving forward. If you are feeling lost, here’s several other resources that are probably way better at explaining pointers than I am:

Under The C: Stringing Me Along

So, back to our code. You’ll recall that Player contains a char *, or pointer to a char. Now that you’re familiar with pointers it’s time to talk about how they relate to arrays and C strings.

C does not have the concept of strings like modern programming languages do. In Python or Java, a string is an object that holds its characters and some metadata. They usually know how large they are and how many characters they hold.

C strings do not have that. They have no idea how big they are. They do not know how their memory is managed and do not care. A C string is nothing more than a pointer that holds an address to an array of char somewhere in memory. That’s right, C strings are simply an array of characters. Here’s an example:

char *s = "Hello";

Sidenote, you may notice that I assigned actual data to a pointer, which in the last section I said you can’t do. C strings are an exception to the rule, because the compiler will make sure that the program sets up the memory for the string when it starts.

Under the hood, a C string is just an array of char with a null byte at the end. What’s a null byte? Just a single byte set to zero. So the actual memory of our string above looks like:

--------------------------
| H | e | l | l | o | \0 |
--------------------------

The \0 represents a null byte. When we do a string operation like using printf() or passing a string to terminal_print(), what we’re actually passing is a pointer to the character H. The function then reads the string char by char until it hits a zero, or \0. If there is no null byte, then it will just keep reading on forever and cause your program to crash or do bizzare things.

Ok Professor, Can We Just Make A Game Now?

By this point I hope your brain hasn’t melted out of your ears. As we keep working on the game itself, there will be plenty of chances to see these concepts in action and learn them.

For the last time, let’s look at player.h

#ifndef RTUT_PLAYER_H
#define RTUT_PLAYER_H

struct Player {
    int x;
    int y;
    char* symbol;
};

void init_player(struct Player *player);
void move_player(struct Player *player, int x, int y);
void set_pos_player(struct Player *player, int x, int y);
int handle_input_player(struct Player *player, int key);

#endif

So we have our struct Player with its two int and a char *. We also declare four functions. Note that all of them take a pointer to a struct Player. We use move_player() to modify the player’s position relative to the coordinates passed in. set_pos_player() will just set the player’s x and y coordinates to whatever is passed in. we will use handle_input_player() to handle the player’s inputs.

The last three functions make sense for taking a pointer to our player. They modify the data inside player, so using a pointer here makes sense. However, you may be wondering why init_player() takes a pointer to an already existing struct Player instead of returning one. This is more of a design decision than a technical one. Our other functions always start with a pointer to a struct Player, so it makes sense to have our API be consistent. It also allows the caller of init_player() to decide how the memory for player is handled instead of letting other code decide, which is pretty normal for C library design. While we aren’t making a library, I always try to keep consistentcy in design patterns.

So let’s implement our three new functions. Here is the beginning of player.c:

#include "BearLibTerminal.h"
#include "player.h"

void init_player(struct Player *player)
{
    player->x = 1;
    player->y = 1;
    player->symbol = "@";
}

We use this function to set our player’s values to a base starting point. You may be wondering what the -> is. Because player in this function is a pointer, not an actual struct Player, we need to dereference it, or get to the data that the pointer is pointing to. The arrow operator, or ->, is a shortcut for dereferencing and accessing data inside a struct pointer. If we didn’t use the arrow, we would first need to dereference the pointer like so: *(player).x. That’s ugly and hard to read, so the arrow operator is the way to go. Just remember: When getting to members of a struct, use the dot operator . for an actual struct and the arrow operator -> for a struct pointer.

Also, because our definition of struct Player is in player.h we need to include it or player.c won’t know what a struct Player is. If the functions in player.c were not using struct Player, then we would not need to include it at all. The compiler will still be able to match our header to our source file.

We need to include BearLibTerminal.h as well for our input handling function.

Next up, let’s define our two movement functions.

void move_player(struct Player *player, int x, int y)
{
    player->x += x;
    player->y += y;
}

void set_pos_player(struct Player *player, int x, int y)
{
    player->x = x;
    player->y = y;
}

Once again, we use the arrow operator to access the members inside our struct pointer. move_player() is for relative movement, set_pos_player() is for setting a specific position.

Now, we have that giant switch statement that handles player movement in main(). It seems to me that player movement should probably be defined in player.c, not in main(). So lets take that entire switch statement from main(), move it into handle_input_player(), and modify it.

int handle_input_player(struct Player *player, int key)
{
    switch (key) {
    case TK_UP:
        move_player(player, 0, -1);
        break;

    case TK_DOWN:
        move_player(player, 0, 1);
        break;

    case TK_LEFT:
        move_player(player, -1, 0);
        break;

    case TK_RIGHT:
        move_player(player, 1, 0);
        break;
    case TK_UP:
        player->y--;
        break;
    }
    return 1;
}

As you can see, I’ve moved the switch from main() and modified it to use move_player() instead of directly changing a variable. key is our input from terminal_read() in main() that we will pass in during a call to handle_input_player().

We’ve changed the return type to an int so that we can pass a true or false value. Remember that C does not have boolean values in the language itself, so we use an int set to 1 for true or 0 for false. If key shows that the close button or ESC was pushed, then it returns 0, which is a false value. Otherwise, ` is returned which is a true value.

Putting Our Player Together

Let’s modify main.c to use our new struct Player. Let’s start with modifying our variables and adding player.h as an #include.

#include "BearLibTerminal.h"
#include "player.h"

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

    int key = 0, exit_game = 1;
    struct Player player;
    init_player(&player);

We first create our struct Player, aptly named player, and then pass it to init_player() to set the default values. Remember that because init_player() wants a pointer to a struct Player, which is just an address in memory, we can pass the address of player by using the & operator.

Next, we will change terminal_print() to print the coordinates of player and simplify the input handling code. This game loop of main() now looks like this:

    while (exit_game) {
        terminal_clear();

        terminal_print(player.x, player.y, player.symbol);

        terminal_refresh();

        key = terminal_read();
        exit_game = handle_input_player(&player, key); 
    }

Our entire new main.c looks like this:

#include "BearLibTerminal.h"
#include "player.h"

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

    int key = 0, exit_game = 1;
    struct Player player;
    init_player(&player);

    while (exit_game) {
        terminal_clear();

        terminal_print(player.x, player.y, player.symbol);

        terminal_refresh();

        key = terminal_read();
        exit_game = handle_input_player(&player, key); 
    }

    terminal_close();
    return 0;
}

Go ahead and run make and run your game. It’s.. the same! Which is good. We’ve done some major refactoring of the code and everything still works. Originally this chapter was going to go into making a basic map as well, but holy cow has it gotten long. we will take a break for now and dive into setting up our map next time.

In the next chapter, we will actually make our first dungeon. A simple one to be sure, but a welcome one.

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.