Survival Shooter using Unity’s Entity Component System

gamevanilla
8 min readJan 2, 2019

--

Unity launched the first, experimental version of its new Entity Component System (ECS) during GDC 2018. This marked the beginning of a new era for the engine, which the company 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:

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.

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. 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:

  • PlayerInputSystem
  • PlayerMovementSystem
  • PlayerTurningSystem
  • PlayerShootingSystem
  • PlayerAnimationSystem
  • PlayerHealthSystem
  • EnemySpawnSystem
  • EnemyMovementSystem
  • EnemyAttackSystem
  • EnemyHealthSystem
  • EnemyDeathSystem
  • CameraFollowSystem

Hopefully, the names give a pretty clear indication of the responsibilities of these systems. In some cases, like with PlayerMovementSystem, 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:

ECS systems derive from the SystemBase class, and you are always required to use the partial keyword with these due to the code generation steps Unity performs internally with DOTS code.

Note how, similarly to traditional MonoBehaviours, there is an OnUpdate method that is responsible for the logic of the system. In this case, we iterate over the entities that have a Rigidbody component and a PlayerInputData component via the ForEach abstraction. The system provides us with many different overloads of this method, so that we can easily retrieve different combinations of the appropriate entities, classic components and ECS components. There is an additional degree of filtering over the player entities via .WithNone<DeadData>. This will make sure dead players cannot be moved.

The .WithoutBurst is needed because this system access Monobehaviour data, which means it cannot be compiled by the Burst compiler. I explain what the Burst compiler is later in the article.

As for the components, let’s take a look at the code of PlayerInputData as an example:

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 entity command buffers 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/Analysis/Entity Debugger), where you can inspect the state of individual entities and even selectively enable/disable specific systems:

Pretty straightforward! Can we still unlock some additional performance, though?

Introducing jobified systems

While we exclusively used systems that run on the main thread in the original project (and still need to do so with many systems that interact with classic MonoBehaviour-based components), we can already move some systems that only process ECS entities to jobified systems, which perform their work in a multithreaded way.

We also have what is known as chunk iteration as the ultimate, most powerful mechanism to iterate over entities, but it is a bit more complex to discuss in the context of an introductory ECS project so we will not use it here (I still recommend you learn about it, as it is the lower-level foundation the rest of the ECS is built upon).

So, how does this type of system look like in practice? Let’s look at a specific example named EnemyHealthSystem, which is responsible for updating the health of enemies that have been hit by the player:

The main difference between this system, which is jobified unlike the previous system we discussed, is that we use ScheduleParallel instead of Run. The system performs the following logic and automatically distributes it across the machine’s cores:

  • It resolves the actual damage over the enemy entity by updating its HealthData component.
  • It removes the DamagedData component to avoid processing the enemy entity more than once.
  • If the enemy’s health reaches a value of 0 or lower, it adds the DeadData component to it to kill the entity. A different system will take care of processing dead enemies.

Note how all this is done via a concurrent EntityCommandBuffer rather than your usual EntityManager (which does not work with jobs).

An interesting tidbit is that, in order to prevent adding the DeadData component to dead enemy entities more than once, we check if the entity in question has the component already by means of a ComponentDataFromEntity<DeadData> array, which gets passed into the job via the WithReadOnly method.

What is even cooler here is that this system is automatically compiled by the Burst compiler, which is a custom compiler built by Unity that accepts a high-performance-oriented subset of C# and makes use of auto-vectorization, optimized scheduling and better use of instruction-level parallelism to reduce data dependencies and stalls in the pipeline in the generated machine code.

I cannot stress enough how game-changing the whole approach to game development with Unity’s ECS, job system and Burst is. Attaining C++-like levels of performance (with the potential to do even better) without going through the pain of actually using the language? Writing highly-performant code while actually enjoying it at the same time? Well, sign me up!

New entity conversion workflow

The original project relied on GameObjectEntity to provide the glue between classic GameObjects and ECS entities, but it has been replaced with the new conversion workflow, which is a superior alternative.

The way the new workflow works is by attaching the ConvertToEntity MonoBehaviour to the GameObjects you want to convert to entities. Then, at scene loading time, Unity will automatically convert them (and you have the option to destroy the original GameObject or not). You can control the conversion process by implementing the IConvertGameObjectToEntity interface. As a practical example, this is what we do for the player GameObject:

FixedUpdate systems

One important thing to note is that, in the original project from Unity, there was some code running in FixedUpdate (namely, the player movement and the camera follow code). In order to run the equivalent ECS systems in FixedUpdate too, we need to add the DisableAutoCreation attribute to them and invoke their update method manually from FixedUpdate. An easy way to do so is via a helper MonoBehaviour:

It is expected that, as the ECS matures, we will have a more expressive way to define how systems are updated.

You can find the complete, updated Survival Shooter project in this GitHub repository. The project requires Unity 2021.3.7 and the ECS-specific code lives in the ECS folder.

What next?

Even with all these updates, there is still a ton of code in the project that depends on classic, MonoBehaviour-based components (mainly everything related to rendering, physics and animations). While we could certainly write our own custom systems for these areas, in the context of this demo I would like to keep everything using the built-in ECS functionality.

Moving forward, more and more core engine systems will be available to use from the ECS. 2019 has just started and I cannot wait to keep on updating our Survival Shooter project as that progress happens.

--

--

gamevanilla

We are an independent game studio specialized in crafting original games and game development middleware. You can find us at www.gamevanilla.com.