native: native to your programming language, ie no serializing / deserializing types
monster = {} // create an entity
monster.hp = 10 // give it properties
monster.pos = [0,0,0]
Using ECS, you instead do the equivalent of this: currID = 1 // ECS init
hpData = {}
posData = {}
id = currID++ // create an entity
hpData[id] = 10 // give it properties
posData[id] = [0,0,0]
Obviously neither version is really implemented that way, but hopefully it shows the key insight - instead of storing properties on the object they describe, you store them in tables full of like data, keyed by the ID of the object they describe.Using ECS is somewhat like treating your game state like a normalized database. It lets you perform operations on all the items of a certain type at once.
Say in my game I need to recenter my origin on a regular basis. In an open world game, you want to make sure that players don't start to see errors caused by floating point arithmetic, and the further away their position gets from 0, the worse this gets. To fix it, one option is to transform all positions by the same amount to keep the player's absolute position near (0, 0, 0).
To do this in the hierarchy style, the code might look like this: (Using JS because I was writing it today so that's where my brain is)
var gameObjects = world.getAllGameObjects();
var offset = [-1000, 0, 500]; // Pretend it's a real vector
// Iterate through every single object in the game
// (Not using foreach or filtering in either example, sue me)
for(var gameObject in gameObjects) {
// Branch on every game object just to see if it's positioned in the world
if(gameObject.position !== null) {
// Do what I really wanted to do
gameObject.position.add(offset);
}
}
Meanwhile, the ECS version would look more like this: // This can vary greatly based on implementation
// I'm going to go with an option that reads easily
// This should just give back the list of components, which we'll say is an array.
// Lookup time in this fictional system is the cost of a dictionary lookup.
var positions = world.getComponents('position');
var offset = [-1000, 0, 500]; // Pretend it's a real vector
// Iterate through only the things we care about
// Not using map here, sue me
for(var position in positions) {
// Do what I really wanted to do
position.add(offset);
}
Not only is the second example shorter, but it's also doing a lot less work. We're adding a vector to an array of vectors. No branching, no touching objects we don't care about, nothing extra.Many ECS systems will allow you to filter down to the set of entities that have two or more components at the same time. Obviously more expensive, but still not as wasteful as checking every object or adding an update function to run every tick on every relevant entity.
The above code was written just before bedtime and not against a real ECS library or OO hierarchy, so please forgive any obvious errors.
If I made a living in games I'd probably try to fill that hole myself, but it's a hobby for me, and one that doesn't get much time allocated.
I was replying to someone who asked about the What.
Incidentally, FWIW I think your post about the why completely misses the point of ECS. Looping over entities with a particular property is easily done with OOP hierarchies or anything else. The benefits of ECS are elsewhere - that you can freely add or remove an entity's properties (without type issues or hot code becoming polymorphic), that the data you frequently iterate over can be stored in friendly contiguous chunks, the usual benefits of composition over inheritance, and so forth.
I meant to say that the why is to treat your data like a normalized database, which provides those properties you list at the bottom.
In fact my example demonstrates at least the following properties:
> (without type issues or hot code becoming polymorphic), that the data you frequently iterate over can be stored in friendly contiguous chunks
My example was not to show that you can loop over every entity. It was to show how easy it is to loop over only what you care about without doing extraneous work, by iterating over a contiguous chunk of exactly what you need to update.
I was going to add an example of creating a regenerating health component and system, but I didn't for a couple of reasons. One, it was already past bedtime. Two, I would have compared it against adding a tick handler and so it would need to be a larger example with more explanation of the way the imagined engine works. The post was already pretty long in the first place so I decided against it.
Your example demonstrated iterating over a filtered list instead of an unfiltered list. The "contiguous chunks of data" benefit of ECS refers to how the underlying data is stored in memory, not to which data you iterate over (and both of those are orthogonal to types/polymorphism).
> Your example demonstrated iterating over a filtered list instead of an unfiltered list. The "contiguous chunks of data" benefit of ECS refers to how the underlying data is stored in memory, not to which data you iterate over
I apologize if this came across unclearly, but that's why I started with a block of comments explaining my fictional example ECS system. I did specifically call out that I was retrieving the backing array for the positions component, implying that components were flat arrays in a dictionary where the key was the component type. I hope we can agree that an array should be expected to be a contiguous chunk of memory when giving a quick example on the internet.
The relevant part of my previous code example:
// This can vary greatly based on implementation
// I'm going to go with an option that reads easily
// This should just give back the list of components, which we'll say is an array.
// Lookup time in this fictional system is the cost of a dictionary lookup.
var positions = world.getComponents('position');
Perhaps I should have been more clear that I meant looking up a pointer to the positions array would cost a dictionary lookup.> (and both of those are orthogonal to types/polymorphism)
I probably should have given an example in a language other than Javascript, since it's not statically typed.
This makes me curious, how bad would it be to actually store game state in say, an in-memory sqlite database? I feel like this could get you some nice things, like auto-destroying all components of an entity via foreign key deletion, being able to merge/join entities with the intersection of multiple components etc. Assuming the clunkiness of interacting with sqlite is abstracted away, I wonder what the real performance differences would be.
My gut feeling is that when crossing the SQL boundary you would lose a lot of the performance you would otherwise be gaining from using an ECS to begin with. Especially when talking to the GPU.
I'd love to be proven wrong if someone has the time though.
And lets be honest, half of my performance problems in webapps are caused by the overhead of sending a query, the database is rarely bottlenecked by inserting or updating the data on the physical storage device if I do not use bulk insert or bulk update methods.
Please try it out though, I would love to see a proof of concept that took the ECS-as-database approach. Like I said earlier in the thread I expect it would not do well, but I'd be interested to see the results either way.
It's a pattern of organizing data and code that consists of:
Entities: Conceptually these are like objects, in memory they might just be an index.
Components: Conceptually these are like fields on objects, they can typically be added and removed unlike normal fields. If entities are being represented as index's, each different type of component is generally stored as some form of Map<Entity, Component>.
Systems: These are where you store your logic, they typically iterate over all entities with a given set of components and do something. For example iterate over all entities with a position and velocity components, and do position += velocity * dt.