4
votes

I'm developing my own game engine with ECS framework. This is the ECS framework I use:

  • Entity: Just an ID which connects its components.
  • Component: A struct which stores pure data, no methods at all (So I can write .xsd to describe components and auto-generate C++ struct code).
  • System: Handle game logic.
  • EventDispacher: Dispatch events to subscribers (Systems)

but I'm confused about how should systems update the members of components and inform other systems? For example, I have a TransformComponent like this:

struct TransformComponent
{
    Vec3 m_Position;
    Float m_fScale;
    Quaternion m_Quaternion;
};

Obviously, if any member of the TransformComponent of a renderable entity is changed, the RenderSystem should also update the shader uniform "worldMatrix" before render the next frame. So if I do "comp->m_Position = ..." in a system, how should the RenderSystem "notice" the change of TransformComponent? I have come up with 3 solutions:

  1. Send an UpdateEvent after updating the members, and handle the event in related System. This is ugly because once a system modify the component data, it must send an event like this:

    {
        ...;
        TransformComponent* comp = componentManager.GetComponent<TransformComponent>(entityId);
        comp->m_Position = ...;
        comp->m_Quaternion = ...;
        eventDispatcher.Send<TransformUpdateEvent>(...);
        ...;
    }
    
  2. Make members private, and for each component class, write a relevant system with set/get methods (wrapping event sending in set methods). This will bring a lot of cumbersome codes.

  3. Do not change anything, but add "Movable" component. RenderSystem will iteratively do update for renderable entities with "Movable" component in Update() method. This may not solve other similar problems, and I'm not sure about the performance.

I can't come up with an elegant way to solve this problem. Should I change my design?

2

2 Answers

3
votes

I think that in this case, the simplest method will be the best: you could just keep a pointer to a Transform component in components that read/write it.

I don't think that using events (or some other indirection, like observers) solves any real problem in here.

  1. Transform component is very simple - it's not something that will be changed during development. Abstracting access to it will actually make code more complex and harder to maintain.

  2. Transform is a component that will be frequently changed for many objects, maybe even most of your objects will update it each frame. Sending events each time there's a change has a cost - probably much higher than simply copying matrix/vector/quaternion from one location to another.

  3. I think that using events, or some other abstraction, won't solve other problems, like multiple components updating the same Transform component, or components using outdated transform data.

  4. Typically, renderers just copy all matrices of rendered objects each frame. There's no point in caching them in a rendering system.

Components like Transform are used often. Making them overly complex might be a problem in many different parts of an engine, while using the simplest solution, a pointer, will give you greater freedom.


BTW, there's also very simple way to make sure that RenderComponent will read transform after it has been updated (e. g. by PhysicsComponent) - you can split work into two steps:

  1. Update() in which systems may modify components, and

  2. PostUpdate() in which systems can only read data from components

For example PhysicsSystem::Update() might copy transform data to respective TransformComponent components, and then RenderSystem::PostUpdate() could just read from TransformComponent, without the risk of using outdated data.

1
votes

I think there are many things to consider here. I will go in parts, discussing first your soutions.

  1. About your solution 1. Consider, that you could do the same with a boolean, or assigning an empty component acting as tag. Many times, using events in ECS is over complicating your system architecture. At least, I tend to avoid it, specially in smaller projects. Remember that a component acting as a tag, could be thought as basically, an event.

  2. Your solution 2, follows up what we discussed in 1. But it reveals a problem about this general approach. If you are updating your TransformComponent in several Systems, you can't know if the TransformComponent has really changed until the last System has updated it, because one System could have moved it one direction, and another one could have moved it back, letting it as at the beginning of your tick. You could solve this by updating your TransformComponent just once, in one single System...

  3. Which looks like your solution 3. But maybe the other way around. You could be updating the MovableComponent in several Systems, and later on in your ECS pipeline, have a single System, reading your MovableComponent and writting into your TransformComponent. In this case, is important that there is only one System allowed to write on the TransformComponents. At that time, having a boolean indicating if it has been moved or not, would do the job perfectly.

Until here, we have traded performance (because we avoid some processing on the RenderSystem when TransformComponent has not changed) for memory (because we are duplicating the content of TransformComponent in some way.

  • Another way to do the same, without having to add events, booleans, or components, is doing everything in the RenderSystem. Basically, in each RenderComponent, you could keep a copy (or a hash) of your TransformComponent from the last time you have updated it, and then compare it. If it's not the same, render it, and update your copy.
// In your RenderSystem...

if (renderComponent.lastTransformUpdate == transformComponent) {
    continue;
}
renderComponent.lastTransformUpdate = transformComponent;
render(renderComponent);

This last one, would be my preferred solution. But it also depends on the characteristics of your system and your concerns. As always, don't try to optmize for performance blindly. First measure, and then compare.