Patterns for building console games in C
Bending C toward the object-oriented language it never became.
· 7 min read
Eceman is an arcade console game I built during the Algorithm and C Programming course. It was made for Windows and inspired by Club Penguin Thin Ice.
Check out the project on GitHub.
Purpose
The main goal of this project was to go back to basics in C:
- Loops
- Arrays
- Structures
- Pointers
- Files
These were topics I was already familiar with, so I decided to go further into C patterns: building a graphical interface for the terminal and managing entity behaviors.

Definition
Rules
The player controls the main character, Eceman, through a maze covered in ice. Ice squares melt as soon as Eceman walks on them. The more ice squares he crosses, the higher the score gets. His goal is to reach the door that leads to the next level.
However, the difficulty increases progressively: holes make him fall back to the previous level, enemies roam the maze, 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#
###################
###################
Different squares
The level grid uses one character per square. Here are the different types of squares:
- Thin ice. Breaks after Eceman walks on it.
- Thick ice. Breaks after Eceman walks on it twice.
- Slippy ice. Drags Eceman up to an obstacle.
- Water. State after thin ice breaks.
- Lightness potion. Makes ice unbreakable until the next level.
- Score bonus. Gives you 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, or ends the game if there is no next level.
- Wall. Cannot be crossed.
- Outside. Inaccessible squares.
Entities
In addition to these squares, there are some interactive entities:
- Mower. Once touched, mows the path and breaks the ice.
- Enemy. A moving character. Once touched, you restart the level and lose some points.
- Eceman. Yourself.
Coding the game
Structuring the code
When I first heard I had to develop a game in C, I wondered how I could write it in a maintainable way. To me, that meant replicating Object-Oriented Programming concepts.
The tricks I learned from reading Linux code on GitHub were to:
- Use
staticwhenever a function is private. - Use
constwhenever a value shouldn't change.
Structures are, in a way, the ancestors of classes. I used them heavily to implement polymorphism.
Managing the entities
A mower and an enemy are very similar in some ways. Their behaviors differ only when they hit an object, and they reach their final state on different events.
- A mower
- moves only when Eceman pushes it;
- stops once it hits a wall or any other obstacle.
- An enemy
- moves from the start;
- stops once it hits Eceman.
I focused a lot on avoiding code redundancy. After a lot of Googling about using Design Patterns in C, I landed on an article[^1] by Adam Petersen.
It turned out I had to use function pointers to simulate the Strategy Pattern. I had never used them before, so I got to try something new!
I first needed to define some function pointers for 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 behavior 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 struct would then include these function pointers.
struct Entity {
Position* pos;
enum EntityState state;
enum Direction direction;
CollidePropertyStrategy collidePropertyStrategy;
CollideStrategy collideStrategy;
FinalActionStrategy finalActionStrategy;
};Let's look at one of these strategy functions: finalActionStrategy(). It describes the behavior of the entity at the end of a round:
- A mower gives the player a point and repaints the board.
- An enemy checks whether it has collided with 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 which strategies our entity will trigger; that's already handled. Instead of having a bunch of switch statements, I call the 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 this polymorphism.
Designing the terminal
I'm the kind of player who quickly gets bored when a game doesn't look attractive. It was challenging to make it look good, since it is a terminal game.
However, it is possible to add foreground and background colors. Since 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 it possible to create your own levels 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)!

Conclusion
This game was programmed in an object-oriented way. C was not a great fit, but it was part of the course contract.
I feel like I sometimes bent what C had to offer to get more out of it. Extending entity properties with function pointers is quite odd in my opinion. Yet, I believe it's an elegant and appropriate way to handle this problem.
References
- adampetersen.se