This is a part 2 for this post about how entities work in Wizwag. Today, I'll show you how "systems" are set up.
When I started the C11 rewrite of the game, I tried to do as much as I could in top level functions. A frame update was one large function, with most work being done inline in named blocks with little comment tags.
This style is somewhat popular in the Handmade space, and for good reason. At the scale of Wizwag, it became difficult for me to maintain, despite the benefits.
In theory, if you want code that runs in one place in one context, writing it inline is the simplest way to ensure that. At scale, it became difficult for me to keep up with, navigate, hold in my head, and reason about.
Reordering a couple systems experimentally required copying a large codeblock somewhere else. I think with a code editor designed around this idea, with a "syntax tree view" where you could drag nodes around to restructure things, it would have been tractable for me. Lacking that kind of tool though, I got bogged down.
I was doing some contract work on a cool indie game called Pixel Washer and enjoying myself.
Pixel Washer is a web game with a custom engine, created by Matt Hackett, who is a delight to speak to and work with. Matt designed a simple Entity Component System for Pixel Washer that has some nice properties, and the convenience of it inspired envy in me.
So I wondered: how much of that convenience could I get in C11? Could I have convenient little systems that were easy to register, that were relatively self contained, and that really separated concerns, without it becoming boilerplate Hell?
Somewhat, is the answer!
So, how could I get to a nicer place? The answer for me was X macros. Wait, don't leave, it's not that bad I promise. My analogue to Pixel Washer's tidy systems array is five files as follows:
This is an X macro file that declares each system, and controls their execution order. It has entries like this:
//-----------------------------------------------------------------------------
// MARK: System Bookkeeping
X(switch_watchers)
X(entity_placer)
X(room_ambient)
X(music)
//-----------------------------------------------------------------------------
// MARK: Pre-Player Systems
X(dialogue)
X(statuses)
X(mobiles)
X(camera)
X(npc)
X(chests)
X(liftable_detection)
// ...
At the time of writing, there are 87 systems in Wizwag. This number will probably balloon to something like ~200 by the time the game ships, if not more.
This is an X macro file that declares each system hook and event. These are the current entries:
// A list of all system calls
//-----------------------------------------------------------------------------
// MARK: Per-Frame Calls
X(tick)
X(tick_after_player)
X(draw)
X(late_tick)
X(debug_draw)
X(debug_ui)
//-----------------------------------------------------------------------------
// MARK: Event Calls
X(on_room_entered)
X(on_area_entered)
X(on_transition_started)
This file is responsible for dragging each system implementation into my unity
build. This file has an #include statement for each system, so lots
of this:
#include "systems/game_over.h"
#include "systems/terrain_detector.h"
#include "systems/warps.h"
#include "systems/woodpiles.h"
// ...
#include "systems/combat/player_arrow_hits.h"
#include "systems/combat/player_disc_hits.h"
#include "systems/combat/player_explosion_hits.h"
// ...
#if WIZ_ANGRY_CHICKENS_ENABLED
#include "systems/combat/chicken_aggro.h"
#endif
// ...
// MARK: ADD NEW SYSTEMS HERE
This file defines what a system is, and defines the shared context that systems are given during each frame. A system is a simple struct holding some function pointers:
//-----------------------------------------------------------------------------
// A struct to define a system.
typedef struct {
const char *name;
void (*init) (wiz_runtime_t *rt);
#define X(name) \
void (*name) (wiz_runtime_t *rt, wiz_frame_ctx_t *ctx);
#include "systems_calls.def"
#undef X
} wiz_system_t;
We use our list of system calls here with an X macro to populate the struct. Part of the reason is that doing bookkeeping across several files just to add something is really annoying to me and I hate, hate, hate, hate, hate doing it.
If I had my druthers I would be able to tell the compiler: Automatically include any file in this directory. Automatically include anything with this property as a member in this struct. Automatically add things of this shape to this list. I digress.
The "runtime" is a large struct from the host program that holds every single piece of data that lives across frames, like entities and save file values. It also holds loaded asset handles, the pathfinding graph, the current room's tile map and other information, etc.
Each frame, we create and initialize a frame context on the stack. It holds stuff like the framebuffer, off screen buffers, and sprite batches, but also gameplay context like "is a transition playing?" and "is the player's weapon hitbox active? what is it?"
This file uses X macros again to collect all registered systems and create functions to run each system call or event across all systems that have it. This is straightforward:
#pragma once
#include "systems.h"
#include "systems_register.h"
//-----------------------------------------------------------------------------
#define X(name) extern wiz_system_t system_##name;
#include "systems.def"
#undef X
//-----------------------------------------------------------------------------
static wiz_system_t *all_systems[] = {
#define X(name) &system_##name,
#include "systems.def"
#undef X
};
//-----------------------------------------------------------------------------
void systems_call_init(wiz_runtime_t *rt) {
for (int i = 0; i < ARRAY_COUNT(all_systems); ++i) {
if (all_systems[i]->init) {
PERF_OPEN(all_systems[i]->name);
all_systems[i]->init(rt);
PERF_CLOSE(all_systems[i]->name);
}
}
}
//-----------------------------------------------------------------------------
// System calls and events
#define X(FN) \
void systems_call_##FN (wiz_runtime_t *rt, wiz_frame_ctx_t *ctx) { \
for (int i = 0; i < ARRAY_COUNT(all_systems); ++i) { \
if (all_systems[i]->FN) { \
PERF_OPEN(all_systems[i]->name); \
all_systems[i]->FN(rt, ctx); \
PERF_CLOSE(all_systems[i]->name); \
} \
} \
}
#include "systems_calls.def"
#undef X
So, all in all, the systems macro hack "grunt work" is under 50 lines of code. To call these system functions is trivial:
// ... buncha stuff ...
systems_call_tick(rt, &ctx);
systems_call_draw(rt, &ctx);
systems_call_tick_after_player(rt, &ctx);
systems_call_late_tick(rt, &ctx);
// ... buncha stuff ...
This setup gets me an array of all systems, in execution order, that I can iterate over to call a a function on all systems that have that function set. It also means that if a system has been added to the list but not actually created yet, it's a compile error.
Is it as idiomatic or tidy as a JS or Lua or even C++ equivalent? No! Does it give me a little bit of an easier time wiring new systems into the game?
Absolutely.
In this scheme, each system is an .h file. The way it works is this:
Add an entry for the system in systems.def
// MARK: End of frame Systems
X(hud)
At this point, you would get a compile error because no system_hud struct
existed for systems.c to refer to.
Create a header file for it, like src/ecs/systems/hud.h
Include that file in src/ecs/systems_register.h:
#include "systems/hud.h"
Now, implement your system in the .h file:
#pragma once
#include "../../util/audio.h"
#include "../../data/data_areas.h"
#include "../../data/data_rooms.h"
#include "../systems.h"
static void
_hud_tick (wiz_runtime_t *rt, wiz_frame_ctx_t *ctx) {
// ...
}
static void
_hud_draw (wiz_runtime_t *rt, wiz_frame_ctx_t *ctx) {
// ...
}
wiz_system_t system_hud = {
.name = "hud",
.tick = _hud_tick,
.draw = _hud_draw,
};
Designated initializers are nice. This is way less nice than having a module with exported functions, but it feels similar enough to stop me retching when I need to add something to my game.
The most important aspect of it is: I don't have to call this system manually anywhere. I don't have to maintain a list of callsites. I can easily comment out an entry in the systems list to turn it off. There are stages of the frame in a flat pipeline, and I can assign code freely among those stages, with minimal bookkeeping to do.
This setup still isn't ideal. There's still some boilerplate and carbuncles.
Because I do a unity build in C for ease and speed, every function which is semantically "local" to a system is nevertheless polluting the global namespace. This whole setup is a hack to work around limitations of how you're allowed to structure and talk about code in C.
I get some nice things by using C: Code runs fast by default in most cases, the compiler is reliable and maintained, I'm not poking at C libraries from behind a veil of what some random language binder thought was a "better" API, and I get access to debuggers and an LSP that actually works.
I don't think about memory management at all, ever, because I'm making a retro game in 2026. If you are also making a retro game in 2026, and struggling with memory management, this is a red flag about something in the fundamental structure of your codebase, and the good news is that it can be fixed (email me).
Still, my brain hates cruft and it can be very distracting for me. "It's just like that" is very difficult to accept, and despite enjoying C as the "lesser of many evils" it's hard to use it on a mid sized project and not feel like I'm walking barefoot over a dirty floor at all times.
Maybe that's user error! One day I will give in and make a programming language, but not yet...