Made With Unity DOTS | Let’s Move A Cube
Here we go again 😂
I will REALLY TRY to explain the basic stuff on Unity DOTS with a small example of moving a cube.
Yes, it’s very baby steps, but learning little by little is the best way I know.
This article aims to share what I have learned when moving a cube using DOTS and explain the difference between object-oriented programming (OOP) and data-oriented design (DOD), which is the whole point of Unity’s DOTS.
Let’s get to it!
What Is DOTS?
DOTS is a combination of technologies that represent a different way of working with Unity. It has existed since 2018 and became stable around 2022 — early 2023.
Taking it from Unity’s website (here):
DOTS is Unity’s Data-Oriented Technology Stack (DOTS), which combines technologies and packages to deliver a data-oriented design approach to building games in Unity. This allows you to apply data-oriented design to a game’s architecture in a highly performant manner.
To clarify, DOTS is a stack of software architecture that allows Unity developers to best use the hardware characteristics and constraints. It is designed to use multicore processors to parallelize data processing, making our applications more performant and better suited for our target hardware.
This new stack brings many new packages to Unity, such as ECS(Entity Component System) for Unity, Collections, Mathematics, and C# Job System.
I will not cover all DOTS features but will touch on ECS since we will use it to move the cube.
A quick note here: there are some packages that you can use with MonoBehaviour without using the ECS architecture. Some packages are the C# Job System, Mathematics, and Collection. But when used with the ECS, they can shine and take advantage with the aim of data optimization.
ECS(Entity Component System)
Entities
The Entities package organizes code and data using the Entity Component System (ECS) architecture. All you need to know about Entities is that they act like IDs. An Entity is a unique identifier associated with individual components that contain data about it.
GameObjects are turned into Entities when the process of Baking occurs.
Do not worry; Baking will be explained when moving the cube :)
Component
The Component is just a Struct or Class that contains runtime data. For the cube example, we use a Component (IComponentData) to use a float value that will be responsible for controlling our cube speed.
using Unity.Entities;
public struct PlayerSpeedData : IComponentData
{
public float Value; // Player speed to move
}
Authoring Component
The Authoring Component is MonoBehaviour with design data (we can see the values on the Unity Inspector) and a Baker.
Baker
A Baker is a class that converts Authoring Components into Entities and Components.
using UnityEngine;
using Unity.Entities;
public class PlayerAuthoring : MonoBehaviour
{
[SerializeField] private float _speed;
private sealed class PlayerAuthoringBaker : Baker<PlayerAuthoring>
{
public override void Bake(PlayerAuthoring playerAuthoring)
{
//The Code Here
}
}
}
System
System is where our behavior now lives. Think of it like the Update() method loop we all know and love. The System contains Entity Queries and Entity ForEach Loop. Entity Queries return Entities that contain specific Components that we want, and Entity ForEach Loop processes entities from query and manipulate data.
using Unity.Burst;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;
using UnityEngine;
public partial struct PlayerMovementSystem : ISystem
{
[BurstCompile]
public void OnUpdate(ref SystemState state)
{
//The Code here updates all entities that match query requirements
}
}
Unity has a course on their Unity Learn page that introduces DOTS: Basics Of DOTS: Jobs And Entities.
What Is The Difference Between OOP And DOD?
If you have used GameObjects in Unity, created a few scripts, and made a simple Player system, you use OOP (object-oriented programming). In OOP, you have created classes (Player, Enemy, PowerUps, etc.) that, when inheriting from MonoBehaviour, can be turned into Components that can be added to GameObjects as instances. From there, you add their respected values when you have a reference to them.
Unity’s DOTS philosophy is grabbed from DOD (data-oriented design). It focuses on the data (used by structs, usually with data types like float and ints) and its structure and manipulation to optimize performance. This means that property values like health, speed, etc., that are common and separated in different areas of memory based on their classes (Player, Enemy, etc.) are stored more closely to improve cache utilization.
Let’s see an example.
OOP VS DOD
Using OOP in Unity, we have a Mammal class inherited from MonoBehaviour that allows us to combine data and logic in C# classes with fields (data) and methods (logic). This class will hold the details of the Mammal.
Classes can be extended through inheritance. The picture below shows that ‘Dog is a Mammal’ and ‘Cat is a Mammal,’ so both Dog and Cat can inherit things from a more generic Mammal class. Dogs have name, breed, and a sound; Cats also have name, Breed, and a sound.
If I need to go through and play all the sounds, I have to check each of these different class instances and find and play the Sound. Each one we go to is far from the previous one in memory, so we can get a cache miss, slowing down our code.
In the DOD case, all the same data types are grouped, and we can directly find them without jumping and checking one by one since there's a single spot of memory where all my ‘Sound’ is.
I know this is not the best explanation since I haven’t touched on how this affects memory cache, CPU, etc.
Using DOD allows us to manage memory as best as possible and get the best-optimized performance on our game.
The Unity Project
The Unity project is straightforward. It is a 3D (URP) Unity project with the Entities package installed and two Game Objects.
We will move our Player (Blue Cube) using our WASD keys with the DOTS technology.
Installing Packages
We start by installing the Entities Graphics package from the Package Manager. This will allow us to render entities, and installing this package will also automatically install the Entities package itself as a dependency.
Creating SubScene
This part is where it gets new😁
To create an Entity from our Game Object, we need to start a process called baking. This process occurs when we create a SubScene in a Unity Scene. When the scene is loaded at runtime (when we press Play Mode) only the baked entities (the Game Object inside the SubScene) will be loaded and not the Game Object.
Press the plus sign in the Hierarchy windows and select New Sub Scene->Empty Scene. You will be prompted to save the SubScene and name it however you like. It will be saved in a folder with the name of your current Unity Scene.
As you can tell, the SubScene is just a Game Object with a MonoBehaviour component/script attached called SubScene.
Our Floor And Player (Cube)
Under the SubScene, add two Cubes. One will be the Floor, and the second will be our Cube/Player.
THE CODE
Now we have enter into new coding turf😎
The image above shows a folder structure with the needed “Scripts” to make our cube move: PlayerSpeedData, PlayerAuthoring, and PlayerMovementSystem.
PlayerSpeedData.cs
The PlayerSpeedData.cs will hold our float Value for the speed we use to tell the System how fast our Cube/Player moves. This component is inherited from IComponentData and uses Unity.Entities namespace.
It's a tiny script with new adjustments.
PlayerAuthoring.cs
PlayerAuthoring.cs will allow us to add the PlayerSpeedData to an entity in the baking process. Having the main class, PlayerAuthoring, inheriting from MonoBehaviour, will allow us to attach this component to our Cube/Player and edit the _speed variable using the Inpector window.
The inner class, PlayerAuthoringBaker, will translate the data from MonoBehaviour (scripts attached to GameObjects) into the ECS world.
When the baking process starts, on line 12, we get the TransformUsageFlags.Dynamic to specify that the entity needs the standard transform components, including LocalTransform. Also, on line 12, we add the PlayerSpeedData component to the entity.
PlayerMovementSystem.cs
Now, we are inheriting from ISystem. By default, the System is automatically instantiated when we press Play, and the OnUpdate() on our PlayerMovementSystem will be called once for every frame. We are performing the following: We are performing a query where we check all of our entities and are just looking for the entity that has both the LocalTransform and the PlayerSpeedData. In this case, we know that this is our Cube/Player.
Since we know we are getting one entity, we store our input and speed, and on line 25, we move our Cube/Player.
Let’s Hit Play
Before pressing play, let’s uncheck our SubScene on the Hierarchy since we no longer want to edit it.
Now, we can press play.
Conclusion
There we have it.
In this article, we’ve taken a hands-on approach to Unity DOTS by moving a simple cube. Through this process, we explored the core concepts of DOTS, including the Entity Component System (ECS) and its advantages over traditional object-oriented programming (OOP).
This is just the beginning of our DOTS journey!💪
To be continued 😁