Today I spent some time on the reorganization of the Dungeon game codebase into a modular structure.
I thought I would document it here (partly/mostly for future Chris), along with key C programming concepts I used in the process.
NB. I am NOT saying I am an expert in this or that this is the correct solution, just this is how I am doing it right now. It might be overkill or under-organized - I don't know!
Project Structure
My repo is now reorganized into the following directory structure:
src/
├── display/ # Display and rendering functions
│ ├── display.c
│ └── display.h
├── include/ # Common includes and global definitions
│ ├── globals.c
│ ├── globals.h
│ ├── maze.c
│ ├── maze.h
│ ├── notconio.c
│ └── notconio.h
├── input/ # Input handling functions
│ ├── input.c
│ └── input.h
├── logic/ # Game logic and mechanics
│ ├── game_logic.c
│ └── game_logic.h
├── screens/ # Title and game over screens
│ ├── screens.c
│ └── screens.h
├── main.c # Main program entry point
└── Makefile # Build system configuration
This structure separates the code into logical components, making it easier to understand, maintain, and extend. Hopefully will make it easier to have, for example, gamepad/joystick control and graphics in the future too.
Header (.h) vs Implementation (.c) Files
In C, code is typically split between header files (.h
) and implementation files (.c
):
Up until now, I had main.c
and a bunch of .h files, but that isn't scalable when you have lots of variables and functions being shared around.
For example, when displaying the game over message I was using a function that places text at a certain x,y coordinate, but the compiler couldn't find it without the include statement.
Header (Library) Files (.h)
Header files contain:
- Function declarations (prototypes)
- Type definitions (structs, enums, typedefs)
- Constant definitions
- Macro definitions
- External variable declarations
They serve as an interface for the functionality a library provides.
For example, in display.h
:
// Function declarations
void output_message(void);
void draw_screen(void);
void draw_momentary_object(unsigned int obj_old_x, unsigned int obj_old_y,
unsigned int obj_x, unsigned int obj_y,
unsigned int obj_tile, unsigned int delay);
Code (Implementation) Files (.c)
Implementation files contain:
- The actual code that implements the functions declared in the header
- Definition of static (file-scope) variables
- Definition of global variables
For example, in display.c
:
void draw_screen(void) {
// Draw the whole screen
int row, col;
if (draw_whole_screen && screen_drawn == false) {
for (row = 0; row < PLAYABLE_HEIGHT; row++) {
for (col = 0; col < MAZE_WIDTH; col++) {
cputcxy(col, row, get_map(col, row));
}
}
screen_drawn = true;
} else {
// Update the screen around the player with a set radius
update_fov(player_x, player_y, 2);
}
}
Why Bother? (Benefits of Separation)
Spaghetti code has its laziness benefits, but after a while, it breaks down. Modularity is better in the longer term:
- Encapsulation: Implementation details don't have to be out in the open.
- Compilation efficiency: Changes to implementation don't require recompiling. all files that use a module.
- Clarity: Clear separation between interface and implementation.
- Reduced conflicts: Minimizes naming conflicts and unintended dependencies.
- Reuse: There is a long history of programmers aiming to only do something once. Sometimes it works out.
Global Variables and the extern
Keyword
One of the gotchas I was getting trapped by was the need for global variables due to my older retro target systems. Global variables are variables that can be accessed from any part of the program.
In a modular codebase, they need to be:
- Defined in exactly one
.c
file - Declared with the
extern
keyword in a header file
Example
In globals.h
, we declare global variables:
// Game state variables
extern bool run;
extern bool in_play;
extern bool obstruction;
extern bool screen_drawn;
extern bool draw_whole_screen;
// Player variables
extern unsigned char player_x;
extern unsigned char player_y;
// ... more variables ...
In globals.c
, we define these variables:
// Game state variables
bool run = true;
bool in_play = false;
bool obstruction = false;
bool screen_drawn = false;
bool draw_whole_screen = false;
// Player variables
unsigned char player_x = 19;
unsigned char player_y = 8;
// ... more variables ...
The extern
Keyword
The extern
keyword tells the compiler "this variable is defined somewhere else." It's a promise that the linker will find the actual definition in another compilation unit.
Benefits:
- Prevents multiple definitions of the same variable
- Allows sharing variables across multiple files
- Maintains a single source for variable initialization
Include Guards
Include guards prevent a header file from being included multiple times in the same compilation unit, which would cause redefinition errors.
Example from globals.h
:
#ifndef GLOBALS_H
#define GLOBALS_H
// Header content goes here...
#endif /* GLOBALS_H */
How it works:
- First time the file is included:
GLOBALS_H
is not defined, so the preprocessor includes the content GLOBALS_H
is defined- Subsequent inclusions:
GLOBALS_H
is already defined, so the content is skipped
Make Files (The Build System)
A Makefile is a script used by the make
utility to build the program. Our Makefile contains:
CC = gcc # Compiler to use
CFLAGS = -Wall -Wextra -g # Compiler flags
LDFLAGS = -lncurses # Linker flags
# Source files
SRC = main.c \
include/globals.c \
include/maze.c \
include/notconio.c \
display/display.c \
input/input.c \
logic/game_logic.c \
screens/screens.c
# Object files
OBJ = $(SRC:.c=.o) # Replace .c with .o for all source files
# Executable name
TARGET = dungeon_modular
all: $(TARGET) # Default target
$(TARGET): $(OBJ) # Link object files to create executable
$(CC) $(CFLAGS) -o $@ $^ $(LDFLAGS)
%.o: %.c # Compile source files into object files
$(CC) $(CFLAGS) -c $< -o $@
clean: # Remove generated files
rm -f $(OBJ) $(TARGET)
.PHONY: all clean # Declare targets that don't create files
Compilation Process
The compilation process has multiple stages:
- Preprocessing: Expands macros, includes header files
- Compilation: Converts C code to assembly
- Assembly: Converts assembly to machine code (object files)
- Linking: Combines object files and libraries into an executable
In our Makefile:
%.o: %.c
rule handles steps 1-3 for each source file$(TARGET): $(OBJ)
rule handles step 4
Special Variables in Makefiles
$@
: Target of the rule (left side of the colon)$^
: All prerequisites (right side of the colon)$<
: First prerequisite
Building the Project
To build the project:
make
To clean generated files:
make clean
New Code Organization
The codebase is organized into logical components:
Global Variables and Structs (
include/globals.h
,include/globals.c
)- Contains all global variables used across the game
- Defines common structures like the enemy struct
Display/Output (
display/display.h
,display/display.c
)- Functions for rendering the game world
- Screen update and message display
Input/Controls (
input/input.h
,input/input.c
)- Keyboard input handling
- Timing functions
Game Logic (
logic/game_logic.h
,logic/game_logic.c
)- Core game mechanics
- Enemy AI and combat
- Map manipulation
Screens (
screens/screens.h
,screens/screens.c
)- Title screen
- Game over screen
Maze Generation (
include/maze.h
,include/maze.c
)- Procedural maze generation
- Object placement
Console I/O (
include/notconio.h
,include/notconio.c
)- Platform-independent console functions
- Abstraction layer for terminal operations
Dependencies Between Components
Components have dependencies on each other, represented by #include
statements:
- Main depends on all components
- Display depends on Game Logic and Input
- Game Logic depends on Display, Input, and Maze
- Input depends on Game Logic and Display
- Screens depends on Display and Input
This dependency graph helps understand how the components interact and ensures proper initialization order.
Next Steps
The reorganized repo is now much more modular, maintainable, and easier to understand.
Each component has a clear responsibility, and the dependencies between components are explicit.
This structure makes it easier to extend the game with new features or fix bugs without affecting unrelated parts of the code. I also have a make system so I can start splitting out dependencies amongst the target platforms.
Next up I want to build out the full game to a state where I can get some test players to give me some early feedback!
Thanks for your contribution to the STEMsocial community. Feel free to join us on discord to get to know the rest of us!
Please consider delegating to the @stemsocial account (85% of the curation rewards are returned).
You may also include @stemsocial as a beneficiary of the rewards of this post to get a stronger support.
the game im working on, Gnar World is programed in C#. i was on art duty but the guy who was on code duty kinda... bailed? im not sure. i think hes over it. so i think i have to learn the code now in order to finish the game.
also, i love your game dev posts. would you maybe consider posting them into the Game Dev community? would there be a reason not to? I'm trying to incubate a Game Dev community on hive with OCD curation. but we need some more activity in there. your dev log work is awesome and interesting.
I would also be stoked to playtest anything you are working on
Will do and thanks:)
When you say c# are you using unity or godot or something?
godot yeah. im thinking i should explore getting some kind of AI coding assistant that can see all the scripts and help me make sense of everything that is going on. one of my friends uses a vs code replacement program called Cursor that he says is next level amazing but i havent tried it yet
You should definitely try it. I tried Cursor out and it is impressive but it gets stuck on big projects or larger code listings. I'm sure it will keep improving.
Godot looks good but I was put off by the "visual" side, even though it is supposed to be easier working that way it confuses the heck out of me