Unity DOTS: Tips and Best Practices

Photo by Kevin Ku on Unsplash

At Mighty Bear Games, we always aim to develop highly performant code so that our players can experience high quality gameplay and graphics while still enjoying smooth framerates. One of the ways we achieve that is by using Unity’s Data Oriented Technology Stack (DOTS). DOTS is a framework which makes it easier for developers to write highly performant code on multicore processors. There are, of course, other benefits, like the resulting code being more testable, but we’ll focus on DOTS in this article.

Utilising DOTS does require a bit of a paradigm shift from Object-Oriented Programming (OOP) to Data-Oriented Design (DOD). There is a learning curve, so if you’re new to DOTS, I’d recommend starting off with this tutorial done by Unity.

Data Oriented Design

One of the principles of DOD is to write logic in systems that would perform operations on entities. These entities have components that store data. We tend to end up with a lot of systems in the games we develop, so it’s important that these systems are optimised as much as possible. We should not update the system if it has no entities to operate on. Fortunately, Unity has got that covered.

Minimising System Updates

https://medium.com/media/f5448d2037b652d70e5a403f07c22728/href

If the above query has no entities,

0 entities

It will not update. Yay!

Game States

Sometimes we want the system to only update when the game is in a certain state, like in the result screen. To indicate that the game is in the result screen, we can create a ResultScreen singleton component. Then in the system, we can make that a requirement.

https://medium.com/media/80de0c5dc0f1f95b73673f5e85f87723/href

No results screen component

The system will not run without the ResultScreen singleton component.

Empty Query with Singleton Component

What if the ResultScreen exists but the query is empty? It should not calculate right?

0.03 ms to update the system in the Unity Editor

Oh, no! It still runs. What gives? As long as the system fulfils the RequireSingletonForUpdate condition, the system will run. To fix that issue, we can create an equivalent query to the entity query and make that a requirement too using RequireForUpdate.

https://medium.com/media/36353ab22a0cab26fcecf585d09364d4/href

That works! But that’s a lot of boilerplate. And it’s quite error-prone. We have to remember to change the required query if the entity query changes.

Magic

It’d be nice to somehow use the entity query itself as the requirement. That’s possible through a bit of magic using WithStoreEntityQueryInField.

https://medium.com/media/1605be192068feb5b07105c4440f7f1e/href

This code looks somewhat magical because the query is initialised before it is called. Under the hood, Unity initialises that query through the magic of code weaving. No, it’s not a fantasy magical term I cooked up. It’s real. The simplistic explanation of code weaving is that it modifies source code before it is compiled.

Grouping Systems Together

What if there are a lot of systems that should only run in the result screen? To simplify the code, we should create aComponentSystemGroupand make those systems a child of that.

https://medium.com/media/f5aee8d9b94fcc095705eb26c6554a98/href

The system will only run if the parent ComponentSystemGroup runs.

It’s great for performance and also helps organise systems.

Change Filters

It’d be nice if we could only update a system if a component changes. For example, only recalculating the score if the number of enemies killed by the player changes. We can do so by using ChangeFilters.

https://medium.com/media/c1d31d534ce39c12e05e2a719440d40d/href

There is a gotcha. A component is considered to be changed when a write query has been declared on it. So setting a query parameter using ref or using GetComponentDataFromEntity(readonly:false) would trigger a change even if the value of the component has not changed.

So if the PlayerStatsComponent did not change, the query should be empty and the system should not run, right?

Empty query because of change filter

Nope! The query is empty but the system is still running. This is because RequireForUpdate does not take into account the change filter. We have to manually skip the update in code.

https://medium.com/media/acd2d1ddc433063c8322658ec034e6e1/href

So, More DOTS?

The more logic heavy the game is, the more systems there will be. While burst-compiled code in systems is quite performant, no calculation is still faster than fast calculation. In a game with lots of systems, these calculations add up. As you’ve read, while Unity handles optimisations for the base case automatically; in more complex scenarios, things are not as straightforward as they seem. With increasing complexity, there is a greater need for tests.

If you’re interesting in learning more about how Mighty Bear builds games on the back end, you should read our previous articles Automated Tests Can Save Your Code and 2 Awesome uses of UniRx.

Meanwhile, if you enjoyed this article on Unity DOTS, drop us a clap or comment down below!


Unity DOTS: Tips and Best Practices was originally published in Mighty Bear Games on Medium, where people are continuing the conversation by highlighting and responding to this story.