Ollie Balaam is a QA Engineer at Improbable. In this, the second in a series of posts, he’ll compare the ECS and MonoBehaviours workflows in the context of the SpatialOS Game Development Kit (GDK) for Unity and what they can do for your game.
As we explained in our first blog about Unity’s Entity Component System, the GDK offers two workflow options:
- A MonoBehaviour-centric workflow that works with Unity’s fully developed MonoBehaviour tooling, workflow and APIs.
- An ECS workflow that takes full advantage of the new ECS development paradigm and associated performance improvements.
The SpatialOS Game Development Kit (GDK) for Unity is built on this ECS. But, before we get into the detail of that, it’s important to remember that these workflows are not mutually exclusive. Most Unity game developers working with SpatialOS get started using MonoBehaviour-centric workflow, and then optimise their game with the ECS workflow. Whichever approach you choose, your game will still take advantage of the GDK core module that we’ve written using the ECS.
The benefits of ECS
In the previous post, we outlined the main performance benefits of ECS. Let’s look in detail at how each of these performance benefits is achieved.
“Code generated by ECS performs significantly fewer jumps between memory addresses than code generated by MonoBehaviours.”
You can avoid jumping between memory addresses by storing the bytes you want to operate on contiguously in the same chunk of memory. As a case study in how Unity’s ECS induces a memory layout that achieves this, let’s imagine a health system that subtracts the value of a damage component from a health component every update loop:
Unity’s ECS categorises every unique combination of component types that appear in an entity into archetypes. The ECS dynamically defines archetypes at runtime so it can categorise new combinations of components as they appear. One unique combination that will be defined as an archetype is health and damage. Another is, for example, health, damage and mana.
Once categorised as an archetype, all health and damage components that exist on entities with no other components will be stored in the same chunk. Similarly, all isolated instances of health, damage and mana will be stored in one chunk. The only exception to this is if there are more entities than a chunk can hold, in which case they will overflow into the next chunk.
When the ECS iterates over components, the memory access of components within a chunk is always completely linear. This results in all of the components that our health system needs to operate on being stored in a relatively small number of chunks, and being stored contiguously within those chunks. This necessitates fewer memory jumps.
“Additionally, the memory layout induced by the ECS allows for further code optimisations that leverage cache prefetching”
Chunks generated by Unity’s ECS contain tightly packed and contiguous data. This layout, as opposed to the sparsely allocated heap memory generated by the MonoBehaviour-centric workflow, makes it easier for the CPU’s hardware prefetcher to predict and fetch the right objects in anticipation of needing them.
Unity’s ECS is able to induce this memory layout because components are represented in memory as blittable types. Blittable types are data types that have an identical presentation (meaning they take up the same number of bytes) in memory for both managed and unmanaged code. Using these, the ECS can write blittable types into a chunk until that chunk is full, then move immediately onto the next, tightly packing each one.
“By decoupling data (entities and components) and logic (systems), the ECS also produces more modular, easy-to-read code.”
The structure of Unity’s ECS encourages a cleaner codebase. Its modular design leads you to write modular code that’s easy-to-read, reuse and refactor. As well as being easy to maintain, this conceptually mirrors the core concepts of SpatialOS (SpatialOS entities, SpatialOS components, and the workers that operate on them), making it a perfect fit for our GDK.
The diagram below portrays an ECS world. A world is a set of entities and systems; an entity is a set of components, and systems act in bulk on entities and their components.
A world constructed using Unity’s ECS.
The snippet below is a very basic example of an ECS component. You can see more complex examples here.
public struct MyComponent : IComponentData
public int myValue;
The current drawbacks of ECS
Unity’s ECS replaces GameObjects with entities and MonoBehaviours with a combination of components (to store data) and systems (to operate on that data). It also does away with the concept of Scenes in favour of worlds, which are much more lightweight, consisting of a set of entities, and a set of systems that operate on those entities.
While these changes unlock the benefits listed above, Unity is currently designed with GameObject and MonoBehaviour-centric workflow in mind. This means numerous features that used to work out of the box (physics, audio and rendering systems) require more engineering to work if you’re only using ECS. This will be the case until Unity adapts or replaces these systems for use with ECS, which they are committed to doing.
This is why we’ve built the GDK to support both workflows.
Our 200-player FPS Starter Project was written exclusively using the MonoBehaviour-centric workflow.
The MonoBehaviour-centric workflow
The main benefit of the MonoBehaviour-centric workflow is that, as a Unity developer, you already know how to use it. You can use all of the tools, concepts and built-in systems (e.g: physics, audio and rendering) that you’re used to.
The main drawback of the MonoBehaviour-centric workflow is that the code it produces is outperformed by ECS code. That said, all games build on the GDK utilize the GDK core module, which we’ve written using the ECS. This means that you’ll enjoy some ECS induced performance gains even if you don’t write any ECS code yourself.
The GDK also comes with a number of feature modules. Feature modules are optional features that you can choose to include or exclude from your game (player lifecycle, for example). They are intended both to give you a head-start in the development of your game, and act as reference material for best practices to use when writing your own features. Feature modules are written using whichever workflow is best suited for that particular feature, and like all of the GDK, the code is publicly available and fully customisable to meet the exact needs of your game.
You can make previously impossible games with the MonoBehaviours on the GDK, as evidenced by our 200-player FPS Starter Project, which was written exclusively using the MonoBehaviour-centric workflow.
Here are some resources to get you started with Unity’s ECS and Improbable’s SpatialOS GDK for Unity.