Unity launched the first, experimental version of its new Entity Component System (ECS) last week during GDC. This marks the beginning of a new era for the engine, which the company has labeled with the motto “performance by default”. What does that mean?
If you are familiar with Unity, you probably know that Game Objects are somewhat heavy and it is usually not a good idea to have too many of them. That is why standard performance guidelines commonly revolve around doing things like:
- Using object pools in order to reduce their instantiation cost.
- Using manager-like objects in order to reduce their processing cost.
If you think about it, the main issue with the Game Object paradigm is that it forces us to think about independent, isolated objects with their own Update methods. Which may be convenient, in a traditional OOP-ish kind of way, but it is definitely not the most friendly approach to achieving great performance on current hardware.
The key to achieving optimal performance on a computer of today consists on processing objects of the same type in batches. This makes it possible to keep most of the processed data in the cache, which is several orders of magnitude faster than accessing the main memory. Console game programmers have been using data-oriented approaches for years now, so it only made sense for Unity to provide the means to write this kind of code at some point.
If you are anything like me, I tend to learn new systems better by working through an actual example. So I took it upon myself to rewrite the famous Survival Shooter tutorial with the new ECS system. Please note we will be using the so-called “hybrid” mode, which is a convenient middle ground between the Game Object paradigm and the full-blown, “pure” ECS mode. It is currently pretty hard to write a complete game using only the pure ECS mode, because there are still many engine areas (graphics, physics, animations, etc.) with no direct support for it, which means you need to write the equivalent code yourself. Unity is currently working on providing access to more and more engine systems from the ECS in the future.
Setting up the ECS
In order to use the ECS, you first need to download the special Unity 2018.1 beta 12 build available here (please note the beta 12 you can find in the usual beta channels will not work).
Once the beta is installed on your machine, you need to set the Scripting Runtime Version to Stable in your Player Settings (this will force a restart of the editor):
Next, you need to edit the manifest.json file located in the Packages folder of your project so that it looks like this:
This will make the experimental ECS library available in your project.
UPDATE (06/13/2018): I have just upgraded the project to Unity 2018.1.0f2 and you do not need to edit the manifest.json file manually yourself anymore; you simply need to open the Package Manager (located in the Window/Package Manager menu) and download the Entities and IncrementalCompiler packages.
Re-thinking the Survival Shooter tutorial in a ECS world
The Survival Shooter tutorial is a perfect example of how you can use Unity today to make a small-yet-quite-complete game, so I figured it would make for an excellent choice to dive into the new way of thinking and programming the ECS entails. As the focus is on the new system, I am not going to go into any detail on how the environment and the prefabs in the project are set up, as this is very well explained in the official videos from Unity.
You can find the GitHub repository of the project here. The ECS-specific resources live in the HybridECS folder.
The ECS revolves around three main concepts:
- Entity: You can think of them as lightweight Game Objects. In fact, they are only identifiers and they do not store any data of their own.
- Component: Components can be added to entities, similarly to how components can be added to Game Objects.
- System: Systems define the behavior of your game. They process entities and their components (and can also process Game Objects and their components, to provide the glue between the Game Object world and the ECS world).
I really like the fact that Unity has settled on a very orthodox ECS paradigm, where entities are not containers but simply ids. It is one of the key aspects to achieving optimal performance, because an independent Entity Manager is the one responsible for managing the memory in the most efficient way.
Earlier on, I mentioned that I was using the “hybrid” mode of the ECS. What this means in practice is that all the entities in the game still have a backing Game Object (with the Game Object Entity component attached to it). I was initially planning on using the “pure” mode, where we have only entities without any Game Objects backing them up. But I quickly realized that I would need to implement my own Rigidbody and Animator equivalents in the ECS world, so “hybrid” mode it was. An interesting side note: the Nordeus technical demo works around this by baking the animation data into textures.
With the basic theory covered, let’s move on to writing some actual code with this new system. If you take a look at the Survival Shooter project’s original code, you can see scripts like PlayerMovement attached to the player prefab or EnemyAttack attached to the enemy prefabs. Transitioning from the Game Object paradigm to the ECS means thinking about systems acting on batches of entities/components of the same type. Following the previous examples, we would have a PlayerMovementSystem acting on player entities or an EnemyAttackSystem acting on both player and enemy entities. In the same spirit, I devised the following systems for Survival Shooter:
Hopefully, the names give a pretty clear indication of the responsibilities of these systems. In some cases, like with PlayerMovement, the original script performed several tasks at once (moving, turning and animating the player) and I divided them into independent systems. It actually felt very natural to do so; there is a unique sense of elegance to ECS in that it forces you to think very clearly about the systems that compose your game, their inputs and outputs, and how they relate to each other. It almost reminds me of the beauty of programming procedurally with plain C or functionally with Scheme. The fact that your code is almost guaranteed to run faster than the object-oriented equivalent only adds to the joy.
But, what does the actual code of a system look like? Let’s take a look at the PlayerMovementSystem as an example:
public class PlayerMovementSystem : ComponentSystem
public struct Data
public int Length;
public GameObjectArray GameObject;
public ComponentArray<Rigidbody> Rigidbody;
[ReadOnly] public ComponentDataArray<PlayerInput> PlayerInput;
public SubtractiveComponent<Dead> Dead;
[Inject] private Data data;
protected override void OnUpdate()
var speed = SurvivalShooterBootstrap.Settings.PlayerMoveSpeed;
var dt = Time.deltaTime;
for (var i = 0; i < data.Length; i++)
var move = data.PlayerInput[i].Move;
var movement = new Vector3(move.x, 0, move.y);
movement = movement.normalized * speed * dt;
var position = data.GameObject[i].transform.position;
var rigidbody = data.Rigidbody[i];
var newPos = new Vector3(position.x, position.y, position.z) + movement;
As you can see:
- Systems derive from the ComponentSystem class and they override its OnUpdate method to perform their logic.
- The system will update all the entities with a matching set of components defined in the inner Data struct (the Inject attribute does all the filtering magic).
- You use ComponentArray for specifying “Game Object components” and ComponentDataArray for specifying “ECS components”.
- The ReadOnly attribute allows you to mark certain components as read-only, which may result in additional performance.
- You can use SubtractiveComponent to specify components that the entities should NOT have in order to be processed by the system. This is particularly useful in certain contexts; for example, in the PlayerMovementSystem the player is automatically ignored when he is dead without the need for conditionals or extra removal logic.
- As entities are processed in batches inside the OnUpdate‘s loop, you can easily do optimizations like moving variables outside the loop.
As for the components, let’s take a look at the code of PlayerInput as an example:
public struct PlayerInput : IComponentData
public float2 Move;
As you can see:
- Components are plain structs that derive from IComponentData.
- We leverage Unity’s new math library, built from the ground up to be highly performant.
Some additional observations after writing the entire code for Survival Shooter:
- You can use “tag-only” components that simply act as helpers to filter components inside systems. I have Player and Enemy components for differentiating between those two in certain systems (I had some funny bugs initially where some systems that should be player-only were also being applied to the enemies).
- You can use components as events. I do this when a player or an enemy is damaged by adding a Damaged component to the respective entity and letting another system (that processes Damaged entities) take care of it.
- Certain systems make use of PostUpdateCommands to queue things like adding or removing components so that they happen after the system’s inner loop has completed.
You can debug the state of the ECS at runtime via the Entity Debugger (located in Window/Entity Debugger), where you can inspect the state of individual entities and even selectively enable/disable specific systems:
I love the direction Unity is going with the ECS and cannot wait for further developments on it; it is pretty clear to me the team knows very well what they are doing and the current preview is really promising already. Would be very nice to write a “pure” Survival Shooter in the future in order to untap that additional level of performance!
If you have any feedback, you can reach me at @_davidpol.
Unity talks at GDC18