In this article, you will get a rough idea of how things work in the ECS world. I will also share some of my experiences with you.
This is a continuation from my last post, where I talked about the Entity-Component-System (ECS) design pattern. Now I want to show you how to actually use it to build a game with it. If you have not already have seen it, check out what kinda game I built with the help of my ECS.
I will admit this does not look much, but if you ever had build your own game without the help of a big and fancy game engine, like Unity or Unreal, you might give me some credit here. ;) So for the purpose of demonstrating my ECS, I simply just need that much. If you still have not figured out what this game (BountyHunter
) is about, let me help you out with the following picture:
Figure-01: BountyHunter objective and rules.
The picture on the left may look familiar as it is a more abstract view of the game you saw in the video clip. Focus is laid on game entities. On the right hand side, you will find the game objective and rules. This should be pretty much self-explanatory. As you can see, we got a bunch of entity types living in this game world and now you may wonder what they are actually made of? Well, components, of course. While some types of components are common for all these entities, a few are unique for others. Check out the next picture.
Figure-02: Entity and their components.
By looking at this picture, you can easily see the relation between entities and their components (this is not a complete depiction!). All game entities have the Transform-Component in common. Because game entities must be somewhere located in the world, they have a transform, which describes the entities position, rotation and scale. This might be the one and only component attached to an entity. The camera object for instance does require more components, especially not a Material-Component as it will be never visible to the player (this might not be true if you would use it for post-effects). The Bounty
and Collector
entity objects on the other hand do have a visual appearance and therefore need a Material-Component to get displayed. They also can collide with other objects in the game world and therefore have a Collision-Component attached, which describes their physical form. The Bounty
entity has one more component attached to it; the Lifetime-Component. This component states the remaining life-time of a Bounty
object, when it's life-time is elapsed, the bounty will fade away.
So what's next? Having all these different entities with their individual gathering of components does not complete the game. We also need someone who knows how to drive each one of them. I am talking about the systems of course. Systems are great. You can use systems to split up your entire game-logic into much smaller pieces. Each piece dealing with a different aspect of the game. There could or actually should be an Input-System, which is handling all the player input. Or a Render-System that brings all the shapes and color onto screen. A Respawn-System to respawn dead game objects. I guess you got the idea. The following picture shows a complete class-diagram of all the concrete entity, component and system types in BountyHunter.
Figure-03: BountyHunter ECS class-diagram.
Now we got entities, components and system (ECS), but wait, there is more.. events! To let systems and entities communicate with each other, I provided a collection of 38 different events:
GameInitializedEvent
GameRestartedEvent
GameStartedEvent
GamePausedEvent
GameResumedEvent
GameoverEvent
GameQuitEvent
PauseGameEvent
ResumeGameEvent
RestartGameEvent
QuitGameEvent
LeftButtonDownEvent
LeftButtonUpEvent
LeftButtonPressedEvent
RightButtonDownEvent
RightButtonUpEvent
RightButtonPressedEvent
KeyDownEvent
KeyUpEvent
KeyPressedEvent
ToggleFullscreenEvent
EnterFullscreenModeEvent
StashFull
EnterWindowModeEvent
GameObjectCreated
GameObjectDestroyed
PlayerLeft
GameObjectSpawned
GameObjectKilled
CameraCreated
CameraDestroyed
ToggleDebugDrawEvent
WindowMinimizedEvent
WindowRestoredEvent
WindowResizedEvent
PlayerJoined
CollisionBeginEvent
CollisionEndEvent
And there is still more, what else did I need to make BountyHunter
:
- General application framework - SDL2 for getting the player input and setting up the basic application window
- Graphics - I used a custom OpenGL renderer to make rendering into that application window possible
- Math - for solid linear algebra, I used glm
- Collision detection - for collision detection, I used box2d physics
- Finite-State-Machine - used for simple AI and game states
Obviously, I am not going to talk about all these mechanics as they are worth their own post, which I might do at a later point. ;) But, if you are enthusiastic to get to know anyway, I won't stop you and leave you with this link. Looking at all the features I mentioned above, you may realize that they are a good start for your own small game engine. Here are a few more things I got on my todo-list, but actually did not implement just because I wanted to get things done.
- Editor - an editor managing entities, components, systems and more
- Savegame - persist entities and their components into a database using some ORM library (e.g., codesynthesis)
- Replays - recoding events at run-time and replay them at a later point
- GUI - using a GUI framework (e.g., librocket) to build an interactive game-menu
- Resource-Manager - synchronous and asynchronous loading of assets (textures, fonts, models, etc.) through a custom resource manager
- Networking - send events across the network and setup a multiplayer mode
I will leave these todos up to you as a challenge to prove that you are an awesome programmer. ;)
Finally, let me provide you some code, which demonstrates the usage of the my ECS. Remember the Bounty
game entity? Bounties are the small yellow, big red and all in between squares spawning somewhere randomly in the center of the world. The following snippet shows the code of the class declaration of the Bounty
entity.
class Bounty : public GameObject<bounty>
{
private:
TransformComponent* m_ThisTransform;
RigidbodyComponent* m_ThisRigidbody;
CollisionComponent2D* m_ThisCollision;
MaterialComponent* m_ThisMaterial;
LifetimeComponent* m_ThisLifetime;
float m_Value;
public:
Bounty(GameObjectId spawnId);
virtual ~Bounty();
virtual void OnEnable() override;
virtual void OnDisable() override;
inline float GetBounty() const { return this->m_Value; }
void ShuffleBounty();
};
The code is pretty much straight forward. I've created a new game entity by deriving from GameObject<T>
(which is derived from ECS::Entity<T>
), with the class (Bounty
) itself as T
. Now the ECS is aware of that concrete entity type and a unique (static-)type-identifier will be created. We will also get access to the convenient methods AddComponent<U>
, GetComponent<U>
, RemoveComponent<U>
. Besides the components, which I show you in a second, there is another property; the bounty value. I am not sure why I did not put that property into a separate component, for instance a BountyComponent
component, because that would be the right way. Instead, I just put the bounty value property as member into the Bounty
class, shame on me. But hey, this only shows you the great flexibility of this pattern, right? ;) Right, the components ...
Bounty::Bounty(GameObjectId spawnId)
{
Shape shape = ShapeGenerator::CreateShape<quadshape>();
AddComponent<shapecomponent>(shape);
AddComponent<respawncomponent>(BOUNTY_RESPAWNTIME, spawnId, true);
this->m_ThisTransform = GetComponent<transformcomponent>();
this->m_ThisMaterial = AddComponent<materialcomponent>
(MaterialGenerator::CreateMaterial<defaultmaterial>());
this->m_ThisRigidbody = AddComponent<rigidbodycomponent>(0.0f, 0.0f, 0.0f, 0.0f, 0.0001f);
this->m_ThisCollision = AddComponent<collisioncomponent2d>
(shape, this->m_ThisTransform->AsTransform()->GetScale(),
CollisionCategory::Bounty_Category,
CollisionMask::Bounty_Collision);
this->m_ThisLifetime = AddComponent<lifetimecomponent>
(BOUNTY_MIN_LIFETIME, BOUNTY_MAX_LIFETIME);
}
I've used the constructor to attach all the components required by the Bounty
entity. Note that this approach creates a prefabricate of an object and is not flexible, that is, you will always get a Bounty
object with the same components attached to it. Where this is a good enough solution for this game, it might be not in a more complex one. In such a case, you would provide a factory that produces custom tailored entity objects. As you can see in the code above, there are quite a few components attached to the Bounty
entity. We got a ShapeComponent
and MaterialComponent
for the visual appearance. A RigidbodyComponent
and CollisionComponent2D
for physical behavior and collision response. A RespawnComponent
for giving Bounty
the ability to get respawned after death. Last but not least, there is a LifetimeComponent
that will bind the existents of the entity on a certain amount of time. The TransformComponent
is automatically attached to any entity that is derived from GameObject<T>
. That's it. We've just added a new entity to the game.
Now you probably want to see how to make use of all this components. Let me give you two examples. First, the RigidbodyComponent. This component contains information about some physical traits, e.g., friction, density or linear damping. Furthermore, it functions as an adapter class which is used to incorporate the box2d physics into the game. The RigidbodyComponent
is rather important as it is used to synchronize the physics simulated body's transform (owned by box2d
) and the entities TransformComponent
(owned by the game). The PhysicsSystem
is responsible for this synchronization process.
class PhysicsSystem : public ECS::System<physicssystem>, public b2ContactListener
{
public:
PhysicsSystem();
virtual ~PhysicsSystem();
virtual void PreUpdate(float dt) override;
virtual void Update(float dt) override;
virtual void PostUpdate(float dt) override;
virtual void BeginContact(b2Contact* contact) override;
virtual void EndContact(b2Contact* contact) override;
};
void PhysicsSystem::PreUpdate(float dt)
{
for (auto RB = ECS::ECS_Engine->GetComponentManager()->begin<rigidbodycomponent>();
RB != ECS::ECS_Engine->GetComponentManager()->end<rigidbodycomponent>(); ++RB)
{
if ((RB->m_Box2DBody->IsAwake() == true) && (RB->m_Box2DBody->IsActive() == true))
{
TransformComponent* TFC = ECS::ECS_Engine->GetComponentManager()->GetComponent
<transformcomponent>(RB->GetOwner());
const b2Vec2& pos = RB->m_Box2DBody->GetPosition();
const float rot = RB->m_Box2DBody->GetAngle();
TFC->SetTransform(glm::translate(glm::mat4(1.0f),
Position(pos.x, pos.y, 0.0f)) * glm::yawPitchRoll(0.0f, 0.0f, rot) *
glm::scale(TFC->AsTransform()->GetScale()));
}
}
}
From the implementation above, you may have noticed the three different update functions. When systems get updated, first all PreUpdate
methods of all systems are called, then Update
and last the PostUpdate
methods. Since the PhysicsSystem
is called before any other TransformComponent
concerned system, the code above ensures a synchronized transform. Here, you can also see the ComponentIterator
in action. Rather than asking every entity in the world, if it has a RigidbodyComponent
, we ask the ComponentManager
to give us a ComponentIterator
for type RigidbodyComponent
. Having the RigidbodyComponent
, we can easily retrieve the entity's id and ask the ComponentManager
once more to give us the TransformComponent
for that id as well, too easy. Let's check out that second example I've promised. The RespawnComponent is used for entities which are intended to be respawned after they died. This component provides five properties which can be used to configure the entity's respawn behavior. You can decide to automatically respawn an entity when it dies, how much time must pass until it get's respawned and a spawn location and orientation. The actual respawn logic is implemented in the RespawnSystem.
class RespawnSystem : public ECS::System<respawnsystem>, protected ECS::Event::IEventListener
{
private:
Spawns m_Spawns;
RespawnQueue m_RespawnQueue;
void OnGameObjectKilled(const GameObjectKilled* event);
public:
RespawnSystem();
virtual ~RespawnSystem();
virtual void Update(float dt) override;
};
voidRespawnSystem::OnGameObjectKilled(const GameObjectKilled * event)
{
RespawnComponent* entityRespawnComponent =
ECS::ECS_Engine->GetComponentManager()->GetComponent<respawncomponent>(event->m_EntityID);
if(entityRespawnComponent == nullptr ||
(entityRespawnComponent->IsActive() == false) ||
(entityRespawnComponent->m_AutoRespawn == false))
return;
AddToRespawnQeueue(event->m_EntityID, entityRespawnComponent);
}
void RespawnSystem::Update(float dt)
{
foreach(spawnable in this->m_RespawnQueue)
{
spawnable.m_RemainingDeathTime -= dt;
if(spawnable.m_RemainingDeathTime <= 0.0f)
{
DoSpawn(spawnable);
RemoveFromSpawnQueue(spawnable);
}
}
}
The code above is not complete, but grasps the important lines of code. The RespawnSystem
is holding and updating a queue of EntityId
s along with their RespawnComponent
s. New entries are enqueued when the systems receives a GameObjectKilled
event. The system will check if the killed entity has the respawn ability, that is, if there is a RespawnComponent
attached. If true
, then the entity gets enqueued for respawning, else it is ignored. In the RespawnSystem
's update
method, which is called each frame, the system will decrease the initial respawn-time of the queued entitys' RespawnComponents
' (not sure if I got the single quotes right here?). If a respawn-time drops below zero, the entity will be respawned and removed from the respawn queue.
I know this was a quick tour, but I hope I could give you a rough idea of how things work in the ECS world. Before ending this post, I want to share some more of my own experiences with you. Working with my ECS was much of a pleasure. It is so surprisingly easy to add new stuff to the game even third-party libraries. I simply added new components and systems, which would link the new feature into my game. I never got the feeling being at a dead end. Having the entire game logic split up into multiple systems is intuitive and comes for free using an ECS. The code looks much cleaner and becomes more maintainable as all this pointer-spaghetti-dependency-confusion is gone. Event sourcing is very powerful and helpful for inter system/entity/... communication, but it is also a double bleeding edge and can cause you some trouble eventually. I am speaking of event raise conditions. If you have ever worked with Unity's or Unreal Engine's editor, you will be glad to have them. Such editors definitely boost your productivity as you are able to create new ECS objects in much less time than hacking all these line of code by hand. But once you have setup a rich foundation of entity, component, system and event objects, it is almost child's play to plug them together and build something cool out of them. I guess I could go on and talk a while longer about how cool ECS's are, but I will stop here.
Thanks for swinging by and making it this far. :)
Cheers!
History
- 22nd November, 2017: Initial version