Posts
Search
Contact
Cookies
About
RSS

Using RayLib with an ECS

Added 18 Mar 2020, 1:08 p.m. edited 18 Jun 2023, 1:12 a.m.

While it’s very common to have arrays or some kind of list of entities in a game, this isn’t the only way to organize your game data.

In order to illuminate some of the techniques involved, I’ve knocked together a basic asteroids type game. It will be useful to refer to this code while you read this post, you can grab it here. Lets look at a typical data structure that you might build if you’re using more a traditional approach.

 typedef struct {
  float x, y;
  float dx, dy;
  int size;
 } Roid; 

This has three distinct pieces of information, a position, a velocity and a size, its no stretch to see that these pieces of information could be used by many different entity types. If you split this data into individual components, you have a position, velocity and size component.

Having these simple components you can build entities made from different component profiles, for example a player entity might have a position and velocity component, where as an explosion might have a position and animation component, but no velocity.

So there is two of the letters of ECS, our Entities have Components, all that’s left is the idea of Systems.

A system is a process that is applied to a (usually) filtered group of entities.

For example in the ’roid sample code there is a render system (that draws entities that have a sprite component) and an update system which updates almost every entity (moving entities and checking for collisions and so forth).

While ideally systems should only act on and know about individual components, it can be useful to group entities into types, I’ve implemented this with a CType component, with the following types ’roid, ship, missile, and explosion. These types are used where checks and behaviours are specific to just that type, by and large though you should be better enacting many of your rules at the component level.

In a situation where you have position and velocity components it makes sense to apply the velocity component in one place in your code, rather than doing it in different places as part of each entity type update.

Having established the idea of types (which are not really anything to do with an ECS) it also allows for the organisation of code in the same manner, when coding the update system, you can either have all your update code all in one function, or you can have just component specific code in it and a switchboard to the various type update functions.

Taking this approach, because a ’roid has a position, velocity, and sprite component it is automatically a moving visual entity, in future you might create an alien saucer entity with these components adding maybe only one or two components specific to the new entity you are creating, you have all the basic properties and behaviours ready to use. The actual update function of the ’roid for example, is only left with maintaining a running count of ’roids and a collision check with the players ship, everything else is taken care of by the ECS.

Even if a new entity type is under a fairly rigid movement algorithm I'd still recommend keeping a velocity component and controlling that to get the behaviour you want, controlling an entity this way often leads to richer more believable movement.

So you can see once you have a pallet of components to choose from, then creating new entity types is greatly simplified. Is an ECS a bit over kill for a simple implementation of ’roids, possibly, is it something I’d use if I were writing a complex RPG, with detailed characters and spell effects, well then definitely!

Having covered an overview of an ECS and how I’ve used it in the example, let’s have a closer look at a few points in detail.

I’ve found it to be good practice when creating a particular type of entity to do it in just one place, looking at the creation of the examples entities...

 void makeExplosion(Ecs* ecs, float x, float y)  
 {
  CEntType tp;
  tp.type = ENT_EXPLOSION;
  CAnim an;
  an.frame = 0;
  CTransform t = (CTransform) {x, y, 0};
  CSprite sprite;
  sprite.texture = expTex[0];
  sprite.origin = (Vector2){128, 128};
  EcsEnt ee = ecs_ent_make(ecs);
  ecs_ent_add_component(ecs, ee, COMPONENT_TYPE, &tp);
  ecs_ent_add_component(ecs, ee, COMPONENT_TRANSFORM, &t);
  ecs_ent_add_component(ecs, ee, COMPONENT_ANIMATION, &an);
  ecs_ent_add_component(ecs, ee, COMPONENT_SPRITE, &sprite);
 } 

All the function knows about is a reference to the ECS system and the required coordinate for the explosion. Because you need to supply it with so little information, its much more isolated and very much more likely to be usable in all sorts of ways and places you never thought of…

Each component is created as a local variable and filled in with their default values, then all that’s needed is a newly created entity that can be fleshed out with these components. As each component is assigned to an entity, the local data is copied into the ECS system.

Looking closely at some of the component data structures you can see some regular structures.

 typedef struct {
  Texture2D texture;
  Vector2 origin;
 } CSprite; 

However if you have a component that is simple enough and to make it more convenient to work alongside RayLib you can use what in effect an alias.

typedef Vector3 CTransform;

So a CTransform component is really just a RayLib Vector3 (I’m using the Z axis for rotation), as you expect once you retrieve this component from the ECS, you can reference its x, y and z members as normal.

Generally rather than having lots of little components, if you do spot a bunch of them that are usually used together then you can roll into one slightly larger component, if only for the sake of convenience. Equally you don't want a bunch of unused values in a component, depending on which type of entity you use it with.

Speaking of convenience as there is only ever one player ship at a time and it is a special case, it hasn’t been separated out into its own code unit. This could easily get out of hand and in any more complex a situation it would probably make sense to separate it out. That said in this case, the idea of a ship is central to how the main game logic works.

You now have some code you can take further is you like which is how some people like to learn (its crying out for the occasional alien saucer!) or alternatively you can use it as a reference as you start to put your own project together.

Enjoy!