"closures" in C
Colony started as a Lua code base with the expectation (and goal) of converting to C to get adequate performance on a Playdate. I made it up to randomly moving one hundred ants in an empty world at 30 fps before Lua performance faltered.
I added a new time rewind mechanic, which requires copying every ant’s state on every frame. The ant state was very basic, contained within a six property table, but even copying the basic state object for every ant on every frame dropped the frame rate to <10fps. Given that the ant’s state will be growing more and more complex, it was time to drop Lua and convert to C.
With less than a thousand lines of Lua code in the prototype, converting to C was mostly straightforward. The biggest obstacle is that C function pointers are not nearly the same as “closures” (or “anonymous functions” or “lambas” depending on your favorite higher level language).
In Lua, you can write code like:
function initialize()
local game_object = ...
crankController:addListener(function (angle_diff)
game_object:process_angle(angle_diff)
end)
end
The crankController
listener is a “closure” that can both take arguments and also captures the surrounding context of game_object
.
C allows function pointers to take arbitrary arguments, but functions must be defined at the file level and cannot capture all types of contexts. Some equivalent C code might look like:
typedef struct game_object_struct game_object;
typedef void (*crank_listen_fn)(game_object* go, int angle_diff);
void crank_controller_add_listener(game_object* context, crank_listen_fn listener);
void process_angle(game_object* go, int angle_diff) {
// Do something with the angle
}
void initialize(void) {
game_object* go = ...
crank_controller_add_listener(go, process_angle)
}
This code is perfectly fine. However, one can also imagine that other functions will have very similar interfaces. The Colony prototype already has listeners for control state changes, time passing, viewport movement, etc. These listeners are all used essentially the same way (get notified of game events), but all operate on additional types.
Rather than duplicating the listening implementation, we can define a closure
struct that allows us to group a function that takes arbitrary arguments with a context in a generic way:
typedef struct closure_struct closure;
typedef void* (*closure_fn)(void* context, va_list args);
closure* closure_create(void* context, closure_fn fn);
void* closure_call(closure* c, ...);
The closure
type can be used to create a generic “event emitter” that allows listeners to be notified of arbitrary events in the game:
typedef struct emitter_struct emitter;
emitter* emitter_create(void);
listener_id_t emitter_add_listener(emitter*, closure*);
void emitter_remove_listener(emitter*, listener_id_t);
void emitter_fire_event(emitter*, ...);
The emitter
code deals only with the generic closure
, and does not care about the specifics of the closure’s context or the arguments that the closure expects. The emitter can notify any context of any event (passed as arguments to emitter_fire_event
).
For example, this code uses closure
and emitter
to set an ant and some grass as listeners to crank and button events:
emitter* crank_emitter;
emitter* button_emitter;
// Setup ant to listen to both crank and buttons
ant* a;
void* ant_advance_time(void* context, va_list args) {
ant* a = (ant*)context;
int time_diff = va_arg(args, int);
printf("Ant %s advanced %d in time", a->name, time_diff);
}
emitter_add_listener(
crank_emitter,
closure_create(a /* context */, ant_advance_time)
);
void* ant_handle_buttons(void* context, va_list args) {
ant* a = (ant*)context;
PDButtons btns = va_arg(args, PDButtons);
printf("Ant %s handles buttons: %d", a->name, btns);
}
emitter_add_listener(
button_emitter,
closure_create(a /* context */, ant_handle_buttons)
);
// Setup grass to listen to only crank not buttons
grass* g;
void* grass_advance_time(void* context, va_list args) {
grass* g = (grass*)context;
int time_diff = va_arg(args, int);
printf("Grass %s advanced %d in time", g->species, time_diff);
}
emitter_add_listener(
crank_emitter,
closure_create(g /* context */, grass_advance_time)
);
// Fire crank event, which would log to console for both ant and grass
emitter_fire(crank_emitter, 10 /* time diff */);
// Fire button event, which would log to console only for ant not grass
emitter_fire(button_emitter, kAButton | kBButton);
Colony uses the generic emitter
concept as the base for crank, time, button, etc events to great success. The closure
type has also been useful beyond the emitter
and has allowed us to write C code that feels like a higher level language.
I’m hard at work on the core engine of Colony, and will be sharing more soon!
Get drifter
drifter
A wandering outsider on a mission to save a corrupt frontier town.
Status | Prototype |
Author | larrry |
Genre | Simulation |
Tags | drifter, Playdate, Top down shooter, Wild West |
More posts
- Game pivotJul 24, 2024
Leave a comment
Log in with itch.io to leave a comment.