I'm going to speak from a little bit of experience, going from a rigid OO-design to an Entity-Component-System (ECS) design.
A while back I was just like you, I had a bunch of different types of things that had similar properties and I built out various objects and tried to use inheritance to solve it. A very smart person told me don't do that, and instead, use Entity-Component-System.
Now, ECS is a big concept, and it's tough to get right. There's a lot of work that goes into it, properly building entities, components, and systems. Before we can do that, though, we need to define the terms.
- Entity: this is the thing, the player, animal, NPC, whatever. It's a thing that needs components attached to it.
- Component: this is the attribute or property, such as a "Name" or "Age", or "Parents", in your case.
- System: this is the logic behind a component, or a behaviour. Typically, you build one system per component, but that's not always possible. Additionally, sometimes systems need to influence other systems.
So, here's where I would go with this:
First and foremost, create an ID
for your characters. An int
, Guid
, whatever you like. This is the "Entity".
Second, start thinking about the different behaviours you have going on. Things like the "Family Tree" — that's a behaviour. Instead of modeling that as attributes on the entity, build a system that holds all that information. The system can then decide what to do with it.
Likewise, we want to build a system for "Is the character alive or dead?" This is one of the most important systems in your design, because it influences all the others. Some systems can delete the "dead" characters (such as the "sprite" system), other systems can internally re-arrange things to better support the new status.
You'll build out a "Sprite" or "Drawing" or "Rendering" system, for example. This system will have the responsibility of determining what sprite the character needs to be displayed with, and how to display it. Then, when a character dies, remove them.
Additionally, an "AI" system that can tell a character what to do, where to go, etc. This should interact with many of the other systems, and make decisions based on them. Again, dead characters can probably be removed from this system, since they're not really doing anything anymore.
Your "Name" system and "Family Tree" system should probably keep the character (alive or dead) in memory. This system needs to recall that info, regardless of what the state of the character is. (Jim is still Jim, even after we bury him.)
This also gives you the benefit of changing when a system reacts more efficiently: the system has it's own timer. Some systems need to fire rapidly, some don't. This is where we start getting into what makes a game run efficiently. We don't need to recalculate the weather every millisecond, we can probably do that every 5 or so.
It also gives you more creative leverage: you can build a "Pathfinder" system that can handle the calculation of a path from A-to-B, and can update as-necessary, allowing the Movement system to say "where do I need to go next?" We can now fully separate these concerns, and reason about them more effectively. Movement doesn't need to find the path, it just needs to get you there.
You'll want to expose some parts of a system to the outside. In your Pathfinder
system you'll probably want a Vector2 NextPosition(int entity)
. This way, you can keep those elements in tightly-controlled arrays or lists. You can use smaller, struct
types, which can help you keep components in smaller, contiguous memory blocks, which can make system updates much faster. (Especially if external influences to a system are minimal, now it only needs to care about it's internal state, such as Name
.)
But, and I cannot stress this enough, now an Entity
is just an ID
, including tiles, objects, etc. If an entity doesn't belong to a system, then the system won't track it. This means we can create our "Tree" objects, store them in the Sprite
and Movement
systems (the trees won't move, but they have a "Position" component), and keep them out of the other systems. We no longer need a special list for trees, since rendering a tree is no different than a character, aside from paperdolling. (Which the Sprite
system can control, or the Paperdoll
system can control.) Now our NextPosition
can be slightly rewritten: Vector2? NextPosition(int entity)
, and it can return a null
position for entities it doesn't care about. We also apply this to our NameSystem.GetName(int entity)
, it returns null
for trees and rocks.
I'll draw this to a close, but the idea here is to give you some background on ECS, and how you can really leverage it to give you a better design on your game. You can increase performance, decouple unrelated elements, and keep things in a more organized fashion. (This also pairs well with functional languages / setups, like F# and LINQ, which I highly recommend checking out F# if you haven't already, it pairs very well with C# when you use them in conjunction.)
GameObjects
. In CK2, the maximum amount of characters I saw at the same time was when I looked at my court and saw 10-15 people there (I didn't play very far though). Just as well, an army with 3000 soldiers is only oneGameObject
, displaying the number "3000". \$\endgroup\$ – Raphael Schmitz Oct 11 '18 at 10:25