I adapted elements of a high-performance c++ game ‘Particle Planet’ that I made while on the Physical Sky team to the alpha state Entity Component System for Unity.
Read on for a high level review of the concepts and strengths of the system, as well as the current state of the API.
The bleeding-edge Entity Component System is a new way to create high-performance applications in Unity. Its a set of extensions to Unity that support and encourage developers to write code that can exploit multiple processor cores more easily, and ultimately upgrade and reuse behavioral code more easily.
A classic Unity application is composed of scripts attached to Game Objects. The code and the data for any given Game Object are effectively tied together. This is a simple metaphor for a developer to follow, but for many games and simulations, it means a lot of waste because the data is scattered around heap memory, so it is not fast to access, nor can the processor optimize the access. Enter ECS to solve these issues.
The ECS achieves its remarkable performance gains with a number elements that work together – the ECS architectural pattern, the C# Jobs System, and the Burst Compiler.
Firstly, ECS – the architectural foundation. Unity is not the originator of the Entity Component System concept – it’s been around for about a decade. In an ECS implementation, each entity is associated with some data in components, which are worked upon by common shared systems for any given aspect of behavior. So, one benefit is that the behavior for any element in game is composed from necessary systems and the data components required by those systems. This compositional design choice means that the systems can be swapped out and upgraded and reused with relative ease, compared to a classic inheritance approach. The other benefit is that, due to the process of marshalling that data into managed lists, it has been rendered into a format that SIMD hardware can take advantage of. Additionally, it is not scattered across the heap.
Next, the Jobs System provides interfaces that make it easier to write multi-threaded code. While it can be used separately from ECS, Unity’s ECS relies on it. And finally, the Burst compiler compiles the Job System code into highly efficient native code. The result is, as the folks at Unity like to say, “performance by default”.
The block challenge.
I decided to create a block simulation similar to the one we created for Particle Planet. Particle Planet’s simulation was written in C++ back in 2005, and could handle 10,000 blocks without trouble on a run of the mill system. The code was not written in a multithreaded fashion, but our approach was to keep the block data very regular ( in spite of the profusion of block behaviors ), and to process it by a common simulation system, which the goal of superior performance. I wouldn’t try to replicate all of the functionality, but wanted to see how a modern system with a modern approach would handle the blocks – how performant would it be? And just as important, how easy to code and maintain?
My ECS simulation includes, for each of the entities, definitions of the position, rotation, color, and velocity – the components. These components must use structs of value types, or special job-system safe types in order to be handled by the job system. They contain no logic. Furthermore, Unity’s ECS implementation includes a concept of archetypes. These are groupings of components into contiguous 16k memory chunks. They allow the system to keep needed data organized and close together for rapid access. I created an archetype for the blocks.
Next, I created the systems. These included:
- a creation system, responsible for adding blocks to the world.
- a velocity calculation system, which queries the world and internal block state to determine velocity for the next timestep.
- a movement system, which applies the calculated velocitites.
- a destruction system, responsible for removing blocks from the world which are flagged for destruction.
- a rendering system, which used a common renderer to draw the blocks.
Learning to think in the ECS way was not so difficult since we had already been adopting some of the practices and patterns in our particle game. Some important patterns were familiar:
- certain systems must be executed in particular order to prevent unpredictable states – e.g. collision testing on all entities must inform velocities first, then actual movement can be applied to all entities.
- addition and removal of entities must be managed at specific times to prevent reference errors.
- creating global systems to handle behaviors made it easy to apply particular preexisting functionality to new block types.
A block wall.
It was easy enough to spawn 100, or even 200 thousand blocks with excellent performance on my I7 8GB machine. The mesh rendering for the cubes was instanced, and causing the blocks to move in a given direction was easy enough. However, it was not entirely smooth sailing. When I attempted to recreate a particular ‘particle’ from Particle Planet – the ‘gas block’ – I ran into my first challenge.
As it turns out, Unity’s random function is not threadsafe. Fortunately, the ECS framework gave clear indication that this was causing issues. I initially looked into a lightweight randomizer class that I could create instances of for each thread, but it was subject to repetition over so many blocks, and seemed to introduce a drift in the particles. I considered precalculating random numbers in a non-threaded loop, but decided to dig a little more.
Fortunately, one of Unity’s strengths is its active community. The ECS forum is a pretty lively place, and I was quickly able to find a nice implementation example of a per-thread randomizer that would work with the ECS. My particles were now zipping around merrily and randomly.
A maturing system.
Unity’s goal seems to be to integrate the ECS into the IDE and API sufficiently so it becomes an alternative to the classic Script-Component-on-Game-Object way of doing things. In its current early state, there are still a lot of basics being worked out – how can I change color of an instanced block using the rendering component that Unity provides? What is the correct way to reference large global data structures from individual entities? The good news is that the upsides of the ECS approach are significant enough to encourage a very active early adopter base. These people are driving the design of the system, creating libraries of patterns, and smoking out all of the architectural weaknesses. And Unity appears to be fully behind this effort. I think ECS has got a great future.