Some folks have been surprised by the architecture of Wizwag (wishlist here), so I thought I would give a quick explainer of how things currently are, and why. Wizwag is written in C, with specific conventions I follow to ensure it remains easy and fun to work on.
Wizwag game objects are simple, and typical of many C games - they are a "fat struct." For those unfamiliar with the concept, it's a monolithic type that is used for all objects ("entities", "things", whatever) in the game, and you get different behavior by configuring the object differently. This can be applied at any "level" of a program where there is a "fundamental unit of thing-ness" with variants.
There are lots of ways to do this!
type field that you switch on.Some games use a union to save space per-object based on a top-level type,
but we don't do that for the following reasons:
Wizwag currently uses a mixture of the "configured sub-objects" style and the entity type field style. Sub-objects are probably less efficient than having the bit array in ways, since fields are not ordered by alignment, but it makes my life a lot simpler when defining an object by writing an object literal, or looking at specific "components" in a piece of code.
Here's how an entity is shaped in Wizwag right now:
typedef struct {
bool exists;
bool enabled;
bool keep_between_rooms;
// ... some other flags ...
} wiz_meta_t;
typedef struct {
f32 x, y, z;
f32 dx, dy, dz;
wiz_dir_t facing;
} wiz_transform_t;
// ... other "component" types ...
typedef struct {
wiz_meta_t meta;
wiz_transform_t transform;
wiz_collider_t collider;
wiz_hitbox_t hitbox;
wiz_npc_t npc;
wiz_player_t player;
wiz_pickup_t pickup;
wiz_giblets_t giblets;
wiz_dust_trail_t dust_trail;
wiz_terrain_detector_t terrain;
// ... many other things ...
} wiz_entity_t;
#define WIZ_MAX_ENTITIES 10000
Each sub-struct in there except a few has a bool enabled; which specifies if
the object "has" that "component" or not.
Then, systems (because this is a fake ECS, after all) filter entities based on what "components" they "have". Make sense? It looks like this:
// ... do any per-frame setup here ...
wiz_entity_t *entity = rt->game.entities;
for (
int entity_index = 0;
entity_index < ARRAY_COUNT(rt->game.entities);
++entity_index, ++entity
) {
// Filter
if (!entity->meta.exists) { continue; }
if (!entity->meta.enabled) { continue; }
if (!entity->foo.enabled) { continue; }
// Fetch or compute things we care about for this entity
wiz_transform_t *transform = &entity->transform;
wiz_foo_t *foo = &entity->foo;
// ... do actual per-entity work here ...
}
// ... do any per-frame finish up here ...
Yes, that's a lot of ifs and some of them are redundantly typed in nearly every system in the game right now. In practice, it's fine.
If it's ever too slow I will implement a skip list of live entities (as an intrusive list) or something like this. When your code stays simple there's always lots of easy perf wins on the table you can reach for.
With this setup, entities can be created inline as object literals, and sometimes I do that for testing an idea, but once something is "real" I will give it a factory function, like this one:
static wiz_entity_t *
wiz_create_chest(
wiz_runtime_t *rt,
f32 tile_x,
f32 tile_y,
u16 save_key,
wiz_chest_reward_t reward_type,
u8 reward_param,
u8 style
) {
f32 x = (tile_x * 16) + 8;
f32 y = (tile_y * 16) + 8;
// If save_key is set, check if already opened; zero means temporary chest
bool already_opened = (save_key && rt->game.save.values[ save_key ] != 0);
if (WIZ_DEBUG_CHEST_OPENING) { already_opened = false; }
return wiz_add_entity(rt, (wiz_entity_t){
.meta = {
.room_is_loading = true,
.entity_type = WIZ_ENTITY_TYPE_CHEST,
.save_key = save_key,
},
.transform = {
.x = x,
.y = y,
},
.collider = {
.enabled = true,
.priority = 10,
.rect = { .x = -6, .y = -8, .w = 12, .h = 8 },
},
.sprite = {
.enabled = true,
.offset_x = -8,
.offset_y = -16,
.desc = {
.sheet = WIZ_SHEET_CHESTS,
.x = ((already_opened)? 16 : 0),
.y = style * 16,
.w = 16,
.h = 16,
},
},
.chest = {
.enabled = true,
.opened = already_opened,
.reward_type = reward_type,
.reward_param = reward_param,
},
});
}
And that's (mostly) all there is to it! I try to keep things super duper simple, whenever I can. Being able to create things solely as an object literal is pleasant.
But wait, what about the special cases? I'm glad you asked! This problem is solved in layers, and I'll go through them briefly.
For this kind of one-off, objects do have an "entity type" enum that can be set and queried wherever necessary in the codebase. A few systems have checks like this contrived example:
if (meta->type == WIZ_ETYPE_CHICKEN) {
air_velocity *= 0.125f;
}
// ... proceed as normal ...
This is the sort of thing I would categorize as a "hack" since it hasn't been elevated into a "feature" that other things could use. If it truly is a special case then this structure reflects that without overcomplicating things.
Sometimes you end up with conceptual categories you can bin multiple objects into, and you want to be able to specify that cleanly. You could store an int on every guy for his base health, but why do that when you can store it per-guy-type, or per-category-of-guys?
The granularity of this is up to you and your game's needs. For Wizwag, there's kind of a spectrum depending on the feature. There is a table of entity metadata keyed off the entity type enum, and that's used to set things like the max health of enemies, whether an entity type is a boss or not, etc.
This configuration doesn't need to live on each individual entity of a category.
Like much other data in the game, this table is a const static array in a header,
with entries that look like this:
[ WIZ_ENTITY_TYPE_BOSS_JELLY ] = {
.is_boss = true,
.health = 20,
.damage = 2,
.iframes = WIZ_IFRAMES_NORMAL,
.ignore_knockback = true,
.shield_knockback = WIZ_SHIELD_KNOCKBACK_HEAVY,
.is_counted_enemy = true,
.drops = WIZ_DROPS_NOTHING,
.hurt_sound = WIZ_SOUND_BOSS_JELLY_HURT,
},
Some systems that care about this stuff will fetch the entity type matadata for an entity and check a specific property or set of properties. Other systems ignore this data entirely.
If a piece of data is so large and uncommon that you don't want every entity to have space for it, you can pull it out and then find a way to associate a given entity with it indirectly.
There aren't many cases of this in Wizwag so far. Even pathfinding data is pretty small, so it is included in the fat struct. Ten thousand entities only take up about 11 MiB of space right now, and that's before I've done anything to optimize their size.
This kind of data comes in two flavors: Shared data and individual data. For example, the pathfinding graph is usually shared data, and in our case belongs to the pathfinding system, but a path itself is individual data, and lives on the entity.
Pathfinding data is dynamic, but you can also have static shared data like the definition for an animation, or a decision tree. In Wizwag, these sorts of things are usually in large tables, keyed with an enum.
For another example, entities can have a "brain" with a specific "brain ID." The runtime state of the brain lives in the entity, but the actual behavior is immutable, encoded in an object literal, and shared across all users of that particular kind of brain.
Most enemies use a single brain type, but some, like a boss with phases, may define multiple brains and swap between them at runtime.
So the entity side of it looks like this:
// ... in some entity object literal ...
.brain = {
.enabled = true,
.brain_id = WIZ_BRAIN_ID_GATAGATA,
},
And the shared behavior itself looks like this:
// ... in the big table of brain definitions ...
[ WIZ_BRAIN_ID_GATAGATA ] = {
.initial_state = WIZ_BRAIN_STATE_WANDER_1,
.states = {
[WIZ_BRAIN_STATE_WANDER_1] = {
.play_anim = WIZ_ANIM_WALK,
.speed = 0.25f,
.walk_durations = { 30, 60, 90 },
.wait_durations = { 0 },
.is_8_directional = true,
},
[WIZ_BRAIN_STATE_STUNNED] = {
.play_anim = WIZ_ANIM_STUNNED,
.transitions = {
{
.next_state = WIZ_BRAIN_STATE_WANDER_1,
.conditions = {{ .type = WIZ_BRAIN_CONDITION_NOT_STUNNED, }},
},
},
},
},
},
I wanted to have the convenience of thinking about entities as bags of components without any of the attendant headaches of strucuturing entities as bags of components in C.
The runtime data for each "component" is one line placing a struct in another struct. Adding a component to an entity is setting a value on a struct. It's dead simple.
This setup means I have zero allocations, all entity data is in contiguous memory, I can access "components" trivially with the dot operator without needing to resolve them from separate per-component-type storage, and cache locality may even be better since I'm usually accessing several "components" on a given entity at a time anyway. I haven't actually checked that though.
More "wide" systems like hitboxes are structured as operating on entities with the appropriate set of "components" but there are also more "narrow" systems that filter on specific entity type values or other criteria.
There's an collection of architectural decisions here that can apply at different levels of granularity for different use cases, but this setup has worked well for me so far on this particular project.
That's all for now! In the next post I may go over how "systems" are set up in this scheme because I had to experiment a bit to find an arrangement I actually like.
Even now it isn't perfect, but it's a lot less painful than it could be.