1
\$\begingroup\$

Preamble: I'm working on a level editor for my engine, which uses an ECS architecture. I have around a dozen component types so far, mostly dealing with graphics, like lighting components, model, and other mesh related things like skeleton and collider components.

My engine only supports 1 of each component type per entity, and systems can deal with optional components as well.


Today I realized I was uncertain on how I should tie-together the building block components used for basic entities into a more complex one.

For example, let's say I want to make a torch / flaming stick, using the components found in a model entity and a lighting entity.
If the model's origin is in the center of the shaft, then the light and fire would appear at the center of the model.
Further, if I wanted a weird torch with fire on one end of the stick and smoke on the other, they would both need different offsets before rendering.

Do I need to create a system just for torch's, adjusting all the components to fit the model? This doesn't sound like it scales well to different models needing 'decorations'.

Should I instead be creating an attachment component, with its own offset variables? What dictates which components of an entity use the offset? Should I be making attachment types for every component type that could need an offset?
Or rather should most components just have a spatial offset as another parameter the user can control?

Anyone know any commonly used solutions?

\$\endgroup\$
  • \$\begingroup\$ This sounds like a job for a transformation hierarchy: one entity and its components represents the torch, and a different entity with its components represents the flame. The transform of the flame is parented to the torch, to move with it at an offset dictated by its local position. All your components rely only on their entity's transform's world position, no additional offsets/special cases required. Same with wheels on a car, etc. Presumably you've looked into this? \$\endgroup\$ – DMGregory Aug 3 at 23:31
  • \$\begingroup\$ I don't think this would work, what if one part of this original entity needs to influence a part of the other? Since they're split into 2 entities, no system can act on the entire set of components defining a torch. Also a system having to look up a component's entity's parent entity's components to find a transformation per-component would totally trash the cache, eliminating one huge benefit of ecs in general \$\endgroup\$ – Yattabyte Aug 4 at 0:24
4
\$\begingroup\$

Games typically approach this type of issue using a transformation hierarchy.

In this model, each entity can be treated as a "child" of a "parent" entity. The entity's local position and orientation components are interpreted to be relative to the position and orientation of the parent.

Parent entities may in turn have their own parents, grandparents, etc., all the way up to "root" objects that are positioned absolutely in world space.

This lets us build complex articulated structures - like a car with independently spinning/suspended wheels and passengers/cargo that follow it along for the ride, or a humanoid character with a hand that pivots at the wrist of a forearm that pivots at the elbow of an upper arm that pivots at the shoulder... all using one simple transformation composition rule, rather than adding special-case offsets and variants throughout all our components.

Contrary to the concern raised in the comments, this does not need to be unduly unfriendly to the cache. In a simple model, we can associate the following data with each entity:

  • a local position
  • a local orientation
  • a local uniform scale
  • a parent entity index (-1 for "this is a root entity")
  • a world position
  • a world orientation
  • a world uniform scale

Most systems make updates only to the local properties (say, a skeletal animation system rotating each bone's local orientation about its pivot) so they don't need to peek into the hierarchy at all and can do their work strictly on one entity at a time (friendly for parallelization). We can defer updates to the world properties until the next physics or rendering step where we need final positions & such.

If we store our entities in non-decreasing order of parent entity ID (this is not too onerous to maintain, since re-parenting is very rare compared to routine transformation updates), then we can update the whole hierarchy's world properties in one linear scan:

  • First, we update all the root entities by copying their local parameters to their world parameters

  • Next, we walk these lists of components with two indices: the current entity we're updating, and the parent index.

    Both these indices move strictly forward through the arrays, so we're not jumping back and forth randomly thrashing the cache. In fact we'll often update several entities in a row with the same or adjacent parents, getting excellent cache utilization despite the indirection.

  • For each of these entities, we update the global properties like so:

    parent = parentIndex[current];
    worldPosition[current]    = worldPosition[parent] 
                              + worldOrientation[parent] * localPosition[current];
    worldOrientation[current] = worldOrientation[parent] * localOrientation[current];
    worldScale[current]       = worldScale[parent]       * localScale[current];

You can even parallelize this work if needed, by dividing your root objects between separate arrays, and placing child objects in the same array as the root. Then each array can have its hierarchy updated independently of the others.

Most interaction between entities in the hierarchy can be accomplished with message systems. So the Torch system doesn't need to directly manipulate the Light Source component on the torch's child entity. It can just leave it a "Turn on" / "Turn off" message when it needs to change state, and the Light system can process that message when it iterates its Light Source components later in the frame.

Now, there will be occasional scripts that do need to reach across to different entities make their decisions and updates. Like an AI awareness system that needs to gather information about nearby entities to update the current entity's state. That's OK though - and largely unavoidable anyway, even with a flat hierarchy. The goal of a data-oriented ECS is not to outright forbid reference-following, it's to keep costly reference-chasing out of the hottest code paths - the things we need to iterate thousands of times per frame.

You can have thousands of animated characters swinging tens of thousands of individual bone transforms around with minimal cache misses, so the less predictable parts of your game scripts - like the player character control logic than only needs to run for a handful of local players each frame - has the breathing room to do its work.

Use the data orientation where it helps you do lots of stuff quickly, but don't let it be a wall that stops you from getting the gameplay behaviour you want.

\$\endgroup\$
  • \$\begingroup\$ So if I'm understanding this correctly, a transformation component stores 2 transforms, a local, and a final world-transform, then all I would need to do is to switch systems over to using the world-transform, and then a new system/mechanism to update the world-transforms every frame. Perhaps I can figure out a way to get that to work, though it will require significant work updating everything, including level serialization and entity inspection w/ tree traversal. Not to mention a messaging system to get these entities to work together \$\endgroup\$ – Yattabyte Aug 4 at 22:58
  • 1
    \$\begingroup\$ It's up to you how granular you want to divide your components. You could think of it as monolithic as one "Transform Component" including all these fields, or as granular as a "Local Position Component" and a "Local Orientation Component" and a "Transform Parent Component" and a "World Position Component"... Other optimizations you can consider are things like a dirty flag to iterate over only entities whose world transforms might have changed this frame and skip any stationary ones. Trickier to get contiguous/predictable access this way, but may be profitable if many objects are static. \$\endgroup\$ – DMGregory Aug 4 at 23:18

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.