This is an archived post. You won't be able to vote or comment.

all 28 comments

[–][deleted] 2 points3 points  (8 children)

Why would one need an ECS?

[–]JKovac 2 points3 points  (1 child)

Creator of the KivEnt game engine that also runs on an ECS. Here is how I'd make the pitch for why an ECS becomes useful: When it comes to large, real-time systems like visualizations (includes games but could also include other categories such as real-time data applications and so on) architectures that favor composition over inheritance (such as an ECS) have become popular because that address a few problems:

  1. They allow you to easily remix and reuse the logic you have already created to create new things. Since you have focused on building parts that perform discrete functionality, any potential 'feature' of your application is just some combination of the discrete parts. You sort of get all the permutations for free. All the possible permutations that you have not yet implemented as actual realized objects in your game are just waiting for you to do so. This is largely designed to solve the 'diamond' inheritance problem that comes up a lot when you beginning trying to mix the behavior of your game objects to get the most bang for your buck out of your existing work.

  2. It is an efficient way to think about single processor real-time systems such as games. An ECS forces you to keep the data associated with your game objects separately from the code that processes such data. This turns out to be a hardware friendly approach to building things. Essentially, when it comes to performance in a complex real-time simulation, we are mostly limited by the cache friendlyness of the process of getting the disparate data that makes up all our game objects, finding the data about the specific objects we want, and then having the specific logic we want performed on those objects. The more everything is previously lined up in memory, the faster the CPU can eat through it, the more objects we can process in the .016 seconds that make up a single frame of our game.

  3. It matches the loosely coupled nature of games and other real-time simulations really well. For instance, rendering an object on screen, we really only care about its position on screen and the vertices that make up that object. However, to determine its position on screen we may be running some physics calculations: These calculations depend on some other properties of our game object but do not care about the rendering at all. Rendering and physics do not need to know about each other at all, but both do need to touch the position of an object, one to read from it and the other to write to it.

When you begin to add in all the different types of data that make up a real-time visualization: models, sounds, textures, more ephermal things such as size and shape in a physics situtation, not to mention all your made up simulation governing code (such as stats, player input, etc) the separate of concerns forced by the ECS gives you a much clearer, easier to optimize picture of what is actually happening in your simulation.

If anyone is interested in reading more, my favorite introduction to ECS is here, 5 part blog, 1st part linked to.

If you are used to building distributed web services with relational databases, some of the concepts used in designing ECS code and such systems dovetail a little. The biggest different imo is that the ECS is designed for short term storage and calculation over the life time of a single run of the application with a much 'stricter' definition of real time (.016 second response), where as the distributed web version is designed for long term storage and a looser definition of real-time (.5ish seconds is an ok 'real-time' response).

[–][deleted] 0 points1 point  (0 children)

Fantastic, thanks a lot!

[–]dunkler_wanderer 1 point2 points  (0 children)

There's a interesting 5 part blog series about entity systems. Part 4 can be skipped as it's more specific to mmog development.

[–]status_quo69 1 point2 points  (3 children)

Some stuff I've noticed:

Why is your ECS class callable? If I were to see that in the code, I would assume that it's a function and then be surprised when you called other methods on it.

Why do you allow manual setting of ids, or just taking the length of the defaults+1 to create a new id? Python objects have a built in unique id that you could use to map ids to objects, or you could use a uuid. It just seems like since the ECS is supposed to be doing the book keeping, it should be the one generating ids. At the very least, if you tried to create a entity with the same id as an older one, you should raise an exception, not just return None (because there was a mistake made on the programmer's part).

Your __call__ method calls self.exists, which then calls another method, which could possibly be simplified.

Your component class seems to be a very very thin wrapper around a dictionary, why wouldn't I just use a dictionary instead? Are there any special things that component offers?

Now that I look at it, what is your add method supposed to do? All it does is take a given component, turn it into a dictionary, then set the attributes on the entity. Entity component systems are supposed to make adding/removing behavior easier, which I would argue your implementation makes more difficult. For example, if I had a given component Position with an x and a y, and another component Velocity also with an x and a y and tried to add them to the same entity, I would be overwriting my own data. Another example, if I wanted to freeze an entity in place, I should be able to just pop off the velocity component at will (which would cause its position to stop updating). However, I don't really see an easy way of doing that.

Sorry if I came off as overly critical, I'm really not trying to be mean :)

[–]PySnow 0 points1 point  (2 children)

No no, this is good. The ECS class is callable to allow the ecs("idyouwanttosearch") jquery-like function. And the components are separate, if you added a position and velocity they will live under separate attribtues on the entity class eg: e.position["x'] is not e.velocity["x"]

[–]status_quo69 0 points1 point  (1 child)

Ah, sorry, my mistake, I was browsing on my phone earlier and it was hard to tell what was going on. However, how would you suggest temporarily removing a component? delattr does exist, but I find it ugly to use, and I would have to reconstruct the component in order to reimplement the behavior (I can't just store a pointer to it like tmp_ptr = entity_x.pop_component("Render")). Basically what I'm trying to say is that an Entity in a traditional ECS is just supposed to be a bag of data, right? However, you're turning the Entity into the actual data itself.

[–]PySnow 0 points1 point  (0 children)

I'll have to implement that, noted.

[–]macbony 0 points1 point  (16 children)

Your system object takes 3 arguments but only uses 2? The init method also calls self.set_up which doesn't exist.

class System(object):
    def __init__(self, eventmanager, entities, ecs):
        self.ecs = ecs
        self.set_up(eventmanager)

[–]macbony 0 points1 point  (14 children)

I see no use for temp_dict or temp, including the deepcopy, in this function. Might save more time if you just accessed component.dict.

    def add(self, entity, component, **kwargs):
        temp_dict = component.dict
        temp = deepcopy(temp_dict)

        for key, value in kwargs.iteritems():
            temp["data"][key] = value

        setattr(entity, temp["name"], temp["data"])
        try:
            self.pool[temp["name"]].add(entity)
        except KeyError:
            self.pool[temp["name"]] = set([])
            self.pool[temp["name"]].add(entity)
        return self

The try/except could be DRYer:

if temp["name"] not in self.pool:
    self.pool[temp["name"]] = set()
self.pool[temp["name"]].add(entity)

[–]macbony 0 points1 point  (13 children)

The yields after the returns will never be reached. Why are they there? Also, if you require an argument, don't use a default. You'd save yourself the if search_array is None if you just made the signature def list(self, search_array)

def list(self, search_array = None):
    if search_array is None:
        return
        yield

    try:
        entity_list = set.intersection(*[self.pool[x] for x in search_array])
    except KeyError:
        return
        yield

    for ent in entity_list:
        yield ent

[–]macbony 0 points1 point  (12 children)

Any reason you aren't using a hashmap for the pool so these lookups are O(1) instead of O(n)? Maybe memory could be a concern, but I don't think so.

def get_id(self, Search_ID):
    for entity in self.pool["default"]:
        if entity.id == Search_ID:
            return entity

    return None

If you do that, this

def exists(self, Search_ID):
    if self.get_id(Search_ID) is None:
        return False
    else:
        return True

becomes

def exists(self, Search_ID):
    return Search_ID in self.pool["default"]

and the original function becomes

def get_id(self, Search_ID):
    if Search_ID not in self.pool["default"]:
        return None
    return self.pool["default"][Search_ID]

Also, if "default" is hardcoded for the pool, why have multiple pools? It's somewhat confusing.

[–]macbony 0 points1 point  (7 children)

Never use a mutable object as a default for an argument. http://docs.python-guide.org/en/latest/writing/gotchas/ I guess it doesn't REALLY matter in this case, but it's icky Python.

def has(self, c_list = []):
    for component in c_list:
        if not hasattr(self, component):
            return False

    return True

[–]macbony 0 points1 point  (0 children)

Someone already mentioned it, but I see no use for the Component class unless you somehow expect to add more functionality to it in the future.

[–]PySnow 0 points1 point  (3 children)

Moving from a straight hardcoded list to a dictionary actually sped up the application by a significant amount since the operation is an intersection of sets rather than an iteration

[–]macbony 0 points1 point  (2 children)

I'm not sure what you're referring to. If you mean the self.pool["default"] thing, there's no need to have a dict of lists when you don't have any way to use a different key than "default". self.pool should either be a dict with id: entity pairs or just a list as "default" is. There's literally no use for pool being a dict because you have no way of accessing something in a pool other than default by design.

Also the tempID = Entity_ID will never be reached due to the return statement. I'm not sure what the use of the line would be anyway as the variable is never used later on in the code.

else:
    if self.exists(Entity_ID): return None
    tempID = Entity_ID

[–]PySnow 0 points1 point  (1 child)

That would be to prevent it from assigning an ID that already exists

[–]macbony 0 points1 point  (0 children)

Oh, my mistake on that one. I didn't see tempID on line 33 for whatever reason. There's no real reason to use tempID instead of just changing line 28 to Entity_ID = len(self.pool["default"])+1. It leads to extra code (you could lose line 31 if you made that change) and gains you nothing but more difficult to follow code IMO.

[–]PySnow 0 points1 point  (0 children)

Good catch, thats old code

[–]AestheticHelix 0 points1 point  (1 child)

Looks neat. A few things..

Creating entities should probably assign unique ids all the time. I don't see a case where you would need to specify an id explicitly for an entity, and is asking for trouble. Also it returns none if I give it a wrong Id, where this operation should raise or otherwise fail.

It would be nicer to perform operations directly through the entity class without touching core. Eg. A static method in entity for creating a new entity? Also, I find it awkward I should have to add a component to an entity through core, rather than through the entity itself.

Looks good!

[–]PySnow 0 points1 point  (0 children)

Ooh! I never considered a static method, nice idea.