3
\$\begingroup\$

I have a working ECS-system set up, largely inspired by this brilliant article (not at all required reading to be able to answer my question) on https://indiegamedev.net/ showcasing an ECS-system with proper data locality. I've modified it quite a bit, but the general layout of the API is pretty much the same.

struct SystemFunctor
{
    void operator()
        (float timeSteps, const std::vector<EntityID>&,/*(arrays ->)*/PhysicsComp*, RenderComp*);
};
void system(float timeSteps, const std::vector<EntityID>&,/*(arrays ->)*/PhysicsComp*, RenderComp*);
...

SystemFunctor f;
engine.registerSystem<Engine::RENDER_LAYER_1>(f);

engine.registerSystem<Engine::RENDER_LAYER_1>(system);
/* Both of these calls register a system that operates on entities with at least a 
PhysicsComp and a RenderComp, and will be called when the system layer RENDER_LAYER_1 is run. */

As the title reads, my question is: How can entities that have no component types in common interact with each other? Since a system only receives data from entities whose types are a subset of the entity composition it operates on, how could it receive the data it needs to perform interactions between wildly different entities?

To get a little more specific, here is the current problem I'm trying to solve:

I'm making a small exercise program where the user draw points on the screen with the mouse, and a point-quadtree for all the points is recomputed and drawn (as boxes) on the screen when new points are added.

The adding and rendering of the points was easy - just one system that checks for input and creates the points and another one for rendering them. But when it comes to storing and managing a quad-tree that can come into contact with the point entities I'm at a loss.

Naturally, you would have some kind of quad-tree component:

struct QuadTreeComp
{
// Whatever data your quad-tree implementation uses
};

But from there, I can't seem to figure it out. How do you set up a system that operates on one entity (the one with the QuadTreeComp) while also operating on a bunch of other entities, i.e. the points?

One possible solution I could see is that the system functor that updates the quad-tree doesn't actually operate on QuadTreeComp, but on the points. In its constructor it instantiates an entity with the QuadTreeComp and stores the entity id. Then in its actual function body, it can get the quad-tree component using the id and update it with the points.

class UpdateQuadTree
{
public:
    UpdateQuadTree(Engine& engine):
        engine_{engine},
        quadTreeEntity_{ engine.newEntity() }
    {
        engine.add<QuadTree>(quadTreeEntity_);
    }
    void operator()
        (float timeSteps, const std::vector<EntityID>& entities, Vector2f* points)
    {
        QuadTree* quadTree = engine.getComponent<QuadTree>(quadTreeEntity_);
        /*
                                      ^
                                      |
                            Already an existing function in my ECS-API
        */
        
        // Update the quad-tree using the points...
    }
private:
    EntityID quadTreeEntity_;
    Engine& engine_;
};

The problem with this one is that don't see how other systems that operate on the quad-tree, like the one that renders it, would know the id of the entity.

What would be the textbook-ECS-way of performing these interactions?

\$\endgroup\$

2 Answers 2

1
\$\begingroup\$

I think your example is very specific, so I don't think there is anything in "textbooks" about this.

I'll start with mentioning that there is no ECS police, and so if an implementation and an approach works for you, your team and your case, then it's fine to go with it and not overthink it.

It's not clear to me what would be the interaction between the Points and the QuadTree in your example, so I'll assume that the QuadTree nodes are modified as new points are added.

I think you're missing a component: QuadTreeNodeComp.

You can assume that there will be only one QuadTreeComp, but that might backfire in the future. Alternatively, you can assume that there are multiple QuadTrees, and identity it as such. Then you can make sure that your Points should be organized according to this specifically identified QuadTree.

Assuming we have a QuadTreeNodeComp that looks like this:

struct QuadTreeNodeComp
{
  // If you don't use std::ptrs infrastructure, make sure those values are 
  // nullable so that you can identify the type of the nodes (top node, leaf 
  // node).

  std::weak_ptr<Entity> mParentQuadTreeNodeComp; 
  std::array<std::weak_ptr<Entity>, 4> mChildrenNodes;
  
  // To identify to which QuadTree this node belongs.
  int mQuadTreeId { -1 };
};

... and a PointComp struct that looks like this:

struct PointComp
{
  Vector2f mPoint;

  // To identify to which QuadTree this node belongs.
  int mQuadTreeId { -1 };
};

... and an updated QuadTreeComp struct that looks like this:

struct QuadTreeComp
{
  int mQuadTreeId { -1 };
  std::weak_ptr<Entity> mRootQuadTreeNodeComp; 
};

... when a point is added/moved/removed, update your set of Entities that have a QuadTreeComp associated with the Points and the QuadTree (based on the QuadTree identifier):

UpdateQuadTree::Update( 
  const std::vector<EntityID>& entitiesWithQuadTreeComp, 
  const std::vector<EntityID>& entitiesWithQuadTreeNodeComp, 
  const std::vector<EntityID>& entitiesWithPointComp )
{
  for ( auto& entitieWithQuadTreeComp : entitiesWithQuadTreeComp )
  {
    auto& quadTreeComp = entitieWithQuadComp.getComponent<QuadTreeComp>();
    auto id = quadTreeComp.mQuadTreeId;
    
    // You can figure of a way to delete the entity/components that no longer 
    // have a point in the node, create new Entities/QuadTreeNodeComp that have 
    // points in their nodes, etc. 
    // 
    // For the Entities that have a QuadTreeNodeComp which is a Leaf node, make 
    // sure they also have a RenderComp. The ones that are not Leaf nodes, make 
    // sure the do not have a RenderComp.
  }
}

This is probably not considered purely a "system function" as it does not operate on entities having a subset of components. It's a function that reorganize your entities. (I suppose that this is the answer to your question "How do different compositions/types of entities interact in an ECS-system?".)

After this, you have

  • a series of Entities that contain a PointComp and a RenderComp,
  • a series of Entities that contain a QuadTreeNodeComp and a RenderComp (those are the leaf nodes),
  • a series of Entities that contain only a QuadTreeNodeComp (and no RenderComp, these are the intermediate nodes, they're not rendered),
  • and one Entity that contains a QuadTreeComp.

The Points and the Leaf nodes will be rendered, and you can do whatever else you need with those other entities.

Finally, this may not be the only way to go about this, but it's the approach I'd likely take if I faced this kind of situation.


The problem with this one is that don't see how other systems that operate on the quad-tree, like the one that renders it, would know the id of the entity.

I don't know how you've engineered your project, but one of the key features I've seen required is a way to know to which Entity a Component belongs from the Component itself. If you don't have that, then you'll likely need to think of a way to have this.

\$\endgroup\$
-1
\$\begingroup\$

The point of System (as in ECS) is to update components of a single entity. System will be designed around that core assumption, with all the pros and limitations.

Your goal, on the other hand, is to update entire set of entities, while using entire table of components as an input to this operation. You'll need to access the input entities all at once (assuming that you want to calculate quad-tree efficiently), and create/update/delete the output entities in a single pass.

Which means that your operation does not fit the idea of System (as in ECS), and there is no point of shoehorning it into a System. Realize that every pattern has a scope, and there is entire game engine outside of it's ECS department.

For example, all games have to have Services — those provide centralized access to shared state. I sometimes see people putting those into a "singleton" entity, but this is just a cargo cult programming, and there is no benefit to doing that. The best practice here would be regular everyday normal Dependency Inversion (or it's poor and evil brother Service Locator).


The most straightforward solution to your specific problem is to handle the task inside a service, that just reads and writes all the relevant entities and components. If there are no more inputs and outputs besides the ECS stuff, then a service may be as simple as a function (or a functor — so it can hold a reference to the ECS tables).

If you want to follow the data-driven approach, generalize the update step to work outside of a single entity. Let's call the update function Updater instead of System (and, quite frankly, the name "System" never made sense to me). During the registration step, an updater may request read or write access to entire table (including the entity-component relation table, to allow creation and deletion of entities). It's up to the updater how to read and update those tables. A System will be just a special case of updater.

\$\endgroup\$

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .