Thanks Kyzrati
The last few days have found me waffling a bit around different languages and various configurations, and at one point I found myself back in Python doing some refactoring of the data representation I use in that language. It struck me, that it might be of some interest to those who have completed the Python tutorial, and/or those who have wondered about how data is modeled in an ECS or similar approach. So, here goes.
It is really easy to run into the 'god' object when you are representing your player, mobs, items, and other things in a roguelike game. I don't believe I need to convince anyone that, in anything other than the simplest games, the 'god' object is going to cause you problems. However, let's set aside all those problems, and just look at the data.
The 'god' object just throws all the data into one place, it has 'slots' for every single type of data in the game. The Python RL tutorial introduces you to the concept of composition. Composition is, as far as the data is concerned, grouping related data together into manageably sized chunks. Another concept that you quickly recognize the value of, is assigning a unique number to each entity in your game, makes it easy to decouple entities from each other and use hashtables and such to map from your unique id number to the entity you want. Lets stop here and look at what we have.
Open up your favorite spreadsheet program. Create a blank worksheet. Now imagine that each column in that worksheet is a type of component holding related data. Down the sides (the rows) are your entities. Let's make the first column a special component called EntityID, that holds a unique number. Now every cell won't have data in it for every entity. Some are going to be empty. Lets call empty cells nulls.
Again, looking at the columns in the worksheet, think about how those are your components, your groups of related data. Each column is probably represented in your code by a class, or maybe a struct, doesn't matter which really. We can say that we have (or should have) a type corresponding to each column. Examining the data requirements, we can pretty quickly realize that we shouldn't have (and really don't need) more than one column of the same type per entity. Even if we did, or maybe if we need more than one instance of a particular data item inside a component, we'd just use an array or some other collection for that item.
For anyone who has any database experience, there's probably a light dawning. Being able to represent our data model in a flat spreadsheet means that really we're just looking at one big table full of records in a relaxed first normal form. Relaxed because we have nulls (empty cells). You could, if you wanted, organize your data into a higher normal form, and you'd start to see something similar to a class hierarchy form. But it gets complex rather quickly, we soon need special columns to hold the ids of things in other tables, and so on.
The OOP approach (not using composition) could also be done, but runs into the same problems or worse, depending on what language you use and how you structure things. So lets go back to that simple spreadsheet model, keep things simple.
At this point, we have entities and we have components. Some people more or less stop here, maybe even back up a step and create a small hierarchy of classes to hold mostly non-overlapping collections of components. Well, we've been talking about the data, but we've been leaving out how we manipulate it. Looking at how we manipulate the data, even without getting into details, we can see we've got a couple problems.
The two biggest problems you run into at this point both boil down to the same thing, while we've expressed the primary relationships of our data, we haven't addressed the secondary ones. Consider, with our behavior in our components, or maybe still some in the entity even, we run into problems where one component needs to talk to another. This is actually harder to deal with than talking to a component on another entity. Mainly because we either have to be able to go from component -> entity -> component without getting tied up in circular references or storing a copy of the parent entity's id everywhere.
I'm not going to tell you you can't keep your behavior in your components, maybe turning your entities into switchboards or mailmen, but I am going to tell you it will likely create non-trivial problems. It gets worse when you find yourself wanting to modify the component mix on your entities at run time. Throw events into that, and we can quickly get ourselves into a snarl.
So what's the solution? Realize that many of the problems you are running into with methods from one component calling into another (or grabbing data from properties on another), would be solved completely if you had access to everything you need at the start of the method. One way to do this is to turn your entity into a switchboard, but.. there's another approach.
Systems. Horrible name really. Better would be Processes. Take the troublesome behavior code out of components and put them in a separate function, even a static function (or free function) completely outside your entity/component model. Reorganize your program flow so that you are passing the necessary data into each process in turn. Yes you're violating the heck out of a bunch of theoretical OOP constraints at this point. But, many have found this to be a viable solution.
Now, not trying to be a salesman, just an explainer (and probably a bad one), however i am going to say "But wait! There's more!"
Once you've moved the code outside of your components, you can start to work on organizing your program's flow so that you reduce the need for much of the event wiring. Realize that the existence or non-existence of a component type on an entity can change program flow. It quickly boils down to individual use cases so explaining it is difficult for me. An example I read elsewhere that made sense, was consider a healthy mob. There's no reason for it to have a health component. It isn't wounded, so its automatically at full health. Since our natural healing process only runs on entities which have a health component, it'll automatically skip the ones at full health. It'd be pointless to process them anyhow, but without a health component, its an automatic de-selection.
What do you do when the mob gets wounded? Simply add a health component to the entity that represents the mob. You can apply the same general ideas to command flow, to effect calculation, and so on. It gives a nice, well defined, clear step by step route through your code. That is the Systems part of ECS.
Wrapping this up, I hope the above explanation helped someone understand the concepts better. It isn't the only way of course, but I think it is worth taking a look at depending upon your game's needs.
Have a good one
Brian aka Omnivore