0
\$\begingroup\$

I have created ECS framework from scratch using C++.

Everything works fine, but I think my code is not so cute (at #1):-

class RocketComponent{
    Entity mainBodyGraphic;   
    Entity topGraphic ;
};
//inside some system (script-like)
void createRocket(){
    Entity mainBodyGraphic = graphicFactory->createCylinder();    
    Entity topGraphic = graphicFactory->createCone();    
    //^ internally create entity (with graphic component and others),
    //         then attach together
    Pointer<RocketComponent> rocket  = create<RocketComponent>();
    rocket->mainBodyGraphic = mainBodyGraphic;
    rocket->topGraphic = topGraphic;
    //.... other logics ....
}

void showDamage(Entity rocket){
     Pointer<RocketComponent> rocket  = rocket->get<RocketComponent>();
     systemGraphic->setColor(rocket->mainBodyGraphic,RED);   //#1
}

#1 is ugly.
In my dream (Object-oriented style), #1 could be :-

rocket->mainBodyGraphic->setColor(RED);                      //#1

The above line is far easier to read and code. It is also less error-prone.

The Change (super entity)

I plan to change my code to support it, but I don't want to lose any disadvantage of ECS.
Therefore, I will create super entity.

For example:-

  • I create a new super Entity class named GEntity (graphic entity).
  • I provide a converter (operator=()) between Entity and GEntity.
  • Whenever a function just want Entity, I can pass GEntity instead, and vice versa!
  • graphicFactory->createCone() and similar functions will return GEntity instead of Entity.
  • The field in RocketComponent will be GEntity mainBodyGraphic;.
  • Entity has no access to game logic like before, but GEntity can access all game logic/system.

Here is my adoption (future plan / promise to myself):-

  • In GEntity, I can add all syntactic sugar functions e.g. setColor() as much as I want.
  • I will use GEntity only when logical type of entity is very obvious.
  • I will also create PEntity = Physic body, because its type is also obvious.
  • I may also create other types of super entity if its type is very obvious && I want the syntactic-sugar.

Here is the result (change marked with $):-

class RocketComponent{
    GEntity mainBodyGraphic;      //$
    GEntity topGraphic ;          //$ 
};
void createRocket(){
    GEntity mainBodyGraphic = graphicFactory->createCylinder(); //$ 
    GEntity topGraphic = graphicFactory->createCone();          //$ 
    Pointer<RocketComponent> rocket  = create<RocketComponent>();
    rocket->mainBodyGraphic = mainBodyGraphic;
    rocket->topGraphic = topGraphic;
    //.... other logics ....
}
void showDamage(Entity rocket){
     Pointer<RocketComponent> rocket  = rocket->get<RocketComponent>();
     rocket->mainBodyGraphic->setColor(RED);                    //$ 
}

Concern (my fear)

I feel that I am violating some ECS rules.
I am probably destroying good ECS abstraction and design pattern in favor of my indolence.

I have already refactored some of my code to the new way.
It looks nice and neat. I think I am biased.

Question

  • What are disadvantage of this approach?
  • What are things (potential issue) that I should concern and prevent?
  • What is the name of this technique/design-pattern (if any)?
\$\endgroup\$
1
\$\begingroup\$

I would actually consider your rocket as a single entity.

That single entity holds a reference to a MeshComponent that is designed to hold a base mesh and zero or more sub meshes. If we think about how meshes are separated to draw them with different materials, this is pretty common. So that component would look like this:

struct mesh_t {
  string meshName;
  // other stuffs
};

struct MeshComponent {
  int entityId; 
  mesh_t baseMesh;
  vector<mesh_t> subMeshes;
};

The next question to ask is whether the graphics system's renderable representation should be exposed at all in your component. I've seen some examples where developers do this:

struct MeshComponent {
  int entityId; 
  mesh_t baseMesh;
  vector<mesh_t> subMeshes;
  GraphicsRenderable* baseRenderable;
  vector<GraphicsRenderable*> subRenderables;
};

This works, but I prefer encapsulation and abstraction. The GraphicsRenderable is the graphics system's scene graph representation of those meshes and I prefer that not to be immediately accessible by anyone who can get my MeshComponent.

Instead, the graphics system maintains an internal mapping between its scene graph renderables and the external MeshComponent instances. The system knows when the component is destroyed, it has to cleanup the appropriate renderables or perhaps when state changes on the component, the system replicates those changes to the renderables internally.

So I end up with something like:

// external to ecs users

struct mesh_t {
  string meshName;
  // other stuffs
};

struct MeshComponent {
  int entityId; 
  mesh_t baseMesh;
  vector<mesh_t> subMeshes;
};

// internal to graphics system

struct Renderable {
  MeshComponent* component;
  vector3 position;
  vector4 rotation;
  scene_ptr node;
  ...
}

To specifically mention your use case about color, I would expose that on the mesh_t struct and your code becomes this to change it:

MeshComponent& mesh = entity.getComponent<MeshComponent>();
mesh.baseMesh.color = Colors.RED;
mesh.subMeshes[0].color = Colors.RED;

The next time the graphics system updates, it will replicate the color changes from the MeshComponent to the associated Renderable and then on the next draw, the meshes will be the color red.

At the end of the day, treat your ECS as a bridge between game code and base/core systems of your game engine. Your game code interacts with components, systems take those interactions and manipulate internal models and structures to achieve the goal.

This separation of concerns will make refactoring code much easier too.

| improve this answer | |
\$\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.