0
\$\begingroup\$

In Java, there are interfaces - I'm not clear on the details (I don't use Java) but from what I've learned, they seem to be "classes for classes": a way to effectively make individual classes "instances" of one overarching class. For example, a "Cup" interface could be implemented by a "Mug" class, a "Glass" class, a "Goblet" class etc. As long as they have the same I/O methods such as fill(), empty() or drink(), they can be used interchangeably.

In languages with duck typing, interfaces are not needed - the language allows for classes to be used interchangeably as long as no invalid properties of methods are referenced. This is another valid was of going about things in OOP. This is how I usually go about things.

My question: how can I create varied behaviour in an ECS if components are data-only? I like to think in terms of first-class functions, but that seems to be taboo since components are not supposed to hold behaviour.

  1. Have a massive switch or if/else chain in my System:

    For example, if I have an AI component with several different behaviours, I could use an integer to define which type of AI to use for that particular entity. It seems a bit convoluted and inelegant.

  2. Have a separate Component and matching System for each behaviour:

    This would be neater in each individual System but as a whole, the program would be a mess. This wouldn't work because of the large amounts of variation such as hundreds of different guns in a shooter game.

  3. Use a flyweight-like data structure independent of the Component:

    This would mean storing all of the different behaviours elsewhere, and storing a key that a System can use to access the behaviour. This is basically a fancy version of the switch option above, but it seems neater as there wouldn't be a massive dump of code in the System.

  4. Creating a complex way of encoding a description of the desired behaviour which the System can interpret:

    This seems to be the most overkill of the lot. Unless I have to, or there are significant advantages, I'm trying to stay away from this.

If there's no other way, I'll just adopt the methodology of first-class functions as I usually do and just ignore any guidelines about ECS I've heard, just for this one case. The problem is, it probably won't just be one case in the future.

Have I missed any other good options? Is there an industry standard or recommendation? If not, what considerations should I make about which option to choose?

\$\endgroup\$
5
  • \$\begingroup\$ Can you describe in more detail what kind of behaviour variants you need? For something like the guns example, we'll often vary them almost entirely through data on the component (fire rate, damage, clip size, recoil pattern, etc...) while for AI we might use a different system entirely like a behaviour tree to assemble complex behaviours out of a kit of common parts. The more you can tell us about your specific needs, the better we can help you meet them. \$\endgroup\$
    – DMGregory
    Apr 28 at 10:43
  • \$\begingroup\$ @DMGregory For my example of guns, I was thinking a bullet-hell type gun, not an FPS type gun. Each one would have a specific firing pattern, trigger different effects, have different stats, etc. For a FPS type gun, data is absolutely enough. Also, I'm implementing some AI into my enemies and I would like distinct behaviours for "active", "passive" and different states. However, I wanted to leave the question generic since Stack Exchange is supposed to be an easily searchable library of answers. \$\endgroup\$ Apr 28 at 11:14
  • 1
    \$\begingroup\$ Make it too generic and we might not be able to give you good answers. You won't be the last person to want to make a bullet hell, so asking for how to incorporate different bullet hell patterns into an ECS is still a question that can be useful to others in the future. And I think it's one with more focused answers: bullet hell weapons are quite similar to particle systems, and particle systems are probably the place we've been doing data-oriented design / ECS style implementations in games the longest! 😁 \$\endgroup\$
    – DMGregory
    Apr 28 at 11:22
  • \$\begingroup\$ A vary orthodox interpretation of ECS would lead you to have different components for different behavior, because different components means different systems will run. You could cut the middle man and have functions. Thus, I hereby grant you a non-fungible meaningless imaginary token you can redeem to have functions in components. It is not the traditional way, but don't let that stop you. However, I would worry about parallelism when those functions want to modify components. Thus, I'd have them pure, have a system just to call them passing what it needs, and the system deal with the update. \$\endgroup\$
    – Theraot
    Apr 30 at 0:08
  • \$\begingroup\$ This is what I[m using right now, maybe I'll consider keeping it. \$\endgroup\$ Apr 30 at 0:34
2
\$\begingroup\$

ECS is effectively a database, and an interface is effectively a parent class, so a good portion of the answer may be borrowed from current DB design practices. ECS-specific and non-ECS solutions also exist.

Component per hierarchy.

Every possible sub-type is merged into a single component. That's right, all the fields of all the would-be sub-types are present in a single component.

Discriminator field used to identify an underlying sub-type. Good to best cache performance except in degenerate cases (e.g. too many sub-types). Need to know all the sub-types in advance, thus making run-time extensibility problematic. Entity is still limited to one component per (concrete) component type, by design.

Discriminator is a field used to distinguish concrete sub-types. Systems may switch on it or use a dictionary that maps discriminator to a component's "driver" (the strategy pattern).

Component per concrete sub-type

Each sub-type is implemented as a distinct component, with all inherited fields repeated.

Allows using language's built-in inheritance (including interfaces). Discriminator field is still needed to distinguish the components when accessing via a polymorphic reference. Discriminator field must also be included into a reference to a component, so you can know where to look when following a reference. Cache performance is not ideal, but has a theoretically possible opportunity for better parallelization. Limiting entity to one component per component type requires extra care.

Extension component per sub-type.

Each sub-type is a component that includes only it's own fields but requires a base component to be present in the entity. That is, the entire component is assembled from multiple pieces, one piece per level in the hierarchy.

Discriminator in the base class is sufficient. Cache performance is even worse, but has a theoretically possible opportunity for even better parallelization. Instantiation and destruction involves multiple components. Working with entire instance of a concrete type is inconvenient.

Varying-size components.

Using the normal inheritance, but custom allocator is employed to pack differently-sized components into a contiguous chunk of memory.

Discriminator in the base class is sufficient. In the other respects it retains all the features of mundane ECS.

Pure behavior.

If all the different behaviors work with the same data (or require no data at all), using just a strategy must be enough. Multiple strategies per component are possible.

No overhead besides the aforementioned limitations.

Feature flags.

Another option in absence of inheritance is to use feature flags. It is sometimes achieved emergently by having the special cases for the properties (e.g. zero effect radius means no effect at all).

Event-driven.

Behaviors may be associated with a component type. In other words, a reference to a behavior implementation is stored per type rather than per instance.

Invocation of a behavior on an entity is not directed towards a concrete type or interface, but rather to any component that supports given behavior. Which means that an entity can have any number of behavior handlers (zero, one, few), depending on the components attached.

In many aspects, this is similar to raising an event on an entity.

Message-driven.

Similar to an event-driven approach, but the events (messages) are stored to a queue to be read later by a system.

Due to delayed handling, message sending must be async, which makes it unfit for certain tasks (e.g. asking for answer immediately). On the other hand, the messages may be processed in bulk and in parallel, which is good for the performance.

Out-of-ECS polymorphism.

Component may include a reference to a mundane object that supports polymorphism by the means of the language.

Depending on the usage, this may seriously disrupt the cache performance, so it may be a good idea to update the "outsourced" element outside of ECS as well.

It's normal for ECS-oriented system to have non-ECS modules. Examples may include AI (behavior trees, pathfinding), GPU physics, scene composition, input methods, scripts, etc.

\$\endgroup\$
6
  • \$\begingroup\$ Great explanation. In my use case, the components all have different but related behaviours. This means I effectively can't use inheritance since they share nothing. As I described, it's a more interface-type thing, which requires a new class. For every single new case. I think my best bet right now is to just store all the behaviours off-site and a key to access the particular one stored in the component. Not the greatest for performance, but it definitely is the cleanest option right now. \$\endgroup\$ Apr 28 at 23:33
  • \$\begingroup\$ I'll accept this answer if no one posts a better one, but I'll give it some time. \$\endgroup\$ Apr 28 at 23:34
  • \$\begingroup\$ You mentioned implementing this outside of the ECS. Where would I store the data that says, "this entity has this behaviour"? If I store it in a component wouldn't that just be what I described above? \$\endgroup\$ Apr 28 at 23:35
  • \$\begingroup\$ @EnderShadow8 If nothing is shared, you still may use event-driven approach. \$\endgroup\$ Apr 29 at 1:18
  • 1
    \$\begingroup\$ @EnderShadow8 I've reworked that part and also added part about message-driven approach. Take a look a tell me if it's still needs improvements. \$\endgroup\$ Apr 29 at 5:25
1
\$\begingroup\$

This question comes from approaching DOD (data-oriented design) from an OOP perspective. In DOD there are no interfaces, as data is the interface.

I think this generic answer about interfaces and DOD is satisfactory. ECS itself is the solution :).

\$\endgroup\$

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.