What are you looking for?
Introducing our new e-book: Unity’s Data-Oriented Technology Stack (DOTS) for advanced developers
May 30, 2024|9 Min
Introducing our new e-book: Unity’s Data-Oriented Technology Stack (DOTS) for advanced developers

Unity's Data-Oriented Technology Stack (DOTS) lets you create complex games at large scale by providing a suite of performance-enhancing tools that help you get the most out of your target hardware.

This 50+ page e-book, Introduction to the Data-Oriented Technology Stack for advanced Unity developers, is now available to download for free. Use it as a primer to better understand data-oriented programming and evaluate if DOTS is the right choice for your next project. Whether you’re looking to start a new DOTS-based project, or implement DOTS for performance-critical parts of your Monobehaviour-based game, this guide covers all the necessary ground in a structured and clear manner.

With Unity 6 in preview and DOTS 1.0 production-ready, this is a great time to explore the opportunities DOTS brings. The e-book, written by Brian Will, senior software engineer at Unity, joins the updated Unity Learn samples, recent DOTS bootcamp, and the GitHub samples in the collection of resources available to developers who want to learn how to work with DOTS.

A guide to help you decide if DOTS the right choice for your game
In Unity’s Entity Component System, all entities with the same set of component types are stored together in the same “archetype”.

Our goal with this e-book is to help you make an informed decision about whether implementing some or all of the DOTS packages and technologies is the right decision for your existing or upcoming Unity project. Each part of the stack plays a role in enhancing a game's execution speed and efficiency. The guide aims to explain each of these parts, how they can be used together, and their common foundation, the Unity Entity Component System (ECS).

A major reason to use DOTS is to get the most performance from your target hardware, and this requires understanding multithreading and memory allocation. Additionally, to leverage DOTS, you’ll need to architect your data-oriented code and projects differently to your C#-based Monobehaviour projects with their higher level of abstraction.

Let’s take a closer look at what you’ll find in the e-book.

CTA: Download Introduction to the Data-Oriented Technology Stack for advanced Unity developers.

What’s in the DOTS e-book?
 A scene from the Firefighters sample, available in the EntityComponentSystemSamples Github

The first section in the guide, which we’ve included below, presents some of the factors that can contribute to poor CPU performance in a game, like garbage collection overhead, data and code that aren’t cache-friendly, suboptimal compiler-generated machine code, and more.

The next section explains how each of the DOTS packages and features facilitate writing code that avoids CPU performance pitfalls. You’ll find helpful explanations for:

  • C# Job System
  • Burst compiler
  • Collections
  • Mathematics
  • Entities
  • Entities Graphics
  • Unity Physics
  • Netcode for Entities

After a rundown of each part of the stack you’ll get an introduction to the EntityComponentSystemSamples GitHub repo, which includes many samples that introduce both basic and advanced DOTS features. Some of the samples in the Github repo are reproduced in a new Unity Learn course on DOTS, Get acquainted with DOTS.

The other key section in the DOTS guide is the appendix. It’s here that Brian Will provides detailed explanations for concepts related to Unity ECS, including memory allocation and garbage collection, memory and CPU cache, multithreaded programming, the limitations of object-oriented programming, and data-oriented programming.

Excerpt: About performance
A profile from the Unity Profiler showing Burst-compiled jobs utilizing the potential of the CPU and running across many worker threads.

If you’re an experienced game developer then you know that performance optimization on target platforms is a task that runs through the entire development cycle. Maybe your game performs nicely on a high-end PC, but what about the low-end mobile platforms you’re also aiming for? Do the frames take much longer than others, creating noticeable hitches? Are loading times annoyingly long, and does the game freeze for full seconds every time the player walks through a door? In such a scenario, not only is the current experience subpar, but you’re effectively blocked from adding more features: More environment detail and scale, mechanics, characters and behaviors, physics, and platforms.

What’s the culprit? In many projects it’s rendering: Textures are too large, meshes too complex, shaders too expensive, or there’s ineffective use of batching, culling, and LOD.

Another common pitfall is excessive use of complex mesh colliders, which increase the cost of the physics simulation. Or, the game simulation itself is slow. The C# code you wrote that defines what makes your game unique might be taking too many milliseconds of CPU time per frame.

So how do you write game code that is fast, or at least not slow?

In previous decades, PC game developers could often solve this problem by just waiting. From the 1970’s and into the 21st century, CPU single-threaded performance generally doubled every few years (a phenomenon known as Moore's law), so a PC game would “magically” get faster over its life cycle. In the last two decades, however, CPU single-threaded performance gains have been relatively modest. Instead, the number of cores in the CPU have been growing and even small handheld devices like smartphones today feature several cores. Moreover, the gap between high-end and low-end gaming devices has widened, with a large chunk of the player base using hardware that is several years old. Waiting for faster hardware no longer seems like a workable strategy.

The question to ask, then, is“Why is my CPU code slow in the first place?” There are several common pitfalls:

  • Garbage collection induces noticeable overhead and pauses: This occurs because the garbage collector serves as an automatic memory manager that manages the allocation and release of memory for an application. Not only does garbage collection incur CPU and memory overhead, it sometimes pauses all execution of your code for many milliseconds. Users might experience these pauses as small hitches or more intrusive stutters.
  • The compiler-generated machine code is suboptimal: Some compilers generate much less optimized code than others, with results varying across platforms.
  • The CPU cores are insufficiently utilized: Although today’s lowest-end devices have multi-core CPUs, many games simply keep most of their logic on the main thread because writing multithreaded code is often difficult and prone to error.
  • The data is not cache friendly: Accessing data from cache is much faster than fetching it from main memory. However, accessing system memory may require the CPU to sit and wait for hundreds of CPU cycles; instead, you want the CPU to read and write data from its cache as much as possible. The simplest way to arrange this is to read and write memory sequentially, and so the most cache-friendly way to store data is in tightly-packed, contiguous arrays. Conversely, if your data is strewn non-contiguously throughout memory, accessing it will typically trigger many expensive cache misses; the CPU requests data that is not present in the cache memory and instead needs to fetch it from the slower main memory.
  • The code is not cache friendly: When code is executed, it must be loaded from system memory if it’s not already sitting in cache. One strategy is to favor calling a function in as few places as possible to reduce how often it must be loaded from system memory. For example, rather than call a particular function at various places strewn throughout your frame, it’s better to call it in a single loop so that the code only needs to be loaded at most once per frame.
  • The code is excessively abstracted: Among other issues, abstraction tends to create complexity in both data and code, which exacerbates the aforementioned problems: managing allocations without garbage collection becomes harder; the compiler may not be able to optimize as effectively; safe and efficient multithreading becomes harder, and your data and code tend to become less cache-friendly. On top of all this, abstractions tend to spread around performance costs, such that the whole code is slower, leaving you with no clear bottlenecks to optimize.

All of the above ailments are commonly found in Unity projects. Let’s look at these more specifically:

  • Although C# allows you to create manually-allocated objects (meaning objects which are not garbage collected), the default norm in C# and most Unity projects is to use C# class instances, which are garbage collected. In practice, Unity users have long mitigated this issue with a technique called pooling (even though pooling arguably defeats the purpose of using a garbage-collected language in the first place). The main benefit of object pooling is the efficient reuse of objects from a preallocated pool, eliminating the need for frequent creation and deallocation of objects.
  • In the Unity Editor, C# code is normally compiled to machine code with the Mono Compiler. For standalone builds you can get better results using IL2CPP (C# Intermediate Language cross-compiled to C++), but this brings some downsides, like longer build times and making mod support more difficult.
  • It’s common that Unity projects run all their code on the main thread, partly because doing so is what Unity makes easy:
    • The Unity event functions, such as the Update() method of MonoBehaviours, are all run on the main thread.
    • Most Unity APIs can only be safely called from the main thread.
  • The data in a typical Unity project tends to be structured as a bunch of random objects scattered throughout memory, leading to poor cache utilization. Again, this is partly because it’s what Unity makes easy:
    • A GameObject and its components are all separately allocated, so they often end up in different parts of memory.
  • The code in a typical Unity project tends to not be cache friendly:
    • Conventional C# and Unity’s APIs encourage an object-oriented style of code, which tends towards numerous small methods and complex call chains. Unlike a data-oriented approach it’s not very hardware friendly.
    • The event functions of every MonoBehaviour are invoked individually, and the calls are not necessarily grouped by MonoBehaviour type. For example, if you have 1000 Monster MonoBehaviours, each Monster is updated separately and not necessarily along with the other Monsters.

The object-oriented style of conventional C# and many Unity APIs generally lead to abstraction-heavy solutions. The resulting code then tends to have inefficiencies laced throughout that are hard to disentangle and isolate.

Who is the DOTS e-book for?
A preview of the new Introduction to the Data-Oriented Technology Stack for advanced Unity developers e-book.

This e-book is freely available to everyone, but is tailored to Unity developers who are experienced with Monobehaviour-based, object-oriented game development, but are new to Unity DOTS and data-oriented design development.

We hope the guide will help you understand DOTS and how these features might benefit your next Unity project, as well as make it easier for you to get the full value from the samples available on our GitHub repo.