1. 21

    1. 3

      if you really do need an answer immediately, you can use a switch statement, or look up a function based on a value on the pet. it’s not worse than OOP, but you don’t get a lot of the ECS benefits, either. you can check the human and the pet for particular components, which is still more flexible than using OOP game objects

      i can’t see why you wouldn’t be able to tolerate a 1 frame delay on something like that, though. human moves, then tries to adopt a pet. the human isn’t going to move again on the same frame, so i don’t really get where that constraint is coming from

      for the UI problem: it’s kinda orthogonal to your game object structure. you can use an OOP UI, or immediate mode, or ECS UI components, and mix it with an OOP or ECS game world. (nobody uses an ECS UI with an OOP game world, though)

      1. 3

        Yeah, I’ve come to a similar conclusion. A big switch statement is.. fine, but extremely awkward when you have many different types of pets. The switch function will have an incredible number of dependencies piped through and become a sort of uber-function. Not great.

        I think adding some basic support for function pointer tables, or polymorphic interfaces would be sensible, to pretty much any ECS. It’s slower, definitely, but sometimes you need it. This is similar to Rust supporting Trait Objects, which are slower than generic traits, but a useful choice when you need just a little more flexibility.

        A 1-frame delay wouldn’t be suitable for an interaction system. Say that humans are players, and pets are objects that they all want to pick up. You need to assign objects to each of the players such that only one player holds one object.

        You wouldn’t want that system to have a one-frame delay, because you would then see a single frame where players thought they were holding objects, but the objects hadn’t yet been assigned. Or.. it would mean a 1-frame delay for interaction, in general. That might be fine in some games, but it would make other logic more complex, and I can imagine more complicated game systems where that would not be suitable. It’s pretty easy to allow a 1-frame delay on a couple of systems that chain together and quickly have something much more laggy.

        1. 1

          i ran into this issue before when using an ECS. where you kinda want a virtual function (my problem was enemy AI, where i wanted an updateAI function on a component, and 50 different implementations of it)

          what i found most convenient, was to keep a global table of id->function, and store the ids on the components. it lets you register functions from many systems, rather than editing one big switch. you could store a function pointer on the component, and do the table lookup during serialization/deserialization (which is basically what OOP is doing under the hood when serializing the instance-type/vtable-pointer), but the performance was fine doing the lookup every frame, in my case

          on the 1 frame delay: you actually get the answer later in the same frame. it’s just that the system that triggered it (like, the human’s AI) already ran, so that system won’t see the result until next frame. it stops you from doing if(tryGetPet()). but, if you generally do player/AI intent first, then game state updates, and then rendering each frame, you should get results on the same frame before the player sees it (you can also interleave intent and state update, but ordering can matter)

          1. 1

            Yeah, I think that’s about where I’ve arrived as well. ECS should support some form of Virtual Functions or ID-based function pointers. Ideally they’re enums or something so it’s all still blittable/serializable.

            Regarding frame delay: If you have virtual function calls, there’s really no reason to add any delay. I think it’s best to handle it all in a single system. Imagine a situation with a big stack of interactable objects, and 100 players all ‘requesting’ to interact. There isn’t a single request per-frame per-human, each human is requesting to interact with everything. You’d have to sub-step the interaction system within the frame, until all of the pets got fully adopted.

    2. 2

      The way I would (naively?) approach this is by creating a different component per adoption preference (ScaredComponent, UglyComponent). Each component implements the same CanBeAdopted() interface. The “cheat” comes in with a specific ECS library feature in the flecs library. All of those unique adoption components would use flecs’ inheritance relationship feature to categorize them as a AdoptionPreferenceComponent type. An Adoption system would iterate through all AdoptionPreferenceComponent components, executing the CanBeAdopted() method for each one.

      But, I don’t know if its good or bad if the component holds some logic? Also, yeah, I’m still using a form of inheritance, but if a library (flecs) lets me have the best of both worlds, why not?

      Edit: Realized the post describes components as data-only. Oh well, I’m definitely down for hearing different patterns for accomplishing it.

      1. 1

        Yeah, I agree that ECS should have some form of polymorphism like this! It’s very cool that flecs enables that! :)

        It’s a little weird to stuff logic in components – but it would work in many cases! It might require piping through a lot of world context, though, because the component object wouldn’t necessarily have access to the rest of the ECS world (although I don’t know how flecs works!).

    3. 1

      The way RobustToolbox (and by extension Space Station 14) handles this is immediate-mode events, i.e. events that get handled immediately and are passed by reference so you can read back results from them. (Fancy callbacks, of a sort. No actual calling back into the event firer but data is passed along.) Pretty much all interaction related logic or generally decoupled logic opts for this, even if it is slower.

      I ultimately don’t think this problem is parallelizable without some serious thought (due to the constraint of “arbitrary behavior may occur during the check”), so this solution is fine for me.

      1. 1

        Oh that’s fascinating! I’ve never heard of a system like this for ECS. I’d love to learn more – do you have any references?

        You’re right – the problem is specifically intended to be non-parallizable. That’s part of what makes it hard for ECS to handle.

    4. 1

      This is pretty easy to answer, and we have innumerable examples of this sort of polymorphic logic in Veloren.

      The answer is: ECS doesn’t preclude polymorphism. You can still use inheritance/extension (or in the case of Rust, enums/sum types) on top of ECS just fine without trouble.

      ECS isn’t there to be all-consuming, and it doesn’t demand that you try to express absolutely everything inside it. We still have entire subsystems that sit outside of the ECS world and work just fine: Heck, the real-time world simulation system even sits entirely outside it for reasons I won’t go into here. ECS is just a way of structuring high-level entity relationships in a manner that is flexible and performant. It’s not a philosophy, a religious creed, or a dogma that needs adhering to at every turn.

      1. 1

        We still have entire subsystems that sit outside of the ECS world and work just fine: Heck, the real-time world simulation system even sits entirely outside it for reasons I won’t go into here.

        While I accept your decision not to discuss this point, I am surprised to hear that — my (unlearned, non-game-developer) assumption had been that simulating a world à la Dwarf Fortress would be among the most suitable use-cases for ECS.

        1. 2

          Sure, I can go into the details a little.

          First, I want to clarify what this system is: it’s not ‘the game’. Regular entities (NPCs, enemies, players, etc. that you see running around) are still in the ECS and are made up of some combination of many dozens of components. That all works fine and great. The world simulation system is an abstraction that sits on top of this and is designed to simulate tens of thousands of NPCs all across the world in real-time, but in a sort of ‘low-resolution’ state. We don’t simulate collisions, the intricacies of combat, etc. It has more in common with a turn-based game of Dungeons and Dragons than it does the main game.

          You’re right that ECS - in the abstract - would be a good fit for this. But there are a few specific reasons that we don’t use it:

          1. The data model. We want to be really specific about the data model for the sake of persistence, and representing all of the data as a series of flat structs makes it easier to manage.

          2. The main benefits that ECS brings (very low iteration overhead, composition, etc.) are not so useful here. Data is largely homogeneous, and we dynamically vary the tick rate of every NPC based on what they’re doing and how far they are from players to avoid overloading the server (in practice, most simulation things continue to function just fine at lower tick rates, sometimes as low as 0.1 TPS). Most of he heavy lifting of the simulation code occurs at the level of AI logic, and ECS doesn’t do much to help us here. So we don’t need rapid iteration, and we don’t really need composition (there are a few cases where it could potentially be useful, but nothing that a sum type won’t handle).

          3. ‘rtsim’ (as it’s called) is a distinct component to the rest of the game. It’s designed to be run standalone (you can imagine a sort of Dwarf Fortress legends viewer or, heck, an RTS being built on top of it), if desired. Therefore, it deliberately has a very weak coupling to the rest of the game, and the main game server interacts with it through a series of callback hooks. You might say that it has a ‘thin neck’. For this reason, we want to keep it cleanly separated from the performance-critical parts of the main ECS.

      2. 1

        I agree with this sentiment. Unfortunately not all ECSs are that flexible. Unity’s just straight up doesn’t really support inheritance (unmanaged types only). You could create some C# objects with inheritance on the outside of the ECS, but.. that sort of starts to defeat the purpose of using ECS in the first place. Especially for a game that has lots of arbitrary gameplay logic, and few shared systems.

        But I do agree. There are lots of ECS fundamentalists, and in part this article is targeting them to see how they think they can solve this entirely in an ECS.

        1. 2

          Ah yes, I’ve seen these sorts of language-specific limitations crop up. I recall having a long and protracted debate with a C# maximalist in a past job who wanted to ‘have his cake and eat it’ (combine C# and ECS without such limitations).

          My argument would be that this isn’t a problem with ECS as an architectural system, but with C# as a language: making ECS both performant and safe requires a language that allows very fine-grained control over data access patterns (something that Rust excels at but, sadly, most managed languages suck at) and this fact tends to aggressively permeate any attempt to work ECS into these languages.

          I think this is why, despite Unity demoing their ECS tech about a decade ago, it’s not yet become mainstream: C# simply isn’t cut out to support that sort of thing. It’s also why ECS is pretty much the default in Rust: it’s just so easy to do and naturally fits into the data-oriented patterns encouraged by the language.

          1. 1

            Agreed. A purpose-built ECS language would be really interesting. It’s something I’ve been exploring for a little while now, actually.

            I’m not sure I agree that it’s easy in Rust. Certainly the mutability control is a major help, but it’s always felt more like it’s just because OOP is so difficult. ECS is really the only sensible option unless you want Rc<> all over the place.