Created
September 25, 2025 21:23
-
-
Save RandyGaul/5b5a2ea81ff3cd8ba7d07f1c5e822075 to your computer and use it in GitHub Desktop.
ECS API in C
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| #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