Skip to content

Instantly share code, notes, and snippets.

@RandyGaul
Created September 25, 2025 21:23
Show Gist options
  • Save RandyGaul/5b5a2ea81ff3cd8ba7d07f1c5e822075 to your computer and use it in GitHub Desktop.
Save RandyGaul/5b5a2ea81ff3cd8ba7d07f1c5e822075 to your computer and use it in GitHub Desktop.
ECS API in C
#define PICO_ECS_IMPLEMENTATION
#include <thirdparty/pico_ecs.h>
ecs_t* g_ecs;
//--------------------------------------------------------------------------------------------------
// Components.
// Adds a component to the ECS.
// ...You must define T_ctor and T_dtor for your component.
#define REGISTER_COMPONENT(T) \
do { \
Entity id = ecs_register_component(g_ecs, sizeof(T), NULL, T##_dtor); \
hadd(g_component_name_to_id_map, sintern(#T), id); \
} while (0)
htbl ecs_id_t* g_component_name_to_id_map = NULL;
// Fetches a pointer to a component given an entity ID.
#define GET_COMPONENT(entity_id, T) \
((T*)ecs_get(g_ecs, entity_id, hget(g_component_name_to_id_map, sintern(#T))))
// Returns true if an entity has a component.
#define HAS_COMPONENT(entity_id, T) !!GET_COMPONENT(entity_id, T)
// Adds a component to an entity.
#define ADD_COMPONENT(entity_id, T, c) \
((T*)add_component_impl(entity_id, sintern(#T), &c))
void* add_component_impl(ecs_id_t id, const char* T, void* c)
{
ecs_id_t* comp_id_ptr = (ecs_id_t*)hget_ptr(g_component_name_to_id_map, T);
CF_ASSERT(comp_id_ptr);
return ecs_add(g_ecs, id, *comp_id_ptr, c);
}
// Removes a component from an entity.
#define DEL_COMPONENT(entity_id, T) \
ecs_remove(g_ecs, entity_id, hget(g_component_name_to_id_map, sintern(#T)))
//--------------------------------------------------------------------------------------------------
// Entities.
// Constructs a blank entity with no components.
// ...Useful for spawning one-off or custom little FX without pre-registering them as discrete types,
// great for particles, bullets, or other small custom items you want defined in-line.
#define make_entity() ecs_create(g_ecs)
// Queues up the destruction of an entity for after all systems are updated.
// ...Avoids dangling references mid-tick.
#define destroy(id) ecs_queue_destroy(g_ecs, id)
// Destroys an entity now, with no queueing mechanism as in `destroy`.
#define destroy_now(id) ecs_destroy(g_ecs, id)
// Registers an entity type with the ECS, so you can call MAKE_ENTITY as-needed. You can of course
// still just make your own blank entities directly via `make_entity`
#define REGISTER_ENTITY(T) \
do { \
FactoryVtable tbl = { \
.make_fn = make_##T, \
.proxy_fn = make_##T##_proxy, \
}; \
hadd(g_factory, sintern(#T), tbl); \
} while(0)
//--------------------------------------------------------------------------------------------------
// Systems.
typedef struct System
{
ecs_id_t id;
bool is_rendering;
void (*on_init)();
void (*on_destroy)();
// Update callbacks are handled by pico_ecs. We call into ecs_update_system manually.
} System;
// Adds a system to the ECS.
// ...You must define update/init/destroy/on_add/on_remove functions for the system.
// ...If the system does rendering, you should instead call `REGISTER_RENDERING_SYSTEM`.
#define REGISTER_SYSTEM(system_name, ...) REGISTER_SYSTEM_IMPL(system_name, false, __VA_ARGS__)
// Adds a rendering system to the ECS.
// ...This system is updated on the rendering category. For general system ticks, use `REGISTER_SYSTEM`.
#define REGISTER_RENDERING_SYSTEM(system_name, ...) REGISTER_SYSTEM_IMPL(system_name, true, __VA_ARGS__)
// Specifies a system only runs upon entities with a required component type.
#define REQUIRE_COMPONENT(system_name, component_name) \
ecs_require_component(g_ecs, hget(g_system_name_to_id_map, sintern(#system_name)), hget(g_component_name_to_id_map, sintern(#component_name)))
// Specifies a system only runs upon entities without a specific component type.
#define EXCLUDE_COMPONENT(system_name, component_name) \
ecs_exclude_component(g_ecs, hget(g_system_name_to_id_map, sintern(#system_name)), hget(g_component_name_to_id_map, sintern(#component_name)))
// Turns on a system so it receives update callbacks.
// ...Systems are enabled by default.
#define ENABLE_SYSTEM(system_name) \
ecs_enable_system(g_ecs, hget(g_system_name_to_id_map, sintern(#system_name)))
// Turns off a system so it no longer receives update callbacks.
// ...Systems are enabled by default.
#define DISABLE_SYSTEM(system_name) \
ecs_disable_system(g_ecs, hget(g_system_name_to_id_map, sintern(#system_name)))
dyna System* g_systems;
// Called to begin a game session.
// ...Game session begins on program startup, or when going from editor -> game.
void init_systems()
{
for (int i = 0; i < asize(g_systems); ++i) {
System s = g_systems[i];
s.on_init();
}
}
// Called when exiting a game session.
// ...Game session ends when program terminates, or when going back to the editor.
void destroy_systems()
{
ecs_reset(g_ecs); // Clear out all entities.
for (int i = 0; i < asize(g_systems); ++i) {
System s = g_systems[i];
s.on_destroy();
}
}
void update_systems()
{
for (int i = 0; i < asize(g_systems); ++i) {
System s = g_systems[i];
if (s.is_rendering) continue;
ecs_update_system(g_ecs, s.id, 0);
}
}
void update_rendering_systems()
{
for (int i = 0; i < asize(g_systems); ++i) {
System s = g_systems[i];
if (!s.is_rendering) continue;
ecs_update_system(g_ecs, s.id, 0);
}
}
htbl ecs_id_t* g_system_name_to_id_map = NULL;
#define REGISTER_SYSTEM_IMPL(system_name, is_rendering_flag, ...) \
do { \
ecs_id_t id = ecs_register_system(g_ecs, system_##system_name##_update, system_##system_name##_on_add, system_##system_name##_on_remove, NULL); \
hadd(g_system_name_to_id_map, sintern(#system_name), id); \
apush(g_systems, (System){ \
.id = id, \
.is_rendering = is_rendering_flag, \
.on_init = system_##system_name##_init, \
.on_destroy = system_##system_name##_destroy, \
}); \
\
} while (0)
//--------------------------------------------------------------------------------------------------
// Proxies.
// ...Small bags of data used for loading entity instances.
// The editor constructs Proxy instances. These store minimal bits of data, either from the
// editor directly, or from decoding a JSON file. These proxies get fed to make_*** functions
// to make entity instances. The entity reads any of the data needed, including general-
// purpose int/float/string/bool data, key'd by strings, in the `properties` array.
//
// The game itself also loads proxy instances from decoding strings directly, when launching
// without the editor.
typedef struct Proxy
{
const char* type;
v2 position;
CF_Sprite preview;
CF_Sprite sprite;
CF_Aabb bb;
dyna struct Property* properties;
bool alive; // Only used by editor.
int tile_id; // Only used by editor.
} Proxy;
typedef struct Property
{
const char* k;
Var v;
} Property;
typedef ecs_id_t Entity;
// Internally used to facilitate making proxies or entities from string, useful for loading
// instances from JSON files.
typedef struct FactoryVtable
{
Entity (*make_fn)(Proxy* proxy);
Proxy (*proxy_fn)(v2 position);
} FactoryVtable;
htbl FactoryVtable* g_factory;
// Makes an entity from a proxy. This is the primary way the editor/game loads entities from JSON.
#define MAKE_ENTITY(type_string, proxy) \
hget(g_factory, sintern(type_string)).make_fn(proxy)
#define MAKE_PROXY(type, position) \
hget(g_factory, sintern(type)).proxy_fn(position)
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment