Patterns for building console games in C

I dive into the C programming language to develop an object-oriented game in the terminal.

7 min read

Eceman is an arcade console game developed in Algorithm and C Programming course at ECE (École Centrale d’Électronique) Paris. It was made for Windows and was inspired by Club Penguin Thin Ice.

Check the project on GitHub.

Purposes

The main goal of this project was to go back to basics in C:

  • Loops
  • Arrays
  • Structures
  • Pointers
  • Files

Those were topics I was already familiar with, so I decided to dive into advanced C patterns: building a graphical interface for the terminal and managing entity behaviors.

Eceman home screen

Eceman home screen

Definition

Rules

The player must control the main character — Eceman — through a maze covered in ice. Ice squares melt as soon as Eceman walks on them. The more ice cubes he passes on, the better the score gets. His goal is to get through the door that leads to the next level.

However, the difficulty is increasing progressively: holes make him fall at the previous level, enemies are walking in some, etc.

Game board

The board is a two-dimensional matrix (15 * 19) of characters generated by a map file.

wwwwwwwwwwwwwwwwwww
wwwwwwwwwwwwwwwwwww
wwwwwwwwwwwwwwwwwww
wwwwwwwZiiiSwwwwwww
wwwwwwwiiiiiwwwwwww
wwwwwwwiiNiiwwwwwww
wwwwwwwiiiiiwWWWwww
wwwwwwWiiWiiwWDWwww
###WWWWooWoWWWoWWW#
#WWWooooiZoooWoooW#
#W$ooooiiZoooWoooW#
#WWWWWWWWZoooooooW#
#########WWWWWWWWW#
###################
###################
Map file for a level

Eceman example level

Level associated to this map

Differents squares

The level’s grid uses one character per square. Here are the different types of square:

  • Thin ice. Breaks after Eceman walked.
  • Thick ice. Breaks after Eceman walked twice.
  • Slippy ice. Drags Eceman up to an obstacle.
  • Water. State after the thin ice.
  • Lightness potion. Makes ices unbreakable until next level.
  • Score bonus. You obtain 10 points.
  • Tunnel. Teleports Eceman to the tunnel exit.
  • Hole. Teleports Eceman to the previous level at the same square.
  • Spawn. Spawn point.
  • Exit door. Leads to the next level (end of the game if not).
  • Wall. No way to go through.
  • Outside. Inaccessible squares.

Entities

In addition to these squares exist some interactive entities:

  • Mower. Once touched, mows the path and breaks the ice.
  • Enemy. Living character that moves. Once touched, you restart the level and lose some points.
  • Eceman. Yourself.

Coding the game

Orienting the code as object

When I first heard that I had to develop a game in C, I was wondering how I could write it as clean as possible. Obviously, that means I had to imitate the concepts of Object-Oriented Programming.

The tricks I learned reading the Linux code on GitHub were to:

  • Use static as often as I could when my function is private.
  • Use const as often as I could when I’m not supposed to change its value.

Structures are in a way the ancestors of classes. I then tried to use them a lot in order to implement polymorphism.

Managing the entities

A mower and an enemy are very similar in a way. They only have a different behaviour when they hit an object and they reach their final state on different events.

  • A mower

    • moves only once Eceman pushes it;
    • stops once it hits a wall or whatever obstacle.
  • An enemy

    • moves from the start;
    • stops once he hits Eceman.

I focused a lot on avoiding code redundancy. After a lot of Googling about using Design Patterns in C, I stopped on an article1 by Adam Petersen.

It turned out I had to use function pointers to simulate the Strategy Pattern. I had never used this functionality before, so I got to give it a try!

I first needed to define some function pointers about each strategy.

/**
 * Gets the collide condition of the entity with a given object.
 * @param symbol The object to try the collision with
 * @param pos The position of the hero
 * @return 1 if collision, 0 otherwise
 */
typedef int (*CollidePropertyStrategy) (const char symbol, const Position* pos);

/**
 * Execute the action of the entity when it collides an object.
 * @param entity The entity we want to change the behaviour of
 * @param direction The entity's new direction
 */
typedef void (*CollideStrategy) (Entity* entity, const enum Direction direction);

/**
 * Runs the entity action once moved.
 * @param game The current game state
 * @param hero The hero
 * @param entity The given entity with new positions
 * @param board The game board with objects
 */
typedef void (*FinalActionStrategy) (GameState* game, Eceman* hero, Entity* entity, char board[ROWS][COLS]);

The Entity should then inherit these functions.

struct Entity {
    Position* pos;
    enum EntityState state;
    enum Direction direction;
    CollidePropertyStrategy collidePropertyStrategy;
    CollideStrategy collideStrategy;
    FinalActionStrategy finalActionStrategy;
};

We’re going to specify one of these strategy functions: finalActionStrategy(). It describes the behaviour of the entity at the end of a round:

  • A mower gives the player a point and repaint the board.
  • An enemy checks if he has collided the hero and launches an appropriate function.
void mowerFinalActionStrategy(GameState* game, Eceman* hero, Entity* mower, char board[ROWS][COLS]) {
    game->levelScore++;

    drawEntity(board, mower);
}

void enemyFinalActionStrategy(GameState* game, Eceman* hero, Entity* enemy, char board[ROWS][COLS]) {
    if (enemy->caseBelow == HERO_CHAR) {
        gotAttacked(game, board, hero);
    }
}

Now, to create a specific entity, we won’t directly call the private createEntity() function but the public createMower() or createEnemy() functions.

static Entity* createEntity(const unsigned int x, const unsigned int y, const enum Direction direction, CollidePropertyStrategy collidePropertyStrategy, CollideStrategy collideStrategy, FinalActionStrategy finalActionStrategy) {
    Position* pos = NULL;
    Entity* entity = NULL;

    pos = malloc(sizeof(Position));
    assert(pos != NULL);

    entity = malloc(sizeof(Entity));
    assert(entity != NULL);

    pos->x = x;
    pos->y = y;

    entity->pos = pos;
    entity->state = ACTIVE;
    entity->direction = direction;

    entity->collidePropertyStrategy = collidePropertyStrategy;
    entity->collideStrategy = collideStrategy;
    entity->finalActionStrategy = finalActionStrategy;

    return entity;
}

Entity* createEnemy(const unsigned int x, const unsigned int y, const enum Direction direction) {
    return createEntity(x, y, direction, enemyCollidePropertyStrategy, enemyCollideStrategy, enemyFinalActionStrategy);
}

Entity* createMower(const unsigned int x, const unsigned int y) {
    return createEntity(x, y, UP, mowerCollidePropertyStrategy, mowerCollideStrategy, mowerFinalActionStrategy);
}

This way, we don’t have to worry about the strategies that our entity will trigger, it’s already handled. Instead of having a bunch of switches, I just call my brand new functions that do the job for me:

if (entity->collidePropertyStrategy(board[entity->pos->x - 1][entity->pos->y], entity->pos)) {
    entity->collideStrategy(entity, DOWN);
    return;
}

As you can see, I don’t even need to know what kind of entity it is anymore. We can consider that as polymorphism.

Designing the terminal

I’m the kind of player that gets quickly bored when the game doesn’t look attractive. It was actually very challenging to make it sexy, as it is a terminal game.

However, it is possible to add some foreground and background colors. As this game is built for Windows only, I wrote a little function to change the color of the output:

void setColor(const int color) {
    HANDLE  hterminal;
    hterminal = GetStdHandle(STD_OUTPUT_HANDLE);
    SetterminalTextAttribute(hterminal, color);
}

I combined this with another function getCaseColor() that returns the color of the object. It is now easier to print colors: setColor(getCaseColor(ENEMY_CHAR)).

Editing the levels

The whole point of the game is to create levels with new entities and objects. I, therefore, made possible to create your own level right in the game.

You can see live changes when you type the character corresponding to the object. A kind of WYSIWYG (What You See Is What You Get)!

Eceman example level

Editing the level 6

Conclusion

This game was very object-oriented in the way it was programmed. Coding is C was definitely not a good fit. It was part of the contract of the course, though.

I feel like I sometimes bent what C had to offer to get more out of it. The way I extended the properties of the entities with function pointers is quite odd in my opinion. I still think it’s an elegant and appropriate way to handle it.