(This is a modified version of the pattern shown in this article by Phantz but botched modified into working for my own usecase which I thought I'd document for both others and myself)
Motivation
I really like making games in C. I'm not sure why, the language just meshes with my brain in a way that I can't get rid of.
Using C as a language for games is nice, it basically forces simple, straight to the point solutions. None of the endless abstractions and hierarchies that I've caught myself writing in Rust, C# or C++ (this may or may not be a me problem).
General purpose game engines are another problem though. You can make a game with reusable parts, but that's generally not what I'd call a “game engine”. The definition of engine I quite like is that for a framework to be a Game Engine it needs to handle at the very least:
- Asset Management,
- Actor/Entity/GameObject lifecycles,
- Input polling/passing
- Media (audio, rendering, etc.),
- And the application loop.
While not prescribing the player's actions or the world's reactions.
Now I use SDL (_render, _mixer, etc.) for my games, so media and input is pretty easy to handle. Asset management takes a bit of writing, but isn't too hard to do in any language. The application loop is similarly easy as well.
But object management kept throwing me for a loop in C specifically. I was never taught the C language, I just have this book that taunts me in my sleep. So I had no clue as to how to do polymorphism in C in a “clean” way. I just wanted some way to pass the engine some object that refers to a game object's data “anonymously” while still exposing the functions the engine needed (Start, Tick, Destroy, etc). So enter stage left:
The Typeclass pattern
In Haskell this is a language feature, Rust has something similar in the form of traits, in C++ we have abstract classes and C# this would be an interface. C obviously doesn't have any of that as part of the language so we have to write it ourselves. At it's most basic, pattern requires two objects and a macro. The two objects are an interface struct containing function pointers, and the typeclass object which contains the interface and a void pointer to what I'll call the data.
Defining this looks something like:
typedef struct { void (*const drop)(void*); } IDrop; typedef struct { void *data; IDrop const *tc; } Drop;
And then we need a function which produces a typeclass Drop for an object of some type, to do this we define a macro.
#define impl_Drop_for(T, drop_f)\
Drop T##_as_Drop(T* x) {\
TC_FN_TYPECHECK(void, drop_f, T*);\
static IDrop const tc = {\
.drop = (void(*const)(void*)) drop_f,\
};\
return (Drop){.tc = &tc, .data = x};\
}
(TC_FN_TYPECLASS is a helper macro defined as #define TC_FN_TYPECHECK(__Return, __Name, ...) __Return (*const __Name##_)(__VA_ARGS__) = __Name; (void)__Name##_ I use to trigger compile time type checking on the functions)
impl_Drop_for can then be used in an implementation file like
Player.c:
impl_Drop_for(Player,
PlayerDestroy
)
Which can then be used to create an object of type drop like:
Player *player = MakePlayer(at_location); // allocate player Drop player_as_drop = Player_as_Drop(player); // wrap in a Drop typeclass player_as_drop.tc->drop(player_as_drop.data); // destroy player using drop typeclass
This is slightly different to Phantz' previously mentioned version. As it forces you into using the <Struct>_as_<Typeclass> naming scheme. This means that I can somewhat streamline Phantz' version of combining typeclasses by letting you require other typeclasses implicitly.
For example my “game engine” (it's not that far along yet) has a BehaviourEntity typeclass. Which serves as the primary way for the engine to interact with game-specific objects. Because I want the engine to take ownership of any entities in the game, it also requires the Drop typeclass.
Now, instead of passing Player_as_Drop to impl_BehaviourEntity_for as an argument, the macro can assume that the naming scheme was followed.
#define impl_BehaviourEntity_for(T, start_f, update_f, draw_f, get_depth_f)\
BehaviourEntity T##_as_BehaviourEntity(T* x) {\
TC_FN_TYPECHECK(void, start_f, T*);\
TC_FN_TYPECHECK(void, update_f, T*, float);\
TC_FN_TYPECHECK(void, draw_f, T*);\
TC_FN_TYPECHECK(long, get_depth_f, T*);\
static IEntityBehaviour const tc = {\
.update = (void(*const)(void*, float)) update_f,\
.start = (void(*const)(void*)) start_f,\
.draw = (void(*const)(void*)) draw_f,\
.get_depth=(long(*const)(void*)) get_depth_f,\
};\
TC_FN_TYPECHECK(Drop, T##_as_Drop, T*);\
TC_FN_TYPECHECK(Mirror, T##_as_Mirror, T*);\
IDrop const* drop = T##_as_Drop(x).tc;\
IMirror const* mirror = T##_as_Mirror(x).tc;\
return (BehaviourEntity){.data = x, .tc = &tc, .drop = drop, .mirror = mirror};\
}
(Don't mind the Mirror bits, or if you do check the Bonus section :3)
This version will still complain when you don't have the typeclass function declared, but will grab it if it's available, while still checking if the Player_as_Drop function signature is valid. So now we are able to pass ownership over our behaviour entities to the engine knowing that they'll be destroyed when we ask it to load a different level.
Now we can put
impl_BehaviourEntity_for(Player,
PlayerStart,
PlayerUpdate,
PlayerDraw,
PlayerGetDepth
)
Below the Drop implementation in Player.c. Then, using a much simpler macro, add
Player.h:
decl_typeclass_impl(Drop, Player);
decl_typeclass_impl(BehaviourEntity, Player);
/* decl_typeclass_impl is defined as
* #define decl_typeclass_impl(__Typeclass, __Type) extern __Typeclass __Type##_as_##__Typeclass(__Type*)
* The semicolon is being eaten intentionally here.
*/
to Player.h and you have everything my engine would require to be a “valid scene entity”.
Wrapping up
I personally quite like this pattern. Mainly because (at least in the way I use it) it's easy to just use it only where it's needed. In the engine I'm writing it's entirely possible to use typeclasses only as a language to speak to the engine while writing your own code using some other form of polymorphism. Or none if that's your style.
This is a pattern mainly for places where there's a need for basic polymorphism with no inheritance or OOP features.
Which is one of the reasons I'm fond of it.
Bonus: The Mirror typeclass
Here's a typeclass that enables casting between typeclasses.
Skip the code if you just want the explanation. Or at least something that looks like an explanation. mirror.h: #include "typeclass_helpers.h" #include "stdint.h" #include "string.h" #include "dictionary.h" #include "strutil.h" // included because the impl macros require strhash typedef uintptr_t typeid; typedef struct { const char *(*const get_typestring)(void* self); typeid (*const get_typeid)(void* self); Dictionary *(*const get_typeclasses)(void* self); } IMirror; typedef struct { void* data; union { IMirror const *tc; // this is cursed, but it allows the TC_CAST macro to work IMirror const *mirror; }; } Mirror; typedef struct { const void *typeclass; void *function; } MirroredTypeclass; static inline int mirror_is_typeid(const Mirror *mirror, typeid id) { return mirror->tc->get_typeid(mirror->data) == id; } static inline int mirror_is_typestring(const Mirror *mirror, const char *id) { return strcmp(id, mirror->tc->get_typestring(mirror->data)) == 0; } static inline int mirror_eq(const Mirror *lhs, const Mirror *rhs) { return lhs->tc->get_typeid(lhs->data) == rhs->tc->get_typeid(rhs->data); } extern const void* mirror_get_typeclass(void* data, IMirror const* tc, const char* typeclass); // get the wrapper function for a given typeclass name // example: // mirror_get_function(physics_entity.data, physics_entity.mirror, "BehaviourEntity") extern void* mirror_get_function(void* data, IMirror const* tc, const char* typeclass_name); // macro reexport of mirror_get_function which will cast the function so it can be called immediately // example: // MIRROR_GET_WRAP_FUNC(physics_entity.data, physics_entity.mirror, BehaviourEntity)(physics_entity.data) #define MIRROR_GET_WRAP_FUNC(Data_, MirrorTc_, Typeclass_)\ ((Typeclass_(*)(void*))mirror_get_function(Data_, MirrorTc_, #Typeclass_)) // casting only works if the typeclass in question exposes IMirror as .mirror // will segfault if the Mirror does not expose To_ // example: // TC_CAST(physics_entity, BehaviourEntity) #define TC_CAST(From_, To_)\ MIRROR_GET_WRAP_FUNC(From_.data, From_.mirror, To_)(From_.data) #define TC_MIRRORS(From_, To_)\ MIRROR_GET_WRAP_FUNC(From_.data, From_.mirror, To_) != NULL #define MIRROR_TRY_WRAP(Into_, Mirror_, Typeclass_){\ MirroredTypeclassWrapFunc fn_ = mirror_get_typeclass(Mirror_, #Typeclass_);\ if(fn_ != NULL) {\ Into_ = (TypeClass_)fn(Mirror_->data);\ }\ } #define impl_Mirror_for(T, get_typestring_f, get_typeid_f, get_typeclasses_f)\ Mirror T##_as_Mirror(T *x) {\ TC_FN_TYPECHECK(const char*, get_typestring_f, T*);\ TC_FN_TYPECHECK(typeid, get_typeid_f, T*);\ TC_FN_TYPECHECK(Dictionary*, get_typeclasses_f, T*);\ static IMirror const tc = {\ .get_typestring = (const char*(*const)(void*)) get_typestring_f,\ .get_typeid = (typeid (*const)(void*)) get_typeid_f,\ .get_typeclasses = (Dictionary* (*const)(void*)) get_typeclasses_f,\ };\ return (Mirror){.tc = &tc, .data = x};\ } #define DECL_REFLECT(T)\ extern const char *T##_get_typestring(T *self);\ extern typeid T##_get_typeid(T *self);\ extern Dictionary *T##_get_typeclasses(T* self);\ decl_typeclass_impl(Mirror, T) #define START_REFLECT(T)\ const char* T##_get_typestring(T *self) {\ static char const *const typestring = #T;\ return typestring;\ }\ typeid T##_get_typeid(T *self) {\ static char init_flag = 0;\ static typeid id = 0;\ if(!init_flag) {\ init_flag = 1;\ id = strhash(#T);\ }\ return id;\ }\ Dictionary *T##_get_typeclasses(T* self) {\ static char init_flag = 0;\ static Dictionary typeclasses;\ if(!init_flag) {\ init_flag = 1;\ typeclasses = dictionary_new(sizeof(MirroredTypeclass));\ MirroredTypeclass tc;\ REFLECT_TYPECLASS(T, Mirror) #define REFLECT_TYPECLASS(T, TypeClass_)\ tc = (MirroredTypeclass){\ .typeclass = (void*)T##_as_##TypeClass_(NULL).tc,\ .function = (void*)T##_as_##TypeClass_\ };\ dictionary_set_raw(&typeclasses, #TypeClass_, &tc); #define END_REFLECT(T)\ }\ return &typeclasses;\ }\ impl_Mirror_for(T, T##_get_typestring, T##_get_typeid, T##_get_typeclasses)
mirror.c #include "mirror.h" MirroredTypeclass* internal_mirror_get_typeclass(void* self, IMirror const* tc, const char* typeclass) { return dictionary_get_raw(tc->get_typeclasses(self), typeclass); } const void* mirror_get_typeclass(void* data, IMirror const* tc, const char* typeclass) { MirroredTypeclass* class = internal_mirror_get_typeclass(data, tc, typeclass); if(class != NULL) return class->typeclass; else return NULL; } void* mirror_get_function(void* data, IMirror const* tc, const char* typeclass_name) { MirroredTypeclass* class = internal_mirror_get_typeclass(data, tc, typeclass_name); if(class != NULL) return class->function; else return NULL; }
Huh, what, why?
So, my game engine has a rudimentary physics engine. I wanted some way to query that physics engine with simple functions like
PhysicsEntity *result = physics_world_query_box(position, extents, layers);
But this comes with a limitation. Say I want to have some objects that can be damaged. I would want a DealDamage function that could be called on the target object. But with what I had at the time, I'd have had to add a deal_damage function to the interface of PhysicsEntity. Which I really didn't want to do because of the
While not prescribing the player's actions or the world's reactions.
part of my “definition” of a generic game engine.
So instead I needed to add another layer of abstraction. I needed some way of exposing any function through the PhysicsEntity typeclass.
There's two ways I considered implementing the Mirror typeclass. It would either allow you to call functions using a string, or allow you to get other typeclasses using a string. I decided on the latter as it would allow me to be slightly better about respecting function signatures. The alternative would require casting function pointers even more than I'm already doing, which I dislike.
How
Implementing the Mirror typeclass for any given struct looks something like this.
Player.h: ... DECL_REFLECT(Player)
This serves the same purpose as the generic decl_typeclass_impl but rather than just declaring Player_as_Mirror it also declares Player_get_typestring, Player_get_typeid, and Player_get_typeclasses. This is because these functions are implemented by some more macros which we'll place in Player.c:
Player.c
START_REFLECT(Player)
REFLECT_TYPECLASS(Player, Drop)
REFLECT_TYPECLASS(Player, PhysicsEntity)
REFLECT_TYPECLASS(Player, BehaviourEntity)
REFLECT_TYPECLASS(Player, Transformable)
END_REFLECT(Player)
...
This is where most of the magic happens, whether it's curses or miracles we can deliberate later.
START_REFLECT will implement the aforementioned functions Player_get_typestring and Player_get_typeid. When END_REFLECT expands, it'll include impl_Mirror_for_Player and all of it's arguments. START_REFLECT ends before the closing brace of Player_get_typeclasses, instead the closing braces are expanded from END_REFLECT.
REFLECT_TYPECLASS is then put inbetween the two. Every instance of REFLECT_TYPECLASS adds some code that will be run the first time Player_get_typeclasses is called. This bit of code inserts a value to a static dictionary containing a pointer to the function that converts a Player instance to a typeclass, with the name of the typeclass as it's key.
So now, if another typeclass has Mirror as a requirement, an end programmer can use Mirror to
- find out if this object implements a given typeclass.
and
- get the object as a different typeclass.
Meaning that I don't have to add a deal_damage function to my PhysicsEntity interface. Instead I can require Mirror in PhysicsObject like any other typeclass, then check if the hit entity implements Damagable, get an instance of Damagable (or IDamagable directly) and call the object's deal_damage function that way.
