Skip to content

Instantly share code, notes, and snippets.

@justinbowes
Last active December 21, 2015 05:09
Show Gist options
  • Save justinbowes/6254718 to your computer and use it in GitHub Desktop.
Save justinbowes/6254718 to your computer and use it in GitHub Desktop.
A complete entity system implemented in C. See entity_system.
C entity system. Justin Bowes, Informi Software Inc. Public domain. Go nuts.
The only external dependency is the wonderful UThash by Troy D. Hanson,
which I overuse.
http://troydhanson.github.io/uthash/
The system is intended to be used primarily through the macros. You
supply the component type you expect; the code hashes the type (as
a string) and casts the result (as pointer to the type you passed).
To process components, I pre-allocate an xpl_component_result_set.
I've included an example processor. It won't compile; it's just to
show you how I use the system.
Outside of the processor, this should be complete or very close.
Yes, I actually use this, with quite a lot of startup code to define
allocators and destructors. I wrap the xpl_ functions in a non-xpl_
singleton version, since I've never needed more than one entity system
in a project.
Due to the use of PS_HASH, compiling with -O1 and better dramatically improves
component fetch performance (not that performance has been an issue for me).
PS_HASH is a compile-time string hash.
Formatting should look fine using spaces(4).
//
// processor_newtonian.c
// p1
//
// Created by Justin Bowes on 2013-02-28.
// Copyright (c) 2013 Informi Software Inc. All rights reserved.
//
// Public domain.
#include "xpl_vec.h"
#include "xpl_entity_system.h"
#include "components/component_newtonian.h"
#include "components/component_position.h"
#include "components/component_orientation.h"
#include "processors/processor_newtonian.h"
static component_result_set_t *results = NULL;
void processor_newtonian_init() {
results = xpl_component_result_set_new();
}
void processor_newtonian_engine_step(xpl_es_t *es, double timestep) {
// Fill result set with all active newtonians.
if (! xpl_components_of_type(es, cnewtonian_t, results)) return;
size_t index;
entity_t eid;
cnewtonian_t *cnewtonian;
component_foreach(results, index, eid, cnewtonian) {
xvec3 velocity_slice = xvec3_scale(cnewtonian->velocity, timestep);
// Fetch the position and accumulate velocity. This is crude.
cposition_t *position = xpl_entity_component(es, eid, cposition_t);
position->position = xvec3_add(position->position, velocity_slice);
xquat rotation_slice = cnewtonian->rotation;
rotation_slice.w /= timestep;
xquat_normalize(&rotation_slice, &rotation_slice);
corientation_t *orientation = xpl_entity_component(es, eid, corientation_t);
xquat_multiply(&orientation->orientation, &rotation_slice, NULL);
xquat_normalize(&orientation->orientation, &orientation->orientation);
}
}
void processor_newtonian_destroy() {
xpl_component_result_set_destroy(&results);
}
//
// xpl_compile_time_hash.h
// p1
//
// Created by Justin Bowes on 2013-02-20.
// Copyright (c) 2013 Informi Software Inc. All rights reserved.
//
// Public domain.
//
// Hash for strings at -O1 and better.
#ifndef p1_xpl_compile_time_hash_h
#define p1_xpl_compile_time_hash_h
// source:
// http://lol.zoy.org/blog/2011/12/20/cpp-constant-string-hash
// dead link
#include <string.h>
#include <stdint.h>
#include <stdio.h>
#define H1(s,i,x) (x * 65599u + (uint8_t)s[(i) < strlen(s) ? strlen(s) - 1 - (i) : strlen(s)])
#define H4(s,i,x) H1(s, i, H1(s, i + 1, H1(s, i + 2, H1(s, i + 3, x))))
#define H16(s,i,x) H4(s, i, H4(s, i + 4, H4(s, i + 8, H4(s, i + 12, x))))
#define H64(s,i,x) H16(s, i, H16(s, i + 16, H16(s, i + 32, H16(s, i + 48, x))))
#define H256(s,i,x) H64(s, i, H64(s, i + 64, H64(s, i + 128, H64(s, i + 192, x))))
#define PS_HASH(s) ((uint32_t)(H16(s,0,17)))
#endif
/*
* xpl_es.c
*
* Created on: 2012-12-13
* Author: jbowes
*
* Public domain.
*/
#include <assert.h>
#include "uthash.h"
#include "xpl_es.h"
typedef struct es_component {
xpl_entity eid;
xpl_type_id type_id;
void *data;
UT_hash_handle hh_main_by_ptr;
UT_hash_handle hh_entity_by_type_id;
UT_hash_handle hh_type_id_by_eid;
} es_component_t;
typedef struct es_component_by_type_id {
xpl_type_id type_id;
es_component_t *by_eid_table;
UT_hash_handle hh;
} es_component_by_type_id_t;
typedef struct es_components {
es_component_t *by_ptr_table;
es_component_by_type_id_t *by_type_id_table;
} es_components_t;
typedef struct es_entity {
xpl_entity id;
es_component_t *component_table;
UT_hash_handle hh;
} es_entity_t;
typedef struct es_entities {
xpl_entity next_id;
es_entity_t *entity_table;
} es_entities_t;
typedef struct es_allocator {
xpl_type_id type_id;
xpl_component_allocator allocator;
xpl_component_destructor destructor;
UT_hash_handle hh;
} es_allocator_t;
struct xpl_es_context {
es_components_t *components;
es_entities_t *entities;
es_allocator_t *allocators;
};
static void entity_destroy(xpl_es_t *context, es_entity_t *entity) {
es_component_t *component, *tmp;
HASH_ITER(hh_entity_by_type_id, entity->component_table, component, tmp) {
xpl_entity_component_destroy(context, entity->id, component->data);
}
HASH_DEL(context->entities->entity_table, entity);
xpl_free(entity);
}
xpl_es_t *xpl_es_new() {
xpl_es_t *context = xpl_calloc_type(xpl_es_t);
context->entities = xpl_calloc_type(es_entities_t);
context->entities->next_id = XPL_ENTITY_NONE + 1;
context->components = xpl_calloc_type(es_components_t);
return context;
}
void xpl_es_destroy(xpl_es_t **ppcontext) {
assert(ppcontext);
xpl_es_t *context = *ppcontext;
assert(context);
es_entity_t *entity, *etmp;
HASH_ITER(hh, context->entities->entity_table, entity, etmp) {
entity_destroy(context, entity);
}
// We don't purge the type_id table when its lists are empty for performance,
// so we have to do it now.
es_component_by_type_id_t *by_type_id, *tmp;
HASH_ITER(hh, context->components->by_type_id_table, by_type_id, tmp) {
HASH_DEL(context->components->by_type_id_table, by_type_id);
assert(by_type_id->by_eid_table == NULL);
xpl_free(by_type_id);
}
es_allocator_t *allocator, *atmp;
HASH_ITER(hh, context->allocators, allocator, atmp) {
HASH_DEL(context->allocators, allocator);
xpl_free(allocator);
}
xpl_free(context->entities);
xpl_free(context->components);
xpl_free(context);
*ppcontext = NULL;
}
xpl_entity xpl_entity_new(xpl_es_t *context) {
assert(context);
xpl_entity eid = context->entities->next_id++;
es_entity_t *entity = xpl_calloc_type(es_entity_t);
entity->id = eid;
entity->component_table = NULL;
HASH_ADD_INT(context->entities->entity_table, id, entity);
return eid;
}
void xpl_entity_destroy(xpl_es_t *context, xpl_entity entity) {
assert(context);
es_entity_t *entry;
HASH_FIND_INT(context->entities->entity_table, &entity, entry);
assert(entry);
entity_destroy(context, entry);
}
xpl_entity xpl_entity_transfer(xpl_es_t *source, xpl_entity source_entity, xpl_es_t *dest) {
xpl_entity destination_entity = xpl_entity_new(dest);
es_entity_t *entity;
HASH_FIND_INT(source->entities->entity_table, &source_entity, entity);
assert(entity);
es_component_t *component, *tmp;
HASH_ITER(hh_entity_by_type_id, entity->component_table, component, tmp) {
void *data = component->data;
const xpl_type_id type_id = component->type_id;
xpl_component_remove_type_id(source, source_entity, data, type_id);
xpl_component_assign_type_id(dest, destination_entity, data, type_id);
}
xpl_entity_destroy(source, source_entity);
return destination_entity;
}
void *xpl_component_assign_type_id(xpl_es_t *context, const xpl_entity entity, void *component, const xpl_type_id type_id) {
if (xpl_entity_component_with_type_id(context, entity, type_id)) {
LOG_ERROR("Duplicate component assignment to %u!", entity);
assert(0);
}
assert(context);
es_component_t *esc = xpl_calloc_type(es_component_t);
esc->eid = entity;
esc->data = component;
esc->type_id = type_id;
HASH_ADD(hh_main_by_ptr, context->components->by_ptr_table, data, sizeof(void *), esc);
es_component_by_type_id_t *by_type_id;
HASH_FIND_INT(context->components->by_type_id_table, &type_id, by_type_id);
if (! by_type_id) {
by_type_id = xpl_alloc_type(es_component_by_type_id_t);
by_type_id->type_id = type_id;
by_type_id->by_eid_table = NULL;
HASH_ADD_INT(context->components->by_type_id_table, type_id, by_type_id);
}
HASH_ADD(hh_type_id_by_eid, by_type_id->by_eid_table, eid, sizeof(xpl_entity), esc);
es_entity_t *ee;
HASH_FIND_INT(context->entities->entity_table, &entity, ee);
assert(ee);
HASH_ADD(hh_entity_by_type_id, ee->component_table, type_id, sizeof(type_id), esc);
return component;
}
void *xpl_component_remove_type_id(xpl_es_t *context, const xpl_entity entity, void *component, const xpl_type_id type_id) {
assert(context);
assert(type_id);
es_component_t *esc;
HASH_FIND(hh_main_by_ptr, context->components->by_ptr_table, &component, sizeof(void *), esc);
assert(esc);
HASH_DELETE(hh_main_by_ptr, context->components->by_ptr_table, esc);
es_component_by_type_id_t *by_type_id;
HASH_FIND_INT(context->components->by_type_id_table, &type_id, by_type_id);
assert(by_type_id);
HASH_DELETE(hh_type_id_by_eid, by_type_id->by_eid_table, esc);
es_entity_t *ee;
HASH_FIND_INT(context->entities->entity_table, &esc->eid, ee);
assert(ee);
HASH_DELETE(hh_entity_by_type_id, ee->component_table, esc);
xpl_free(esc);
return component;
}
void xpl_component_allocator_for_type_id(xpl_es_t *context, const xpl_type_id type_id, xpl_component_allocator allocator, xpl_component_destructor destructor) {
assert(context);
es_allocator_t *entry;
HASH_FIND_INT(context->allocators, &type_id, entry);
if (! entry) {
entry = xpl_calloc_type(es_allocator_t);
entry->type_id = type_id;
HASH_ADD_INT(context->allocators, type_id, entry);
}
entry->allocator = allocator;
entry->destructor = destructor;
}
static void *component_create_with_type_id(xpl_es_t *context, const xpl_type_id type_id, size_t size) {
assert(context);
es_allocator_t *allocator;
HASH_FIND_INT(context->allocators, &type_id, allocator);
if (allocator) return allocator->allocator();
return xpl_calloc(size);
}
void xpl_component_destroy_with_type_id(xpl_es_t *context, void *component, const xpl_type_id type_id) {
assert(component);
// This looks dangerous. We actually do the free FIRST so that any
// last second eid lookups in registered destructors will work. THEN
// we destroy the association with the entity, after the component
// is dead and gone.
es_allocator_t *allocator;
HASH_FIND_INT(context->allocators, &type_id, allocator);
if (allocator) {
allocator->destructor(component);
} else {
xpl_free(component);
}
es_component_t *esc;
HASH_FIND(hh_main_by_ptr, context->components->by_ptr_table, &component, sizeof(void *), esc);
if (esc) {
// Not necessary for the component to be registered.
// The ES allocates the component and allows it to be detached, so it must
// allow destroy after detach.
xpl_component_remove_type_id(context, esc->eid, esc->data, esc->type_id);
}
}
void * xpl_component_new_with_type_id(xpl_es_t *context, xpl_entity entity, const xpl_type_id type_id, const size_t size) {
void *component = component_create_with_type_id(context, type_id, size);
xpl_component_assign_type_id(context, entity, component, type_id);
return component;
}
void xpl_entity_component_destroy(xpl_es_t *context, xpl_entity entity, void *component_data) {
const xpl_type_id type_id = xpl_component_type_id(context, component_data);
xpl_component_destroy_with_type_id(context, xpl_component_remove_type_id(context, entity, component_data, type_id), type_id);
}
xpl_entity xpl_entity_with_component(xpl_es_t *context, const void *component) {
assert(context);
assert(component);
es_component_t *esc;
HASH_FIND(hh_main_by_ptr, context->components->by_ptr_table, &component, sizeof(void *), esc);
return esc ? esc->eid : XPL_ENTITY_NONE;
}
xpl_type_id xpl_component_type_id(xpl_es_t *context, const void *component) {
assert(context);
assert(component);
es_component_t *esc;
HASH_FIND(hh_main_by_ptr, context->components->by_ptr_table, &component, sizeof(void *), esc);
return esc ? esc->type_id : 0;
}
xpl_component_result_set_t *xpl_component_result_set_new() {
size_t max_size = 2;
xpl_component_result_set_t *set = xpl_calloc_type(xpl_component_result_set_t);
set->result = xpl_calloc((max_size) * sizeof(xpl_component_result_t));
set->max_size = max_size;
set->size = 0;
return set;
}
void xpl_component_result_set_destroy(xpl_component_result_set_t **ppset) {
assert(ppset);
xpl_component_result_set_t *set = *ppset;
assert(set);
xpl_free(set->result);
*ppset = NULL;
}
size_t xpl_components_with_type_id(xpl_es_t *context, const xpl_type_id type_id, xpl_component_result_set_t *results) {
assert(results);
assert(results->max_size > 0);
results->size = 0;
es_component_by_type_id_t *by_type_id;
HASH_FIND_INT(context->components->by_type_id_table, &type_id, by_type_id);
if (by_type_id) {
es_component_t *esc, *tmp;
HASH_ITER(hh_type_id_by_eid, by_type_id->by_eid_table, esc, tmp) {
size_t index = results->size++;
results->result[index].eid = esc->eid;
results->result[index].component = esc->data;
// Resize in case of imminent overflow
if (results->size == results->max_size) {
results->max_size *= 1.75 + 1;
results->result = xpl_realloc(results->result, (1 + results->max_size) * sizeof(xpl_component_result_t));
}
}
}
// We pad the allocated array so that this is always safe.
results->result[results->size].eid = XPL_ENTITY_NONE;
results->result[results->size].component = NULL;
return results->size;
}
void * xpl_component_with_type_id(xpl_es_t *context, const xpl_type_id type_id, xpl_entity *entity_out) {
es_component_by_type_id_t *by_type_id;
HASH_FIND_INT(context->components->by_type_id_table, &type_id, by_type_id);
if (by_type_id) {
es_component_t *esc, *tmp;
HASH_ITER(hh_type_id_by_eid, by_type_id->by_eid_table, esc, tmp) {
if (entity_out) *entity_out = esc->eid;
return esc->data;
}
}
if (entity_out) *entity_out = XPL_ENTITY_NONE;
return NULL;
}
void * xpl_entity_component_with_type_id(xpl_es_t *context, xpl_entity entity, const xpl_type_id type_id) {
es_entity_t *ee;
HASH_FIND_INT(context->entities->entity_table, &entity, ee);
if (! ee) {
assert(ee); // Entity doesn't exist
}
es_component_t *ec;
HASH_FIND(hh_entity_by_type_id, ee->component_table, &type_id, sizeof(type_id), ec);
return ec ? ec->data : NULL;
}
xpl_entity xpl_only_entity_with_component_type_id(xpl_es_t *context, const xpl_type_id type_id) {
xpl_entity e;
xpl_component_with_type_id(context, type_id, &e);
return e;
}
/*
* xpl_es.h
*
* Entity system with component constructor/destructors.
*
* Requires UThash by Troy D. Hanson. https://github.com/troydhanson/uthash
* Also uses a preprocessor hash for string hashing (credited).
*
* Created on: 2012-12-13
* Author: jbowes
*
* Public domain.
*/
#ifndef XPL_ES_H_
#define XPL_ES_H_
#include <stdint.h>
#include "uthash.h"
#include "utlist.h"
#include "xpl_compile_time_hash.h"
#define XPL_ENTITY_NONE 0
typedef uint32_t xpl_entity;
typedef uint32_t xpl_type_id;
typedef void *(* xpl_component_allocator)(void);
typedef void (* xpl_component_destructor)(void *);
typedef struct xpl_component_result {
xpl_entity eid;
void *component;
} xpl_component_result_t;
typedef struct xpl_component_result_set {
xpl_component_result_t *result;
size_t size;
size_t max_size;
} xpl_component_result_set_t;
typedef struct xpl_es_context xpl_es_t;
xpl_es_t *xpl_es_new(void);
void xpl_es_destroy(xpl_es_t **ppcontext);
xpl_entity xpl_entity_new(xpl_es_t *context);
void xpl_entity_destroy(xpl_es_t *context, xpl_entity entity);
// Transfer an entity's components from the source entity system to a new entity
// in the destination entity system (without reallocation). Returns the new entity.
xpl_entity xpl_entity_transfer(xpl_es_t *source, xpl_entity source_entity, xpl_es_t *dest);
void xpl_component_allocator_for_type_id(xpl_es_t *context, const xpl_type_id type_id, xpl_component_allocator allocator, xpl_component_destructor destructor);
void *xpl_component_new_with_type_id(xpl_es_t *es, xpl_entity entity, const xpl_type_id type_id, const size_t size);
void xpl_component_destroy_with_type_id(xpl_es_t *es, void *component, const xpl_type_id type_id);
void xpl_entity_component_destroy(xpl_es_t *es, const xpl_entity entity, void *component_data);
xpl_entity xpl_entity_with_component(xpl_es_t *es, const void *component);
xpl_type_id xpl_component_type_id(xpl_es_t *context, const void *component);
void *xpl_component_assign_type_id(xpl_es_t *context, const xpl_entity entity, void *component, const xpl_type_id type_id);
void *xpl_component_remove_type_id(xpl_es_t *context, const xpl_entity entity, void *component, const xpl_type_id type_id);
size_t xpl_components_with_type_id(xpl_es_t *context, const xpl_type_id type_id, xpl_component_result_set_t *result);
void * xpl_component_with_type_id(xpl_es_t *context, const xpl_type_id type_id, xpl_entity *entity_out);
xpl_entity xpl_only_entity_with_component_type_id(xpl_es_t *context, const xpl_type_id type_id);
void * xpl_entity_component_with_type_id(xpl_es_t *context, const xpl_entity entity, const xpl_type_id type_id);
xpl_component_result_set_t *xpl_component_result_set_new(void);
void xpl_component_result_set_destroy(xpl_component_result_set_t **ppset);
#define xpl_component_new(context, entity, type) \
((type *)(xpl_component_new_with_type_id(context, entity, PS_HASH(#type), sizeof(type))))
#define xpl_component_allocator(context, type, allocator, destructor) \
xpl_component_allocator_for_type_id(context, PS_HASH(#type), allocator, destructor);
#define xpl_component_destroy(context, component, type) \
xpl_component_destroy_with_type_id(context, component, PS_HASH(#type));
#define xpl_component_assign(context, entity, component_data, type) \
xpl_component_assign_type_id(context, entity, component_data, PS_HASH(#type))
#define xpl_component_remove(context, entity, component_data) \
xpl_component_remove_type_id(context, entity, component_data, xpl_component_type_id(context, component_data))
#define xpl_components_of_type(context, type, result) \
xpl_components_with_type_id(context, PS_HASH(#type), result)
#define xpl_component_of_type(context, type, entity_out) \
((type *)xpl_component_with_type_id(context, PS_HASH(#type), entity_out))
#define xpl_only_entity_with_component_type(context, type) \
xpl_only_entity_with_component_type_id(context, PS_HASH(#type))
#define xpl_entity_component(context, entity, type) \
((type *)xpl_entity_component_with_type_id(context, entity, PS_HASH(#type)))
#define xpl_component_foreach(results, idx, entry_eid, el) \
for(idx = (el = results->result[0].component, entry_eid = results->result[0].eid, 0); \
idx < results->size; \
(el = results->result[++idx].component, entry_eid = results->result[idx].eid))
#endif /* XPL_ES_H_ */
//
// xpl_memory.h
// p1
//
// Created by Justin Bowes on 2012-09-02.
// Copyright (c) 2013 Informi Software Inc. All rights reserved.
//
// Public domain.
//
// Replace this with your own memory allocator binding.
#ifndef xpl_memory_h
#define xpl_memory_h
#include <stdlib.h>
#define xpl_alloc(size) malloc(size)
#define xpl_calloc(size) calloc(size, 1)
#define xpl_realloc(ptr, size) realloc(ptr, size)
#define xpl_free(ptr) free(ptr)
#endif
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment