Skip to content

Component Best Practices

MaverickAl edited this page May 31, 2018 · 4 revisions

This document describes some of the best practices to bear in mind when writing systems or designing components.

Systems are verbs, components are nouns

A common mistake when you first start writing components is to think in terms of the "object on the screen", and make a component and a system named after it, which simulates the entirety of the behaviour of that object.

A simple way to avoid this mistake is to think of systems as performing an action (thus, verbs), and components as adding properties and data to an entity (thus nouns, or in some cases adjectives). Sometimes there is a one-to-one mapping between components and systems, but often there isn't. Systems may use multiple components to achieve their purpose, and a single component is likely to be used by multiple systems. For this reason, there is no need to give systems and components the same name, and in fact in some cases if is detrimental as it discourages the flexible reuse the component system is intended to provide.

An example of this principle in action is the extremely simple MeasureDistance system. Here the verb is "measure": it calculates the distance travelled over the course of a frame from the data in the PhysicsComponent, and adds it to the DistanceTravelled component. DistanceTravelled is then used by a number of other systems which base their behaviour on the total distance travelled by an entity (which may be a projectile, a car, or whatever) over the course of its lifetime.

It is definitely possible to waste time thinking of names for things, but a little bit of consideration can help guide you to a more flexible, reusable design.

One system, one responsibility

This is closely related to the point above about naming. A single system should have a single, clearly-defined responsibility. It should use the minimum number of components required to achieve its task, preferably with zero or few Optional components hiding parts of its runtime in an if(....Exists()) clause.

The so-called single responsibility principle is common to all programming paradigms. Classes in object-oriented code, functions and typeclasses in functional code, and systems in component-oriented code should all be designed with this in mind. It is particularly important for systems, however, because their execution is based on the presence or absence of their Required components. The more bloated a system gets, the more components it requires, and the more fragile it becomes in the face of components being added or removed from a certain class of entities.

What do I mean by "fragile"? Well, one of the weaknesses of our component-oriented design is as follows: you have a system with many required components and a number of responsibilities, running on many kinds of entities in the world -- say cars, missiles, and planes for example. You get a bug report describing a problem in the behaviour of cars in the game, and you track it down to this system. The solution is to add another required component which you know all the cars have, and make use of it in the Run() function to fix the bug. You make the change, test it, and it works, so you mark the bug "fixed". Little do you realise that planes, which don't even exist in the level you were testing, don't have this component, and so you have unwittingly disabled the system for all planes in the game!

Desigining systems such that they have a single responsibility and act on few components can help mitigate this problem by reducing the surface area impact of entities affected by a particular system.

Even when the behaviour required is complex, it is usually best to achieve it by composing together multiple simple systems rather than cramming tonnes of functionality into one big system that does everything.

Don't call GetComponent() within a signal, or at all if possible

This is probably the longest-standing tip in this document, but it still happens occasionally so it bears repeating: GetComponent() is potentially slow, increases code fragility, and breaks in multi-threaded environments. We are hoping to refactor the component system so that it is impossible to call this function within a signal, and possibly to make declared inputs and outputs the only way to get access to components. Use of GetComponent() in new code being written today takes us further away from that goal and prevents us from making significant improvements to the component system as a whole, and it should be avoided at all costs.

In fairness to some of the current uses of GetComponent() in the codebase, there was a time, before we had the hierarchy and events systems we have now, where it was unavoidable. That time is long past, and introducing new calls to GetComponent() in signals code at this stage is unacceptable.

Avoid manual iteration and arrays in components

Sometimes, when writing a system, you may be tempted to add a component which has an array of some data or resource which you will iterate over in the system code. This is often a sign of the design being "the wrong way round", so if you notice this it's an opportunity to rethink your design and consider whether there's another approach which would take better advantage of the facilities the component system provides.

The component system is, at its core, a sort of iteration engine. You set up a collection of self-contained entities in a hierarchy, and the component system will handle iterating over them for you, allowing you to concentrate on the actual functionality of those entities. As we improve the component system and add support for multithreading, your entities will take advantage of those improvements automatically -- so long as you leave the mechanics of the iteration up to the framework.