ECS I: Entities, Components and Systems (basic design)

Published November 04, 2014
Advertisement
Last entry:

https://www.gamedev.net/blog/1930/entry-2260428-gui-iii-widget-configuration/

Motivation:

It took me a while to get back on writing since I've been busy. But now, as promised, I'm going to give an intro to the entity/component-system I've developed for my engine.

So back in my early projects, I used to have a "GameObject"-base class, with a virtual draw and render method. This always felt kind of clumsy to begin with. I had to have dozens of different classes for "Player", "Enemy", "Environment", "Background", let alone differnt "manager"-classes, since all of those sub-objects had to somehow be loaded, managed and interact with each other in a different way.

Now I've already heard about ECS back then, but never really wanted to use it. Position as a component?! Dafuq? Why would I ever want to... thats going to be so slow... everything needs a position... you all know the "arguments" one can make in his head, especially when being a novice. But I've learned a thing about permature optimization since then, also for a scalable game-engine ECS seemed to be perfect from what I read.

Getting started:

I still didn't feel comfortable enough to design a such vital system for the engine on my own, without some sort of insight at how it can be done (like I used to sometimes check Qt if I felt unsafe about something). So I was suggested EntityX at the forums. Check it out, its a really cool entity/component-libary:

https://github.com/alecthomas/entityx

Still, the rules for me are "no external libaries", so I just studied its source code and took a note here and there. So don't wonder if you see some sort of similarities.

The general design:

So lets talk about the actual design of the ECS I came up with. I wanted it to be useable as easy as possible. So there is an entity-class that you can create:ecs::Entity& entity = m_entities.CreateEntity();
This entity is basically empty upfront. It has a name (for displaying/serialization purposes). But to give it functionality, one needs components. A component is declared by deriving from a base-class in a very specific way:class Position : public ecs::Component{public: Position(math::Vector2 vPosition); math::Vector2 vPosition;};
Notice the class name is a template argument of its base class. This is called the "CRTP"-pattern, and allows a few very neat things. First of all, this allows each entity to have its unique runtime-id, without using hashes, random-number
etc. Except for DLL-issues with templates, is totally safe. This allows me to access components in the simpliest way possible:
Position* pComponent = entity.GetComponent();
Since each component class has its own ID, and each component instance stores its type-id, I can do a component lookup and (almost) totally safe cast, without any (C++) RTTI. Now the CRPT is useful for many other things too,
like implementing a "Clone"-method for all components, without having to do it manually for every component. Like that:templateclass Component : public BaseComponent // we obviously need a non-templated base component class in order for some polymorpic stuff{ BaseComponent& Clone(void) const override final { return *new Derived(sys::safe_cast(*this)); // safe_cast does dynamic_cast in Debug-build, but only casts statically in release mode }};
The CRPT is actually a really useful patternm that can be applied in quite a lot of places to reduce duplicated code.

Creating components:

Creating components works in a similar way. Since I'm using C++11, I've applied variadic templates in order to create an in-place function for attaching a component:entity.AttachComponent(math::Vector2(32, 32));
The arguments of this function are forwared to the components ctor. This is a shorthand forPosition* pPosition = new Position(math::Vector2(32, 32));entity.AttachComponent(*pPosition);
which safes a lot of unneccessary typing after a time, but also provides a bit more abstraction and possibility for extending code.

Component functionality (system):

Up until this point, you should have noted that the components have no functionality (except for their base classes), but contain purely data. In order to get functionality, one has to define a system. Systems work on
components, and are defined like this:class MoveSystem : public System{public: void Update(double dt) override;};
Notice again the use of CRTP. This is so I can do this again:m_systems.AddSystem();// ...m_systems.UpdateSystem
And what happens inside a systems update? Usually, the system goes over all entities with a specific component, and performs some action on them:void MoveSystem::Update(double dt){ auto vEntities = m_entities.EntitiesWithComponents(); for(auto pEntity : vEntities) { auto pPosition = pEntity->GetComponent(); pPosition->vPos.x += dt; }}
The advantage of this design is, that you can easily exchange and extend component behaviour without touching any non-related code. You can even do that at runtime by enabling/disabling specific systems. Cool, huh?

What to improve:

Obviously, there is a lot of features that are still missing, which I'm going to address in one of the next entries. The worst issue so far, which I havn't really fixed till yet, is that each entity really stores
its own components in a vector. This is bad for data locality and cache coherency, and I'm going to adress it at a time (once it actually starts to really matter and/or if I want to do some impressive benchmarking).
So far, its still working fine - 10000 objects with multiple components that are all being updated didn't pose a problem the last time I did some testing on this (of course the PC was half decent, but still). Here are two screenies
showing the results of this. Its exactly 10000 arrow objects (or where it 20000? damn, it don't even remember), each ~56 vertices, with I belive at least 2 shadow-casters, at 25 FPS with DX9 (framerate is obviously GPU-capped).
Benchmark.jpgBenchmark2.jpg
_______________________________________________

So next time, I'm going to talk about how to handle component communication (hint: messaging incoming). Thanks for reading, see you next time!
0 likes 0 comments

Comments

Nobody has left a comment. You can be the first!
You must log in to join the conversation.
Don't have a GameDev.net account? Sign up!
Advertisement