1
\$\begingroup\$

Is often read that in entity component system pattern we should treat components just as a passive data structure with no logic at all, this way we follow to a data oriented design approach with efficient usage of cache memory and gain performance.

I was developing a simple game using this rule and when the game was getting bigger I start facing some serious code smells such as repeated code in systems, let's suppose its a shooter to illustrate an example. In this shooter I had bullets being fired so I created a BulletComponent and a BulletSystem that handles when it collides and make some damage and if don't collide with anything the bullet dissapear in five seconds. Then I wanted some explosives so I created the ExplosiveComponent and ExplosionSystem, once the explosive is placed it explodes in ten seconds. At this point we can notice the first repeated code between systems then we can make an abstraction and we create a DurationComponent and a DurationSystem so the bullet and the explosive contains it with the time it should last.Then I realized that when the explosive should explode I want to create some particle effect in the area so this abstraction is no longer valid as I want to perform differents actions when time runs out, but if I do as at the beginning I have the repeated code between systems (ugh!). This example is simple but I'm facing this with more complex things like AI, or reacting to collisions.

One solution I came to this is that I really wanted an ActionComponent with a ActionSystem that executes the action contained in the component but this way I'm breaking that components only contains data and they should not have logic! this is more an 'hybrid' approach. But if you start thinking deeply the ActionSystem is very generic and you can almost do anything with it so it's hard to determine whether a system is necessary or not. The same can be applied to collisions, instead of having lots of systems with logic for different entities colliding, you can just have a CollisionHandlerComponent with the logic on it for each entity.

I found the following advantages making generic systems like this:

  • Higher flexibility (you can customize each entity easily)
  • No repeated code
  • Less systems (more performance?)

But as I said the main disadvantage is that is really confusing if a system is needed as you can stick logic to components.

Is it wrong to follow this sort of hybrid ecs? If yes, what would be your approach to the example without repeating code in systems?

\$\endgroup\$
  • \$\begingroup\$ This God Object smells way worse than your original design problem: "...you start thinking deeply the ActionSystem is very generic and you can almost do anything with it..." \$\endgroup\$ – Patrick Hughes Jan 12 at 22:05
  • \$\begingroup\$ Also smells to me but not as having a lot of repeated code and lots of systems for having custom behaviour. I named it ActionComponent but it could be something like ScriptComponent that executes the contained script every tick. I think Unity uses some approach like this for I've seen but I never used it before. Also what concerned me about having a lot of systems is that every time a component is added or removed families of entities of all systems should be updated, this add overhead to these operations cause it has to iterate and update list of entities of systems that doesn't even change \$\endgroup\$ – Alejandro Jan 12 at 23:19
4
\$\begingroup\$

I was considering voting to close this question as opinion-based, but I think there are a couple of misunderstandings about Entity-Component Systems and Data-Oriented Design here that are worth addressing.

  1. ECS is just a tool

    Like any programming pattern or principle, it exists to help you write good software. It is not an unbreakable law of physics or a straight jacket meant to inhibit you from doing your work.

    If you find that for your particular needs, adding logic to your component makes your game perform better or more reliably, or helps you write clearer or more maintainable code, or helps you improve your development and iteration speed, then that might very well be a worthwhile exception to make to the rule.

    There's no absolute standard to which every component system must adhere, so make yours work for you. Games get no points for compliance to X or Y's definition of what its code "should" do, only for shipping and offering a great play experience.

    (This is the reason I normally consider "am I allowed to/do I have to do X in an ECS" questions to be opinion-based)

  2. Not everything has to be a component or system

    We wouldn't search for every instance of + in our codebase and replace it with an AdditionComponent processed by an AdditionSystem — that would be just silly.

    A duration isn't much more complicated than that. You're checking a time against a threshold, something you can do in one line. So you don't gain any particular utility, clarity, or simplicity/reduction in code by packing this up into a DurationComponent

    If you really want, you could make a simple Expiration struct that's just a wrapper around a timestamp, computing an expiration date duration seconds in the future upon construction, and exposing an IsExpired predicate to check the stored timestamp against the current time. This packages up this small amount of time-related code for reuse, without making it a whole new component & system.

    Then your bullet component, explosive component, etc can store an Expiration as part of their data, and their respective system updates can check the expiration and do the appropriate thing for bullets or explosives respectively.

    Far from unnecessarily duplicating code, you can gain advantages from doing it this way — for instance, your bullet system can recycle expired bullets as it runs through its batch, saving any cost of a separate removal by doing it as part of the iteration it's already doing anyway.

  3. Fewer systems does not necessarily mean better performance

    The major factor in how data-oriented ECS systems aim to boost performance is cache.

    When you have a system that does one consistent set of operations repeatedly on a contiguous array of similar data, the code you need stays hot in the instruction cache, and the data access patterns are predictable so pre-fetching keeps your data cache full, so your processor can hum along at full speed.

    As soon as you throw in indirection, like firing some arbitrary "On Expiration" or generic "Action" callback/delegate/function pointer that might point to something different for every item in the batch, that goes out the window, and you risk cache misses costing hundreds of processor cycles for each hop.

    So no, reducing the amount of unique code won't necessarily make it perform better, and it can in fact make it slower if you're counting on data-oriented batching to push your cache to its limits.

    It's also not necessarily good for development iteration. Although ostensibly you have less code to write and keep track of, that code ends up being a lot more generic and ambiguous about what it's doing than a group of systems each focused on doing their one job. The latter gives you much more predictable code that's easy to follow and debug than chasing data triggers all over memory.

So, the upshot is: you can adapt your version of a component system to work however you want. Just be sure you understand the reasons you're making the choices you're making, and the reasons the recommendations that caution against them exist, so you can weigh those considerations and make the call that's right for your game's needs.

\$\endgroup\$
  • \$\begingroup\$ 2. It is arguable that an addition is just as an abstract that entities expiring. For my particular case I do have a timestamp that compares to actual time and when the entity expires is scheduled for removal, but its just in one place. It could be wrapped as you said but the feel of repeated code still there between systems and components, I mean the concept of in X time schedule removal, or even in X time do Y. I know this example was simple, but it also happens with more complex things like AI. \$\endgroup\$ – Alejandro Jan 13 at 0:42
  • \$\begingroup\$ In AI you have a lot of specific behaviour for individuals entities, and sometimes maybe just an AI for an unique entity. -If you have multiples components & systems for differents AI types, you have lot of repeated stuff in the systems. -If you have just one component specifing the AI type, you end with one huge unmaintainable system handling all AI cases. -If you do the polymorfic approach leting the AIComponent handle the specific behaviour of the AI the code got cleaner. So this make me think that pure data approach its not viable in all cases. \$\endgroup\$ – Alejandro Jan 13 at 0:52
  • \$\begingroup\$ 3. I know the amount of unique code won't necessarily make it perform better, I did not express myself enough. This is really dependant of the implementation of the ECS, but in my case when a component is added or removed of an entity it iterates all systems to check if the mask of the components on it matches the system, so it add it or remove from its list. This is pretty fast for a few systems, but its not the same if you have 10 systems than if you have 500 systems and maybe even some systems just hold logic for one particular entity. This don't happen with generic systems. \$\endgroup\$ – Alejandro Jan 13 at 1:10
  • \$\begingroup\$ That sounds like an implementation bug. You can do that iteration (lazily if you choose) once for each component mask and save the results, so your cost to add a component is proportionate to the number of systems that need it, not the number of systems you have. But you really don't need that many systems because 2: not everything has to be a system. Specific to your example, AI don't all need to run through one monolithic system, nor have unique bespoke systems each. A behaviour tree for the logic leveraging a small kit of shared movement/pathfinding/action components can often work well. \$\endgroup\$ – DMGregory Jan 13 at 2:00

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.