Search Unity

We’re rebuilding the core of Unity with our Data-Oriented Tech Stack, and many game studios are already seeing massive performance wins when using the Entity Component System (ECS), the C# Job System, and the Burst Compiler. At Unite Copenhagen, we had a chance to sit down with Far North Entertainment and go deep into how they implemented these DOTS features into their otherwise traditional Unity project.

Far North Entertainment is a Swedish studio co-owned by five friends from engineering studies. Since releasing Down to Dungeon for Gear VR in early 2018, the company’s been working on a game that explores a classic PC game genre – the post-apocalyptic zombie survival game. What makes the project stand out is the number of zombies chasing you. The team’s vision included thousands of brain-hungry enemies coming after you in enormous hordes.

However, they quickly run into lots of performance issues when prototyping this idea. Spawning, despawning, updating, and animating all those zombies was a major bottleneck, even after the team tried implementing possible solutions like object pooling and animation instancing.

This led the studio’s CTO Anders Eriksson to look at DOTS, and how to change his mindset from an object-oriented mindset to a data-oriented one. “The key insights that helped us make the shift was to stop thinking about objects and object hierarchies, and start to think about the data, how it is transformed and how it is accessed,” he says. That means that the code doesn’t have to be modeled around something that makes sense in real life and it doesn’t have to solve the most general case. He’s got a lot of advice for anyone trying to make the same shift:

“Ask yourself what the actual problem is that you are trying to solve, and what data is relevant for a specific solution. Will you do the same transformations of the same set of data over and over again? How much relevant data can you pack into a CPU cache line? If you are also looking to convert existing code, then identify how much garbage data you are filling the cache lines with. Can you split up the calculations on multiple threads, and/or utilize SIMD instructions?”

The team came to understand that entities in Unity Component System are just lookup ids into streams of components. Components are just data, while the systems contain all logic and filter out entities with a certain component signature known as Archetypes. “I think one insight that helped us to visualize this was to think of ECS as an SQL database. Each Archetype is a table where each column is a component and each row is a unique entity. You then use the systems to query into these archetype tables to do operations on entities,” says Anders.

Getting started with DOTS

To get to this understanding, he studied the Entity Component System documentation, the ECS Samples and the sample that we created with Nordeus and unveiled at Unite Austin. But general materials about data-oriented design were also extremely helpful to the team. “The talk from Mike Acton about Data-oriented design from CppCon 2014 is what initially opened our eyes to this way of programming.”

The Far North team posted what they’ve learned on their Dev Blog and this September, came to Copenhagen to talk about their experiences switching to the data-oriented mindset at Unite.

This blog post builds on this presentation and explains the specifics of their implementation of ECS, the C# Job System, and the Burst Compiler in more detail. The Far North team has also kindly shared a lot of code examples from their project.

Lining up zombie data

“The problem we faced was with doing client-side interpolation of translations and rotations for thousands of entities,” says Anders. Their first object-oriented approach was to make an abstraction of a ZombieView script which inherited a more general EntityView parent class. An EntityView is a MonoBehaviour attached to a GameObject. It acts as a visual representation of the game model. Every ZombieView was responsible for handling its own translation and rotation interpolation in its Update function.

This seems fine until you realize that every entity is allocated in a random place in memory. That means if you’re accessing thousands of entities, the CPU has to catch them one by one in memory, which is very slow. If you lay your data in neat continuous blocks, the CPU can cache a whole bunch of them at once. Most CPUs today can get around 128 or 256 bits per cycle from a cache.

The team decided to convert the enemies to DOTS with the hope to eliminate client-side performance bottlenecks. First in line was the Update function of the ZombieView. The team identified what parts of it should be separated into different systems, and what data would be needed. The first and most obvious thing was the interpolation of positions and rotations since the world of the game is a 2D grid.  Two floats represent where the zombie is heading, and the final component was a target position component that keeps track of the server position for the enemy.

Next up was creating the archetype for the enemies. An Archetype is simply a set of components that belong to a certain entity, in other words, a component signature.

The project uses prefabs for defining archetypes since there are more components needed for the enemies and some of them need references to GameObjects. The way this works is that you can wrap your component data in a ComponentDataProxy which will turn it into a MonoBehaviour that can be attached to a prefab. When you call instantiate with the EntityManager and pass a prefab it will create an entity with all component data that were attached to the prefab. All component data are stored in 16kb chunks of memory called ArchetypeChunks.

Here is a visualization of how the component streams in our archetype chunk will be organized, so far:

“One of the main advantages of archetype chunks is that you often don’t have to do new heap allocations when creating new entities since the memory has been allocated upfront This means that the creation of entities often just means writing data to the end of the component streams inside the archetype chunks. The only time a new heap allocation needs to be done is when you create entities that won’t fit within the chunk boundary. This will either trigger a new archetype chunk of 16kb to be allocated or if there is an empty chunk of the same archetype, it can be reused. The data for the new entities will then be written to the component streams of the new chunk,” explains Anders.

Multi-threading your zombies

So now that the data was tightly packed and laid out in a cache-friendly way in memory, the team could easily take advantage of the C# Job System to run their code in parallel on multiple CPU cores.

The next step was creating a System that filtered out all entities from all archetype chunks that have a PositionData2D, HeadingData2D and TargetPositionData component.

To do this Anders and his team created a JobComponentSystem and constructed their query in the OnCreate function. It looks something like this:

This declares that there is a query that filters out all entities in the world that have a position, heading and target. What they aimed to do next is to schedule jobs each frame via the C# Job System in order to distribute the calculations over multiple worker threads.

“The great thing about the C# Job System is that it is the same system that Unity uses under the hood in its code, so we didn’t have to worry about execution threads halting each other by claiming the same CPU cores and causing performance issues,” says Anders.

The team has chosen to use IJobChunk because thousands of enemies meant having a lot of Archetype chunks that will match their query during runtime. IJobChunk distributes the right chunks over the different worker threads.

Every frame, a new job called UpdatePositionAndHeadingJob is responsible for handling the interpolation of the positions and rotations of enemies in the game.

The code for scheduling the jobs looks like this:

Here is the declaration of the job:

So when a worker thread has pulled a job from its queue it will call the execute kernel of that job.

Here is what the execute kernel looks like:

“You might notice that we use selects instead of branches and the reason for this is to avoid something called branch misprediction. The select function will evaluate both expressions and choose the one that matches the condition, so if your expressions are not that heavy to calculate, I would recommend using select since it is often cheaper than having to wait for the CPU to recover from a branch misprediction,” Anders points out.

Bursting with performance

The last step of the DOTS transformation for the enemy position and heading interpolation was to enable the Burst Compiler. Anders found this quite easy: “Since the data is laid out in contiguous arrays, and since we are using the new Mathematics library from Unity, all we had to do was to add the BurstCompile attribute to our Job”.

The Burst Compiler gives us Single Instruction Multiple Data (SIMD); machine instructions that can operate on multiple sets of input data and produce multiple sets of output data in a single instruction. That helps us fill more seats on the 128-bit cache bus with the right data.

The Burst Compiler in combination with a cache-friendly data layout and the job system gave the team enormous speedups. Here is a performance table they put together after measuring the difference after each step of the transformation.

This meant that Far North got completely rid of the bottlenecks related to the client-side interpolation of position and heading of zombies. Their data is now laid out in a cache-friendly way and their cache lines are populated with only relevant data. All cores of the CPU get a workout and the BurstCompiler outputs highly optimized machine code with SIMD instructions.

DOTS tips and tricks from Far North Entertainment

  • Start thinking in streams of data, since in ECS, entities are just lookup indices into parallel streams of component data.
  • Think of ECS as relational Database where Archetypes are tables, components are columns and entities are indices within the table (rows).
  • Organize your data in contiguous arrays to utilize the CPU caches and hardware prefetcher.
  • Forget about your first instincts to create an Object hierarchy and trying to make a general solution before you understand the actual problem you are trying to solve.
  • Think about garbage collection. Avoid excessive Heap allocations in performance-critical areas, Use unity’s new Native containers instead. But beware that you must handle the clean up manually.
  • Understand the cost of your abstractions, beware of virtual function call overhead.
  • Utilize all cores of the CPU with the C# Job System.
  • Understand the hardware. Is the Burst compiler generating SIMD instructions? Use the Burst Inspector for analyzing.
  • Stop wasting cache lines. Think of packing data into cache lines as you do when packing data into UDP packets.

The top tip that Anders Eriksson wants to share is more general advice for anyone whose project is already in production: “Try to identify specific areas in your game where you’re having performance issues and see if you can start to apply DOTS in that isolated area. You don’t have to transform the entire code base!”

Going forward

“We want to adopt DOTS in more areas of our game and we were quite excited about Unite announcements on DOTS animations, Unity Physics, and Live Link. We would like to be able to convert more of our game objects to ECS entities and it seems like Unity is making good progress in making that a reality,” concludes Anders.

If you have more questions for the Far North Entertainment team, we recommend you join their Discord!

Check out our Unite Copenhagen DOTS playlist to learn how other innovative game studios use DOTS to make great games faster, and how all the upcoming DOTS-powered components, including DOTS Physics, our new Conversion Workflow, and the Burst Compiler, work together.

12 replies on “Creating a third-person zombie shooter with DOTS”

DOTS sounds wonderful to me, but my experience is not that bright (quite opposite, actually). Even with the package documentation and a few examples, I still struggle to understand how to use ECS for specific cases, or whether it’s possible to use it that way. Specifically, ECS relies heavily on hybrid approach, depending on classic Unity components, with very limited way to use pure ECS, and even no tutorials on proper hybrid approach.
There are also several things that are quite an enigma to me. Like the World, which I don’t know whether it’s a substitution for scenes or way to group or “layer” certain entities and systems.
Another thing would be the bootstrap. By disabling the default one, I have to manually create all the system that I want to use, except I don’t know what those default systems actually are, or whether I caused any more issues I need to solve manually now.
Speaking of systems, using reflection to find them is very time demanding and inefficient. I would rather have some bootstrap class where I have to manually list all the systems I want to instantiate, with more control over their running, rather than having reflection finding and running all the systems in my project, of which some may be used for different part/scene of my game, and some should not be run at all, such as different variations of the same system, prototypes, or unused game feature.
I would really welcome some more detailed information about DOTS development (maybe even roadmap), because so far it looks like all the other features for Unity are now prioritized over DOTS.

The code example is awesome. I was just wondering if finding all entities in range of something would be fast enough for a large number of entities. I would suggest you change the call math.length(toTarget) to math.lenthsq(toTarget) the constant 0.008 to 0.000064 , sqrt is very slow.

Maybe better way for unity will be create language for unity and convert it to more efficient output (dots) when project will be done (compiled). I think unity dots conversion can be hard to learn. I think it is good direction for your game engine but i don’t know did you made it without pain for devs.

HI Thiago,
As we mature DOTS and prepare it to be production ready, more documentation and samples will follow. Right now parts are still in flux, and changes happen. So the material we show isn’t as rooted yet.

Best regards

It might make for an interesting MSc dissertation project. Maybe implementing a game with and without DOTS and reflecting on the development experience and the technical outcome. I might need to see if something like this would be feasible. As an educator, I think the paradigm would be an interesting alternative to OOP and may justify a dedicated module on DOTS.

I have no idea how to code in this new paradigm. All that seems like gibberish to me. I’ve been working with Unity since Unity 3 and now I feel like I’ll have to learn everything again from zero.

The new DOTS stack looks amazing, but still there is a question, what to do in the following situation:
1. I have accumulated a nice codebase and know-hows using classic Unity approach with GameObjects and MonoBehaviors
2. The games I develop are not cpu-intensive and won’t benefit from migrating to DOTS
3. I want to keep updating Unity to get access to all the nice new features

So, the question is – will I be able to keep using GameObjects/Monobehavior approach with new versions of Unity down the line, or will I be forced at some moment in future to switch to DOTS entirely?

Can’t wait for the new DOTS FPS sample! My body is prepared… (Suddenly a voice in the back of his head shouts “YOU ARE NOT PREPARED! Go back to docs!”)

Comments are closed.