An entity system
Here’s a cross-post from my Moonman devlog.
Update: Well it’s been a while, but here’s my first update for 2014. I haven’t touched MM code for a month but instead have been designing and implementing a cleaner and simpler entity system. I’ve been meaning to do this for quite a while now, but after using Unity and looking at other bits of code like entityx I decided to finally attempt it. This is helped in part by the new c++ support in VS2013. The system is also data-oriented – all components and entities are tightly packed in memory. I use a similar free list setup as in MM. I also had to use some c++ techniques I haven’t used before, such as variadic templates and typelists. But that is all behind the scenes, this is what the API looks like:
[cpp] EntitySystem es;
// Create an entity // and add some components Entity& e = es.create(); e.add(Transform(4,5)); e.add(Health(10)); e.add(Physics(0,-10)); e.add(ShortDescription(“Ben”)); e.add(Description(“An architecture-obsessed programmer.”));
// Create another entity // with different components // this time using intializer_list shorthand Entity& chest = es.create(); chest.add(Transform(-4.5f, 0.8f)); chest.add(Inventory{ { Item::SWORD }, { Item::POTION, 4 }, { Item::POTION, 3 }, { Item::ARROW, 64 } });
// If we need to keep a reference to an entity // then we use ID’s (uint32s) ID chestId = chest.id;
// Then later on somewhere we can get the entity // and do something with it, e.g., if (es.has(chestId)){ Entity& ch = es.lookup(chestId);
// Shift the chest one unit horizontally Transform& tr = ch.get(); tr.x += 1; }
// Iterating through all entities // can be done with a range-based for loop for (Entity& e : es.entities()){ std::cout « e; }
// Likewise, we can iterate through all // components of a particular type. For // example a PhysicsSystem might want to // process all the Physics components.
for (auto p: es.components()){ // Apply viscocity p.vy *= 0.9f; p.vx *= 0.9f; // Move entity Entity& e = es.lookup(p.entity); Transform& tr = e.get(); tr.x += p.vx; tr.y += p.vy; }
// Everything is done with references // if e doesn’t contain C, then // e.get() returns a blank component // (which can be checked for validity) Entity& e = es.lookup(id); Health& health = e.get(); health.health = 666; if (health){ // It’s valid }
// Components themselves are just structs // The CRTP gives them a unique class id // that is used to store them in EntitySystem struct Health: public Component { float health; bool poisoned; Health(float health = 0.f, bool poisoned = false) :health(health), poisoned(poisoned){}
std::string what() const; // human readable rep static const char* Name(); // name };
// The logic of a component is implemented // in a system. e.g., the HealthSystem might // be responsible for decreasing a character’s // health if they are poisoned.
class HealthSystem : public ISystem { public: bool implements(int componentIndex) override { return Health::Index()==componentIndex; }
void setup(Entity& e) override { e.get().health = 100; }
void update(EntitySystem& es, double dt) override { for (Health& h : es.components()){ if (h.poisoned){ h.health -= 0.1f * (float)dt; if (h.health <= 0){ // Create KILL EVENT } } } } }; [/cpp]
Besides this I’ve also been thinking about having Script components. These will be a special type of component that uses traditional polymorphism and lambdas to offer a concise and easy way to implement special behaviours. For example, I could attach a custom c++ poison script to a entity like this:
[cpp] Script* newPoisonerScript(Entity& e){ // variables to be captured by the lambdas float* duration = new float(0.f); float* timer = new float(0.f);
auto poisoner = new Script(“poisoner”);
poisoner->destroy = [duration, timer](Entity& e){ Health& health = e.get(); if (health) health.poisoned = false; delete duration; delete timer; };
poisoner->start = [](Entity&e){ Health& health = e.get(); if (health) health.poisoned = true; };
poisoner->update = [timer, duration](Entity& e, double dt){ *duration += (float) dt; Health& health = e.get(); if (health){ if (health.poisoned){ *timer -= (float) dt; if (*timer < 0){ health.health -= 1; *timer = 1.0f; } } } };
poisoner->finished = [duration](Entity& e){return *duration>1.5f;}; return poisoner; } [/cpp]
Anyway, once I’ve finalised the new system it’s going to take a couple of days to incorporate it into Moonman, but I definitely think it’s worth the deviation. Basically it means that I’ll turn macro’ed code that looks like this (see devlog for explanation of how it works):
[cpp] ATTRIB(e, x) += 0.1f; bool isOnGround = ATTRIB_OR(e, is_on_ground, false); bool hasPhysics = HAS_ATTRIB(e, is_physics); [/cpp]
Into nicely autocompleting code that looks like this:
[cpp] e.get().x += 0.1f; // or with shorthand e.transform().x += 0.1f; // and bool isOnGround = e.get().isOnGround; bool hasPhysics = e.has(); [/cpp]