DEV Community

Cover image for The C Roguelike Tutorial - Part 2: The Map
Ignacio Oyarzabal
Ignacio Oyarzabal

Posted on

The C Roguelike Tutorial - Part 2: The Map

In this part of the tutorial we will set up the tile system we will use for the map, set all of the tiles in our map to be walls, and then start carving a room for our player to move around in. Before we get into defining our map tiles, let's take some time to refactor our main.c code to separate the setup code and the game loop code into their own functions.

Create a new file in src/ called engine.c and add the following code into it:

#include <rogue.h>

void cursesSetup(void)
{ 
  initscr();
  noecho();
  curs_set(0);
}

void gameLoop(void)
{ 
  int ch;

  mvaddch(player->pos.y, player->pos.x, player->ch);

  while(ch = getch())
  { 
    if (ch == 'q')
    { 
      break;
    } 

    handleInput(ch);
    clear();
    mvaddch(player->pos.y, player->pos.x, player->ch);
  } 
} 
Enter fullscreen mode Exit fullscreen mode

This is the same as the setup and loop code found in main.c but separated into individual functions.

In Part 1 we used the calloc() function to dynamically assign memory needed for our Entity* player variable. Whenever you dynamically assign memory to pointers in this way, it is good practice to free this memory when you don't need it anymore, since C does not do this automatically. We can use the free() function defined in the stdlib.h header file to do so. Since the player pointer should be freed when the game closes, let's take this opportunity to make a function we can run to process all the code needed at the end of the program.

Still on engine.c, append the following code beneath the gameLoop() function:

...

void closeGame(void)
{ 
  endwin();
  free(player);
} 
Enter fullscreen mode Exit fullscreen mode

The free() function takes a pointer argument and releases the memory allocated to it so that the program can reuse that memory later. If you continuously allocate memory for pointers and you don't free them when no longer in use, your program will incur memory leaks which can eventually lead your program to terminate unexpectedly due to a lack of available memory. You can read more about free() here.

Before we can use these functions in our main.c, we need to add their declarations in our rogue.h file, like so:

...
} Entity;
+
+//engine.c functions
+void cursesSetup(void);
+void gameLoop(void);
+void closeGame(void);

// player.c functions
...
Enter fullscreen mode Exit fullscreen mode

Now we can use these functions in our main.c to simplify it. By removing all the now duplicate code and replacing it with the appropriate calls main.c now looks like this:

#include <rogue.h>

Entity* player;

int main(void)
{
  cursesSetup();

  Position start_pos = { 10, 20 };
  player = createPlayer(start_pos);

  gameLoop();

  closeGame();

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

That looks a lot nicer! Compile the game again to verify that it works the same as before. Now we're ready to start defining our map tiles.

The Tile Struct

Tile structs will contain the information of each individual map tile. These tiles will represent our floors, walls and staircases. Open your rogue.h file and add the following struct definition in between your Position and Entity structs:

...
} Position;

typedef struct
{
  char ch;
  bool walkable;
} Tile;

typedef struct
...
Enter fullscreen mode Exit fullscreen mode

For now our Tile structs will only have a ch member to represent how to draw them and a walkable member to allow the player to walk on floors but not through walls. We'll add a few more features later on in the tutorial. We don't give our Tile structs a Position member because we are going to use them in a 2-d array which will intuitively manage the positions of our tiles through its indices.

In order to create our 2-d map array we should define the dimensions of our map with two constants. Add the following to our main.c file:

#include <rogue.h>
+
+const int MAP_HEIGHT = 25;
+const int MAP_WIDTH = 100;

Entity* player;
...
Enter fullscreen mode Exit fullscreen mode

And now add these constants as externs in rogue.h:

...
// externs
+extern const int MAP_HEIGHT;
+extern const int MAP_WIDTH;
extern Entity* player;
...
Enter fullscreen mode Exit fullscreen mode

We also want to add an extern for an array of tiles we'll use to draw our map:

...
// externs
extern const int MAP_HEIGHT;
extern const int MAP_WIDTH;
extern Entity* player;
+extern Tile** map;
...
Enter fullscreen mode Exit fullscreen mode

And of course, let's add this variable to main.c as well:

...
Entity* player;
+Tile** map;

int main(void)
...
Enter fullscreen mode Exit fullscreen mode

The map variable is a pointer to pointers to Tile structs. In other words, the first pointer will point to an array of pointers, each of which will point to an array of tiles. In this way, we get a two dimensional array of tiles which we will be able to use with the notation map[y][x] to access the individual tiles. We'll need a function that allocates the memory for all the rows of tiles. Create a new file in src/ called map.c and add the following code to it:

#include <rogue.h>

Tile** createMapTiles(void)
{ 
  Tile** tiles = calloc(MAP_HEIGHT, sizeof(Tile*));

  for (int y = 0; y < MAP_HEIGHT; y++)
  { 
    tiles[y] = calloc(MAP_WIDTH, sizeof(Tile));
    for (int x = 0; x < MAP_WIDTH; x++)
    { 
      tiles[y][x].ch = '#';
      tiles[y][x].walkable = false;
    }
  } 

  return tiles;
} 

void freeMap(void)
{ 
  for (int y = 0; y < MAP_HEIGHT; y++)
  { 
    free(map[y]);
  } 
  free(map);
} 
Enter fullscreen mode Exit fullscreen mode

Let's go over these two functions. createMapTiles() takes no arguments and returns a two dimensional array in the form of a pointer to pointers to Tiles.

  Tile** tiles = calloc(MAP_HEIGHT, sizeof(Tile*));
Enter fullscreen mode Exit fullscreen mode

Here we declare the variable called tiles, named like that to avoid confusion with the global variable map, and then allocate memory for an amount of Tile* (pointers to Tile) equal to MAP_HEIGHT. In other words, we allocate as many pointers to Tile as the length of our map's y axis.

  for (int y = 0; y < MAP_HEIGHT; y++)
  { 
    tiles[y] = calloc(MAP_WIDTH, sizeof(Tile));
Enter fullscreen mode Exit fullscreen mode

With this for loop we iterate through all of the pointers we have just allocated, and we allocate for each of them an amount of Tiles equal to MAP_WIDTH. In this way, each pointer to Tile in our y axis now points to an array of tiles of the same length as our x axis.

    for (int x = 0; x < MAP_WIDTH; x++)
    { 
      tiles[y][x].ch = '#';
      tiles[y][x].walkable = false;
    }
Enter fullscreen mode Exit fullscreen mode

And finally we loop through the newly allocated x axis and access each of our tiles in order to initialize their member variables. We are using '#' to represent our walls and setting walkable to false in order to keep the player from moving through walls. We will soon have to code some movement logic that uses this boolean.

At the end of this function we simply return the tiles pointer, which is now a map filled entirely with walls.

The freeMap() function is a simple cleanup function that iterates throught the array of pointers in order to free each row of tiles before finally freeing the map pointer itself.

Again, we need to add these functions to rogue.h:

...
void closeGame(void);
+
+//map.c functions
+Tile** createMapTiles(void);
+void freeMap(void);

// player.c functions
...
Enter fullscreen mode Exit fullscreen mode

Let's use createMapTiles() in our main.c file to initialize the map variable:

...
  player = createPlayer(start_pos);
+  map = createMapTiles();

  gameLoop();
...
Enter fullscreen mode Exit fullscreen mode

Now we need a way to draw the map and the player every turn. Let's create a new file called draw.c to keep all of our drawing logic. Add the following code to it:

#include <rogue.h>

void drawMap(void)
{ 
  for (int y = 0; y < MAP_HEIGHT; y++)
  { 
    for (int x = 0; x < MAP_WIDTH; x++)
    { 
      mvaddch(y, x, map[y][x].ch);
    } 
  } 
} 

void drawEntity(Entity* entity)
{ 
  mvaddch(entity->pos.y, entity->pos.x, entity->ch);
} 

void drawEverything(void)
{
  clear();
  drawMap();
  drawEntity(player);
} 
Enter fullscreen mode Exit fullscreen mode

These functions are fairly self-explanatory. drawMap() iterates through the map array and adds the ch symbol to the screen, drawEntity() is just a generic function to simplify drawing any entity on the screen, and drawEverything() simply runs all of the drawing steps needed to fully render the scene.

Add these functions to our rogue.h file:

...
} Entity;
+
+//draw.c functions
+void drawMap(void);
+void drawEntity(Entity* entity);
+void drawEverything(void);

//engine.c functions
...
Enter fullscreen mode Exit fullscreen mode

Now we can replace our draw step in the gameLoop() function with drawEverything(). Open engine.c and make the following changes:

void gameLoop(void)
{ 
  int ch;
+
+  drawEverything();
-  mvaddch(player->pos.y, player->pos.x, player->ch);

  while(ch = getch())
  { 
    if (ch == 'q')
    { 
      break;
    } 

    handleInput(ch);
+    drawEverything();
-    clear();
-    mvaddch(player->pos.y, player->pos.x, player->ch);
  } 
}
Enter fullscreen mode Exit fullscreen mode

Try compiling your file now. You should now see the screen is filled with # through out the dimensions specified in our MAP_HEIGHT and MAP_WIDTH. You may need to enlarge your terminal in order to see the entire rectangle.

The issue we have now is that the player is just flying over the walls. We need to add the logic to check whether we're moving into a wall or not. Open our player.c file and make the following changes to the handleInput() function:

void handleInput(int input)
{
+
+  Position newPos = { player->pos.y, player->pos.x };
+
  switch(input)
  {
    //move up
    case 'k':
-      player->pos.y--;
+      newPos.y--;
      break;
    //move down
    case 'j':
-      player->pos.y++;
+      newPos.y++;
      break;
    //move left
    case 'h':
-      player->pos.x--;
+      newPos.x--;
      break;
    //move right
    case 'l':
-      player->pos.x++;
+      newPos.x++;
      break;
    default:
      break;
  }
+  
+  movePlayer(newPos);
}
Enter fullscreen mode Exit fullscreen mode

And now add a new function called movePlayer() below the handleInput() function:

...
void handleInput(int input)
{
...
}

void movePlayer(Position newPos)
{ 
  if (map[newPos.y][newPos.x].walkable)
  {
    player->pos.y = newPos.y;
    player->pos.x = newPos.x;
  }
}
Enter fullscreen mode Exit fullscreen mode

We've changed handleInput() to create a newPos variable which will start off the same as the player's current position, and then it will be modified according to the user's input. Whether newPos is modified or not, it will then be passed to the function movePlayer(), which will take care of actually determining whether the player can actually move to the location required by the user's input. We do this by simply checking for the boolean value of map[newPos.y][newPos.x].walkable. If it's true, then we change the player's position to be the same as newPos.

Add the new function to rogue.h:

// player.c functions
Entity* createPlayer(Position start_pos);
void handleInput(int input);
+void movePlayer(Position newPos);
Enter fullscreen mode Exit fullscreen mode

And that's all we have to do to get the logic working. Try the game out now. The player should not be able to move through walls. The problem of course is that the player can't move at all because we didn't create any floor space in our dungeon.

Add the following function in our map.c file in between the createMapTiles() and the freeMap() function:

...
  return tiles;
}

Position setupMap(void)
{
  Position start_pos = { 10, 50 };

  for (int y = 5; y < 15; y++)
  {
    for (int x = 40; x < 60; x++)
    {
      map[y][x].ch = '.';
      map[y][x].walkable = true;
    }
  }

  return start_pos;
}

void freeMap(void)
{
...
Enter fullscreen mode Exit fullscreen mode

In this function we are simply hard coding a double for loop to iterate through a 10x20 sized section of our map matrix and replacing that space with floors. We represent the floors with the . character and assign true to their walkable member. We are also hard coding a starting position which we will pass to the calling function so that they can use that to place the player in an appropriate location. In the next part of this tutorial we will be generating the rooms and the player starting position randomly.

Add this function to our rogue.h:

//map.c functions
Tile** createMapTiles(void);
+Position setupMap(void);
void freeMap(void);
Enter fullscreen mode Exit fullscreen mode

Now open main.c and make the following modifications:

int main(void)
{
+
+  Position start_pos;
+
  cursesSetup();
-
-  Position start_pos = { 10, 20 };
-  player = createPlayer(start_pos);
  map = createMapTiles();
+  start_pos = setupMap();
+  player = createPlayer(start_pos);

  gameLoop();

  closeGame();

  return 0;
}
Enter fullscreen mode Exit fullscreen mode

Compile and run the game now. You should be able to move the player around a square of dots, but he'll stop at the walls. Perfect! Now we're ready to move on to the next part, where we'll look into procedurally generating our dungeon.

You can take a look at the entire source code for Part 2 here.

Discussion (0)