-
Notifications
You must be signed in to change notification settings - Fork 8
Component Entity System
Usagi is built on a Hierarchical Component-Entity-System architecture. This is based on the popular CES design which has taken the games industry by a storm over the last ten years or so, but with some of our own innovations which come out of arranging our Entities in a hierarchy which helps to define the flow of information, and allows us to make a number of thread-safe optimisations.
This document is intended to be a living document, as many aspects of the design are prone to change. There are also areas where the implementation is incomplete, so there may be statements which are true in theory, but aren't necessarily true in practice yet. Such comments are highlighted like this[^1], with a footnote explaining how reality differs.
There has been a general move toward component-based design in the games industry for a while, although people's interpretation of that differs from place to place. We have adopted quite a "pure" design, whereas others (notably Unity) have taken an approach somewhat closer to traditional object-oriented programming.
- Evolve Your Hierarchy This is a good introduction to CES systems, by a guy who retrofitted one into the Tony Hawks games.
- Entity Systems are the Future A pretty in-depth series of articles on why Component-Entity-Systems are good, with some ideas about how to implement them.
- Role of systems in entity systems architecture The accepted answer for this question gives a really easy-to-understand analogy for how systems work out whether they should run or not based on what components are associated with a particular entity. The description given here is the closest I've found to the way that our CES architecture works.
Traditional component entity systems were popularized in the PS3 era, when work had to be distributed between all of the SPUs with jobs. Splitting work in this manner was efficient, but having worked with them they did have a number of shortcomings in terms of creating complex code, and the amount of manual work that was required to manage them; if as a small team we were going to down this road we wanted solutions to both of these issues.
Most examples of entity systems perform single 1:1 tasks (like updating particle systems), however in our experience most game code (both engine and gameplay) is one-to-many.
By introducing a hierarchy we found that we were did not need special case code for interactions between entities. Although systems only act on components belonging to a single entity they can read from components of parent entities (whose update of that system is guaranteed to be finished first).
We also introduced messages between systems to handle special events, allowing systems attached to one entity to trigger signals on completely different entities. Between these two abilities complex interactions were made possible without negatively impacting performance.
To avoid the amount of manual work setting up a system we created a design where parallelization candidates could be automatically chosen by looking at overlap of inputs and outputs, as well as by splitting the hierarchy tree. By enforcing a rule that no system could act on components other than in a single entity, and only read from entities directly above it in the tree we ensured that all branches of the tree could be safely updated in parallel. A single priority value is used to ensure the correct ordering where dependencies exist.
As this the most unique aspect of Usagi we would eventually like to decouple the systems code into a separate library and respository.
Our hierarchy is a tree of entities, going from the root which contains world information right down to each characters finger bones (and even down to the gun nozzle if they are holding one).
So what does the hierarchy look like.
(Please forgive the quick doodle)
That hierarchy drives everything about code design in Usagi.
Although entities often represent objects and bones in the world they do not have to do so, it is perfectly acceptable (and indeed one of the design principles) to have entities in the tree which are only logically part of the game and which do not represent any physical object or sub-object.
One of the constraints we impose for simplicity is to only allow one component of each type per entity, but what if you wanted to update multiple UV sets at different speeds? You can attach a UV modifier to a seperate child entity (they will message the first parent entity with a model component with the new values).
Here are some of the things we are trying to achieve with the design. Some of these things fall naturally out of the component-based idea, others are the result of our specific implementation choices, and haven't all been fully realised at the time of writing.
-
Separation of code and data
This improves compositionality and reuse. Both the data can be reused (by various systems which make use of some, but not all, of an entity's components), and the code can be reused (the same system will run on a variety of different entity types, provided they have the required components).
-
Data-pipeline processing
The various systems form a pipeline, not dissimilar to the way shaders are constructed. This should result in better cache use (the smaller the number of components required by a system the better), and easier parallelisation. In theory, it should be possible to double-buffer the components, meaning we could run the systems in parallel safely without having to use mutex locks everywhere (in practice we haven't tried this yet).
-
Explicit specification of state
Because input and output components are specified as part of the system's definition, it is easy to see at a glance what part of an entity's state might be modified by a particular system.
-
Extremely lightweight components, entities, and systems[^2]
It should be possible to have thousands of entities, each constructed of however many components they need. Both in terms of syntax (how easy it is for the programmer), and runtime cost, adding a new component or breaking an existing component into two shouldn't feel like a "big decision". As a rule of thumb, adding a new system or a new component type should feel no more annoying than adding a new class does when writing object-oriented C++ code.
When making design choices, it is useful to frame the tradeoffs in terms of the above four pillars.
A Component-Entity-System architecture is named after the three parts that make it up:
-
Components are pure data -- think of them in terms of a plain C struct.
- They should be composed, as far as possible, of simple "POD" data types, such as
int
,float
, and so on. - Because we make games, we consider types like
Vector3f
andMatrix4x4
to be "simple", even though they aren't strictly speaking POD, because they are so fundamental. - We can also add
RuntimeData
to components, which allows us to hold handles to resources, such as models and layouts. Importantly, though, they don't actually manage the resource itself; they just store a handle to it so that systems can make modifications to that resource. In a "pure" architecture, these handles would just be IDs, and therefore POD types. The runtime overhead for that is a little bit excessive for our purposes, though, so we allow the use of pointers and trust the programmer not to do anything too wild with them.
- They should be composed, as far as possible, of simple "POD" data types, such as
-
Systems are pure functionality -- think of them as a (collection of) C function(s), with some metadata about the inputs and outputs to those functions.
- They will automatically get run for any entity that has all the requisite components.
- Since all their members are static, it is impossible to store any data in them. Instead, you must store any data you need in components and specify the component as an input or an output for the system.
-
Entities are just an ID which associates a collection of components with each other.
- In a "pure" entity system this would just be an
int
or similar. - We have an Entity class, which stores information about what components it has and what systems will run on it, as an optimisation. As a user, though, it will help to ignore all that and just pretend it's an ID number.
- In our implementation, an entity is allowed at most one of a particular component. When you want to have multiple of the same component -- for example four wheels on a car -- you have to break that up into different entities; so in the previous example you would have an entity representing the body of the car and four separate entities representing the wheels of the car. You would then make the wheel child entities of the car.
- In a "pure" entity system this would just be an
We have augmented this with some extra concepts:
- Entities are arranged into a hierarchy, where each Entity has 0 or 1 parents and 0 or greater children.
- Systems declare three sets of components: Inputs, Outputs, and ColliderInputs. Inputs and ColliderInputs are read-only; Outputs are write-only[^3].
- Components can either be Required or Optional for a particular System. Systems with optional components will get run even on entities which don't have those components. You can test whether the component was found or not using
Exists()
, and then access the component usingForce()
. - Inputs can be retrieved FromSelf (the default), FromParents, FromSelfOrParents, or FromParentWith< SomeComponent >, where
SomeComponent
is a component owned by one of your parents. Outputs can only be retrieved FromSelf -- you can only write to your own components. - ColliderInputs are only used in
OnCollision
and are taken from the entity you collided with. As with Inputs, they can be retrieved from the entity itself or any of its parents, but not its children.
- Components can either be Required or Optional for a particular System. Systems with optional components will get run even on entities which don't have those components. You can test whether the component was found or not using
- Systems implement a number of Signals, which is the mechanism by which code gets executed.
-
Run()
is the main update signal, triggered once per frame. -
OnCollision()
is triggered when there is a collision. -
OnEvent< EventType >()
is a templated signal, one for every kind of event. Events form the main way of communicating across or up the hierarchy, since we can only directly read components from our parents. -
LateUpdate()
is run at the end of the frame, afterOnCollision
andOnEvent
. -
GPUUpdate()
is run outside of the main game loop, at a time when it is safe to write to the GPU.
-
[^1]: In the colour of rose-tinted glasses.
[^2]: While Components and Systems are pretty lightweight in our current implementation, Entities are not. Each Entity contains a sparse array of both the Components attached to it and the Systems that can run on it, which is hugely space-inefficient (most of the entries in both arrays are NULL
)
[^3]: In fact, Outputs are read-write, as there is no way in C++ to make a variable write-only. Reading from an Output component is discouraged, however.