lundi 6 mars 2017

Entity-Component System : fear of losing type & readability

I have a Entity-Component System.

Components : small classes without complex function
Entity : integer + list of Components (<=1 instance per type per entity)
Systems : each one iterates 1-3 (mostly 1) type of component to do some game logic

Here is a sample. A bullet entity pewpew is exploded, so it will be set to be invisible:-

class Component_Projectile : public ComponentBase{
    public: GraphicObject* graphicObject=nullptr;     //###
    public: Entity whoCreateMe=nullptr;
    public: Entity targetEnemy=nullptr;
    //.... other fields ....
};
class System_Projectile : public SystemBase {
    public: void explodeDamage(Entity pewpew){ //called every time-step
        Pointer<Component_Projectile> comPro=pewpew;
        comPro->graphicObject->setVisible(false);     //###
        //.... generate some cool particles, damage surrounded object, etc
    }
    //.... other functions ....
};

It works OK.

New Version

Half year later, I realized that my architecture looks inconsistent.

  • I cache all game-logic object using Entity.
  • But cache Physic Object and Graphic Object by direct pointer.

I have a crazy idea :
Physic Object and Graphic Object should also be game-logic object!
They should be encapsulated into Entity.

Now the code will be (the change is marked with ###):-

class Component_Projectile : public ComponentBase{
    public: Entity graphicObject=nullptr;                         //###
    public: Entity whoCreateMe=nullptr;
    public: Entity targetEnemy=nullptr;
    //.... other fields ....
};
class System_Projectile : public SystemBase {
    public: void explodeDamage(Entity pewpew){ //called every time-step
        Pointer<Component_Projectile> comPro=pewpew;
        system_graphic->setVisible(comPro->graphicObject,false);  //###
        //.... generate some cool particles, damage surrounded object, etc
    }
    //.... other functions ....
};

After playing them for a week, I can conclude pro/cons of this approach as below :-

Advantage

1. Drastically reduce couple between game logic VS graphic-engine/physic-engine

All graphics-specific function is now encapsulated inside 1-3 systems.
No other game systems (e.g. System_Projectile) refer to the hardcode type e.g. GraphicObject.

2. Dramatically increase flexibility in design

Old vanilla graphic object is not just a graphic object anymore!

I can change it to something else, especially add special feature that is totally insane / too specific for physic/graphic engine, e.g.

  • rainbow blinking graphic
  • strange gravity, magnet
  • swap in/out many physic-object type in the same Entity (not sure)

3. Reduce compile time
It is accidentally become a pimpl idiom.
For example, System_Projectile don't have to #include "GraphicObject.h" any more.

Disadvantage

1. I have to encapsulate many graphic/physic-object's functions.
For example,

system_graphic->setVisible(comPro->graphicObject,false);

is implemented as

public: void setVisible(Entity entity,bool visible){
    entity.getComponent<Graphic_Component>()->underlying->setVisible(visible);
}

It is tedious, but not a very hard work.
It can be partially alleviated by <...>.

2. Performance is (only) little bit worse.
Need a few additional indirection.

3. Code is less readable.
The new version is harder to read.

comPro->graphicObject->setVisible(false);                //old version
system_graphic->setVisible(comPro->graphicObject,false); //new version

4. Losing type + Ctrl+space is less usable
In old version, I can easily ctrl+space in this code :-

comPro->graphicObject-> (ctrl+space)

It is now harder. I have to think which system I want to call.

system_(ctrl+space)graphic->(ctrl+space)setVisible

Question

In most code location, the advantage overcome the disadvantage, so I decided I will use the new version.

How to alleviate the disadvantages, especially number 3 and 4?

Aucun commentaire:

Enregistrer un commentaire