1

I am currently building an Entity Component System (ECS) in cython in order to speed up operating on large numbers of game objects in python. In the process of building this system, I ran into the following architectural issues that I would appreciate shedding some light upon. I have read through lots of online blog posts describing ECS strategies, but most of these are geared towards C++ and do not describe interfacing such systems with a scripting language like python.

I am defining the parts of my ECS as follows for clarity, since everyone seems to have different terminology/mental frameworks for these concepts:

  • Handle: This is a uint64_t that is subdivided into index, version, type, and free parts. For use in my CompMaps.
  • Entity: Just a handle.
  • Comp (Component): Just a handle.
  • CompType: A uint8_t (derived from the Handle's "type")
  • CompMap: This is a "slotmap" data structure. Memory for each component/entity can be retrieved using Handle objecsts.
  • Archetype: A set of CompTypes, mapped to a uint16_t for easy access.

Since the component structs are not directly accessible in python outside of numpy arrays, and since I don't have access to C++ templating in python that is typically used for registering components, I expose all accesses of entities and components through these handles. As such, I have the following questions regarding how to safely provide access to CompTypes and Components in my ECS.

  1. Should each "world" in the ECS be able to define its own unique CompTypes and Archetypes?
    • Pros: Allows some flexibility (i.e. there could be a UI specific set of CompTypes for a "UI world" and there would be no need to "waste" CompType int slots for these in the general gameplay world.
    • Cons: Systems can't reliably operate on multiple worlds if the ints for each of the world's CompTypes mean different things!
  2. How should "built-in" CompTypes and Archetypes from the rest of the game engine be handled?
    • Currently doing this as a global enum inside a cython module.
    • Should these built-in types be auto-registered into every world upon initialization?

Please let me know if there are any parts that were unclear, and I would be happy to clarify them. Any help would be greatly appreciated.

  • Too broad. Narrow your question down to one specific, software design-related problem you are having. – Robert Harvey Jul 5 at 16:57
  • @RobertHarvey I was worried that might be the case. I guess if I had to focus on one part, it would be on how to expose my CompTypes to python. I will go ahead and clean this question up. – CodeSurgeon Jul 5 at 17:02
  • @RobertHarvey Thanks. I am aware that both ctypes (and cffi as well) exist. However, these would not play well with the static typing of structs that cython offers. Additionally they would add the overhead of generating python objects to each of my components, and would reduce cache coherency as a result if I were to loop over these ctype-wrapped structs. It's for the same reason that I ditched wrapping the data of my components in cython's "cdef class" objects. – CodeSurgeon Jul 5 at 17:20
  • 1
    I may have misunderstood something, but are you saying that the problem is essentially that you've came up with a scheme that lets you identify presence of certain component types on entities, where the code in your systems (on the python side) will have to understand the meaning behind these types/handles (essentially hardcoding it in a bunch of if-s), but now that gets in the way of your desire to allow developers to define their own comp types and comp sets (or, at least, you anticipate that, somewhere in there, the code will be hackish and messy)? Is it something along those lines? – Filip Milovanović Jul 6 at 0:18
1

I am answering this for completeness. The resulting API design that I went with can be best illustrated with some example user code:

from pyorama.core.app import *
from pyorama.core.system import *
from pyorama.core.world import *

class EnemySystem(System):

    def init(self):
        pass

    def quit(self):
        pass

    def update(self):
        COMP_GROUP = self.comp_group_types["test"]
        print("Updating...", id(self.world), COMP_GROUP)

class Game(App):

    def init(self):
        MAX_ENTITIES = 10000
        COMP_A = 123
        COMP_GROUP = 0

        super().init()
        self.world = World()
        self.world.init(MAX_ENTITIES)
        self.world.register_comp_type(COMP_A, 1000, 32, b"4Q")
        self.world.register_comp_group_type(COMP_GROUP, [COMP_A])

        comp_a = self.world.create_comp(COMP_A)
        comp_b = self.world.create_comp(COMP_A)

        ent_a = self.world.create_entity()
        self.world.attach_comp(ent_a, comp_a)
        self.world.attach_comp(ent_a, comp_b)

        enemy_system = EnemySystem()
        self.world.attach_system(enemy_system, {"test": COMP_GROUP})
        self.world.set_system_update_order([enemy_system])

    def quit(self):
        super().quit()

    def update(self):
        self.world.update()

game = Game(use_sleep=True, use_vsync=False, ms_per_update=1000.0/60.0)
game.run()

The solution that I came up with to map System objects to the appropriate World and CompGroupType objects (what I had previously referred to as Archetypes) was to create an attach_system method. This method would be responsible for registering a system and supplying the correct world data. The correct components could then be located using the keys passed into the dict of CompGroupTypes.

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.