The Melodist»Blog

My Game's Entity System vs. OOP (3/9/2018)

About a year ago, I wrote a blog post in which I discussed my change of heart regarding the way I wanted to program, and why object-oriented programming wasn't consistent with that desire in that it over-complicated problems and abstracted away what the computer was doing. Despite this change in heart, I still had the tendency to think of programming problems in terms of objects as opposed to thinking about the what the computer must do in order to solve programming problems; this is, of course, due to the fact that I had been thinking in an object-oriented fashion for such a long time.

This is embodied most by my game's entity system. Initially, when this project was first introduced to Handmade Network, I was using standard C++ polymorphism to store different types of entities. After I had moved towards writing almost in pure C (it was still compiled as C++, but I was only taking advantage of a few semantic C++ features, like function overloading), I was still doing polymorphism, but I was instead doing it manually (as opposed to using C++ language features to do it, which consists of employing virtual functions, inheritance, etc.). The word "entity", at this point, meant effectively anything you could see on the screen that needed to be stored in a map's file on disk. The player was an entity, the tile maps were entities, the trees were entities, and the particle generators were entities. It isn't too difficult to point out the fact that the word "entity" was nebulous enough that it meant almost nothing. My reasoning for treating all of these different in-game objects as the same type was that they had commonalities. I might've said something like: "They all need a position and they all need to be rendered; obviously they need to be stored as the same type!"

There was distinction between structures that a map consisted of, however; I also had "collision bodies" which were more specific than entities. They specifically referred to static physics objects that were arbitrary convex polygons; they were used to make the player run into (and land on) things. One might ask the following: "If the word 'entity' has a definition that has become so broad, why aren't collision bodies considered entities?" I asked the same question, and it made me feel as if my code was "unclean" or "poorly structured".

Over the months, I'd have trouble wanting to prototype, say, a new kind of object in the world, because the growing pains of my polymorphic entity system were becoming increasingly apparent. To add a new kind of entity, I'd have to make a new struct, add an enumeration value, potentially write a set of switch cases in several functions, ensure that the entity's data would be saved and loaded properly from disk in my map loading and saving functions, and add functionality in the map editor to ensure that when the new type of entity was added, it'd call the right entity initialization function.

...Wow. That's a hell of a lot to do to prototype a game mechanic, and it definitely showed in my failure to produce many interesting game mechanics in an entire year. It has to be said, though, that heavily object-oriented languages make this sort of polymorphic system really easy to implement.

In C++, instead of just having a million switch statements, you'd start with an Entity class. Entity would have bunch of pure virtual functions. Then, you'd have a bunch of derived classes for each type of these entities; in each of these derived classes you'd write to all of the pure virtual functions and then, whenever that virtual function is called, at run-time the program will just look up the right function to call, and then you never have to touch those pieces of code in which those virtual functions is called! Every single type is encapsulated nicely into a little box, which makes for a really clean and elegant solution...

...right?

Well, no... That's all kinds of wrong. See, what implementing a polymorphic system in pure C has taught me is that a system like this is doing lots and lots of unnecessary work: When I'm searching through all of the entities to find the player entity, why am I even checking tile maps if they're a player? Why am I checking static objects if they're a player? When I'm trying to render all of the entities, why do I have to check the type of each entity I'm trying to render so I can call the right function?

This demonstrates an important point. When heavily object-oriented languages hide away that type checking and type-based function call differentiation, that work isn't going away, it's simply being done for the programmer (as opposed to being done by the programmer). It's still happening, and it's still as much a part of the program as it would be if it were implemented manually, it's just no longer under the programmer's control: It has been moved away from the eyes of the programmer. So, in terms of what's actually running on the hardware when one runs the program's executable, the problem isn't actually clean or elegant, it simply provides the illusion of being clean and elegant. The superficial layer of the program, the code, appears clean and elegant, but the literal instructions that the CPU is following to get something done are not, at all, "clean" or "elegant".

The job of a programmer is to transform data in a useful, effective, and efficient manner... right? That task is all done with processors. If what the processor is doing when it runs a program is not very robust, effective, and efficient... is the program well-programmed? The answer should be obvious: No.

This thinking led me back to my entity system. Why am I having to differentiate the functions I call based on the specific type of an entity? Heck, why am I even storing this data together? Why is the player right next to the tile-maps in memory which are right next to the particle generators and static objects in memory when they are, in fact, completely different? Even if the data were similar, they're handled completely differently!

The answer that I've come to regarding those questions is the following: Differentiation is okay. It's necessary. It's useful. Overgeneralization, on the other hand, is none of those. Generalizing something that can be made general can definitely be useful, but the programmer shouldn't stretch to generalize something; if something specific has to happen for specific data, why would anything else need to be written?

It's with this mindset that I've begun to narrow definitions within my program. "Entity" now means something far more specific: It is a sprite with physics with movement (good for the player, the old man in the game, other living things, etc.). I can still differentiate a bit depending on the particular type of entity, but now, that differentiation is far less wide-spread. It is also not polymorphic; if some kind of in-game object needs data that isn't supported by what the entity structure provides, then it's not an entity, but something else. Following from this, I've separated static objects to be something entirely separate from anything else. I've done the same with particle generators, collectibles, and water (among other in-game objects).

All of these in-game objects are now neat, fixed-size packages of data, and with this arrangement of data, there are already massive benefits. Now, all entities can be stored together as one big block of memory, and when I update entities, I can sweep through them all and update each one. I don't need to check what type they are, because now I can make more assumptions: An entity must meet this specific definition and must be updated in this way, so given a big block of entities, I'll sweep through them and update them in the best way possible. The same methodology applies to, say, static objects or collectibles. Additionally, to save all of the entities, I can just grab that big block of data and write it to a file- no type-differentiation is necessary! There are bound to be more benefits, I'm sure, and I'm bound to dig them up as I continue to work with this system, but these are the initially obvious ones.

So, the new entity system in my game contains a series of distinct sets of objects; each object in a given set is extremely similar to another object of the same set. There can be a bit of differentiation within these sets, but the definition of an object in each of these sets is far more narrow than my old definition of "entity". I'll be writing code in this system to be specialized, not general... and that's perfectly okay.

Thanks for reading, everyone!
-Delix
Allen Webster, Edited by Allen Webster on
Awesome read! I totally empathize with that feeling where OOP is all you know and you can't help but approach using that mindset, even when you're trying to do C. I had a similar blog post about simulating runtime polymorphism with switches and then realizing that writing C++ style structures with C stye code was not that much better than C++ style code. Interestingly we found different ways to resolve the problem. I have that blog post archived somewhere, but it's not on the internet anymore :/
Jeremiah Goerdt,
This is good stuff. I'm still struggling to shake my OOP roots from Java in college.

I'm curious about what you do when adding a new type of object that doesn't fit one of your current object types. Have you done anything to allow a 'linear' amount of effort when adding new types, or is it still a large amount of work if the new object interacts with all the other object types in your game?
Oliver Marsh,
Great read, thanks for sharing!
Simon Anciaux,
I was looking for the previous blog post you mentioned. I think this is the link but it's not available anymore. Did you remove it or is it a problem with the site ?
Ryan Fleury,
Thanks, Mr4thDimension! I'm glad you enjoyed the read. I've come to the same realization as you did: What matters is what the hardware is doing, so a C++ polymorphic structure will be almost equally as bad as a similar system implemented in plain C (it will also be more cumbersome to write and maintain). As I mentioned in the post, just because languages give one the ability to do something horrible with great ease doesn't mean that thing isn't horrible. :P

I'm actually really curious to see how you solved the problem. If you find that old blog post of yours, definitely let me know!
Ryan Fleury,
CaptainKraft,

Honestly I'm not sure what will happen in that situation currently, as I'm still spending some time rewriting the system and have not yet experimented with prototyping. It will probably require a similar amount of work as just adding a new entity in the previous system, but it's possible that there is far less to reason about. In my previous system, I had several "generic" entity functions, like
1
void add_entity_physics_objects()
and
1
void request_entity_resources()
I won't really have to reason about these for all entities anymore; some objects won't require physics or for additional resources to be requested, or they might not need to be updated. This might be similarly cumbersome as the previous system, but I'm going to continue to attempt to think about and refactor it to attempt to make prototyping the game and experimenting with new game mechanics as easy as possible.

I'm glad you enjoyed the post!
Ryan Fleury,
mrmixer
I was looking for the previous blog post you mentioned. I think this is the link but it's not available anymore. Did you remove it or is it a problem with the site ?


I think it's a problem with the site... I've now noticed that several of my old blog posts are missing (and I didn't delete any).
Jeroen van Rijn,
I'll look into this tomorrow, Ryan.
Ryan Fleury,
Kelimion
I'll look into this tomorrow, Ryan.


Great, thanks Jeroen! I appreciate it.
Jeroen van Rijn,
Found it. I'll be restoring it (along with some more blog posts) sometime later this week after I've dug into the reason for them disappearing in the first place.
Simon Anciaux,
Any news on restoring those articles ?
Ryan Fleury,
mrmixer
Any news on restoring those articles ?


That's weird... For some reason I remember them being restored but now they're not.

(It's also possible that they were never restored and I'm misremembering)