Mouse

Entity Component System for Unity: Getting Started [FREE]

The cake is a lie.
For years, you’ve built your Unity applications around the best object-oriented practices: Classes, inheritance and encapsulation. But what if you’ve been doing it all wrong?
Unity’s new Data-Oriented Tech Stack, or DOTS, moves away from OOP toward data-oriented design. Central to this paradigm shift is the Entity Component System, or ECS.
ECS restructures your workflow around your game’s data and how it’s stored in memory. The result is more performant code that can handle massive scenes more efficiently.
Monobehaviour, we hardly knew ya.
In this tutorial, you’ll update part of a simple shoot ‘em up to use the Entity Component System. In doing so, you’ll learn the following:
How to create Entities
How to use hybrid ECS to ease into this new paradigm shift.
Components, and how they can store data efficiently, if used correctly.
Systems, the holders of logic and behaviors that act on your data, manipulating and transforming it for your game.
How to hook up all of the above to fully leverage ECS.

Note: This tutorial is for experienced readers. Before you begin, you’ll need working knowledge of C#, the Unity Editor and Unity 2019.3 or above for some features.

Getting Started
Use the Download Materials button at the top or bottom of this tutorial to get the project files. Unzip and open the IntroToECSStarter project.
In Window ► PackageManager, install and update the following packages.

Note: Selecting In Project from the drop down menu next to the + symbol will restrict the list of packages for easier updating.

The following packages are essential for any ECS-based project:

Entities implements the new ECS features.

Hybrid Renderer renders all entities created in ECS.
The demo also uses the following packages for visuals:

Cinemachine controls the follow camera.

Universal Render Pipeline, or URP, holds the graphical settings.

TextMeshPro displays the UI text elements.
Examine Assets/RW. Here, you’ll find some folders containing assets used to build the demo.

There’s a lot here, but in this tutorial you won’t touch the following directories:

Fonts.

Materials.
Models.

ParticleSystems.

PipelineSettings.

PostFX.

Shaders.

Sounds.

Textures.
There are folders for Scenes and Prefabs, but you’ll mostly work in the Scripts folder for this tutorial.
You’ll find several custom components for handling the player input, movement and shooting in Scripts/Player. Also, Scripts/Managers contains pre-built components to manage the game logic.
Stress Testing the Demo
Open SwarmDemoNonECS in Scenes.
Now, choose Maximize on Play in the Game view and set the Scale all the way to the left to get the full picture of the interface. Then enter Play mode to test the game.

Use the arrow or WASD keys to move the player’s tank. Point the mouse to aim the turret and left mouse button to fire bullets.
Notice that every few seconds, a new wave of enemies surrounds you. By default, the drones explode on contact, but don’t destroy the player.
This invincibility mode allows you to stress test your app. Hundreds of explosions, bullets and enemies clutter the Hierarchy.
To see the gameplay’s real-time impact, use the Stats in the Game window to track the main CPU time and the frame per second, or FPS count.

If you play long enough, you’ll notice the render time per frame increases while the FPS decreases. After a minute or so, the game slows down and becomes choppy as too many objects fill the screen.
Entity Component System to the rescue!
You can disable player invincibility for slightly more realistic test conditions. Locate EnemyDroneNonECS in RW/Prefabs/NonECS. Then open the prefab to edit and check Can Hit Player in the Enemy Non ECS component.

Save the prefab and play the game again. See your tank die when it collides with a drone.

Although enemies can’t keep spawning ad infinitum, Unity still stutters and spikes under too many objects at once.

ECS: Performance by Default
In classic Unity development, GameObjects and Monobehaviours let you mix data with behavior. For example, floats or strings can live side-by-side with methods like Start and Update. Making a mishmash of data types within one object translates into a memory layout like this:

For example, your GameObject might reference several data types like a Transform, Renderer and Collider. Unity scatters the varied data across non-contiguous memory. With enough objects, it spills into slower RAM.
In contrast, ECS tries to group similar data into chunks. It attempts to allocate memory with fewer gaps, packing the data more tightly. Doing this keeps as much as possible in the very fast CPU memory cache tiers (L1, L2, L3).

DOTS replaces object-oriented programming with data-oriented design. This architecture focuses on how to keep the data compact, which, unfortunately, means replacing the Monobehaviours you’re accustomed to using.
Instead, you’ll build your app from Entities, Components and Systems.

Entities are items that populate your program, although they are not objects in the traditional sense. An entity is a small integer ID pointing to other bits of data.
Components are the actual data containers. They are structs that hold values without any logic, and you’ll no doubt have a lot of them. ECS revolves around storing these small Components in a clever way.
Systems hold behaviors and logic. You’ll use them to manipulate and transform your data. Because Systems work on entire arrays of Entities at once, they can do so more efficiently.
Together these three parts form ECS.
Following this architectural pattern tends to cluster your data toward the very fast cache memory. The result is a significant speedup compared to the OOP equivalent. Unity calls this phenomenon performance by default.
Note: Avoid confusing an ECS Component from Unity.Entities with classic UnityEngine.Object Components. Despite similar names, they’re completely separate.

Removing Non-ECS Code
Load SwarmDemoECS from Scenes, which removes the code used to generate the enemies.
Then open EnemySpawner.cs in Scripts/Managers.
Normally, this script would instantiate enemy waves, but some of the logic is missing. Notice the Start and SpawnWave methods are blank. You’ll fill those in during the tutorial.
Head on back to the Unity Editor. Disable DemoManagers and PlayerTank in the Hierarchy. But don’t delete them! You’ll need them later.

Confirm in Play mode that enemies no longer spawn. Don’t worry, you’ll add them back using ECS.
For now, you should have an empty scene except for a plane with a grid texture. This is a perfect blank slate to create some entities!

Creating an Entity
At the top of EnemySpawner.cs, add the following using directive to import the resources needed for ECS:
using Unity.Entities;
Then define this field:
private EntityManager entityManager;
An EntityManager is a class to process Entities and their data.
Next, fill the Start method with these lines:

private void Start()
{
// 1
entityManager=World.DefaultGameObjectInjectionWorld.EntityManager;

// 2
Entity entity=entityManager.CreateEntity();
}

All Entities, Components, and Systems exist in a World. Each World has one EntityManager. Though very verbose, this line simply grabs a reference to it.

Then you invoke entityManager.CreateEntity to generate your first Entity.
Save the script and enter Play mode in the Editor.
In the Hierarchy, notice that…nothing happened! Or did it?
Since ECS entities are not GameObjects they don’t show up in the Hierarchy.
To view your Entities, you need to use a particular interface called the Entity Debugger. You can find it by selecting Window ► Analysis ► Entity Debugger.

Dock the window, or keep it accessible, since you’ll refer to it often while working with ECS.
In Play mode, the Entity Debugger window shows the various behaviors, or Systems, running on the Default World in the left-hand panel.

Highlight All Entities (Default World) and you’ll see two Entities appear in the middle panel: WorldTime and Entity 1. You’ll find information about their corresponding chunks of memory in the right-hand panel.
Now, select WorldTime, which the World creates by default. The Inspector shows the game clock.

Select Entity 1. Here it is! This is your first custom-built entity!

Ok, it’s not much to look at right now. :] Entities are empty when created. You need to add data for them to be meaningful.
Adding Components
In one approach, you can populate your Entity with Component data strictly using code.
Entities are separate from Monobehaviours. Thus, they need their own libraries for transforms, math and rendering.
Looking back at EnemySpawner.cs, using Unity.Mathematics; is already there. Add these two as well:

using Unity.Transforms;
using Unity.Rendering;

Below that, reserve some fields for the enemy’s mesh and material:

[SerializeField] private Mesh enemyMesh;
[SerializeField] private Material enemyMaterial;

Now, modify the Start method to look like this:

private void Start()
{

entityManager=World.DefaultGameObjectInjectionWorld.EntityManager;

// 1
EntityArchetype archetype=entityManager.CreateArchetype(
typeof(Translation),
typeof(Rotation),
typeof(RenderMesh),
typeof(RenderBounds),
typeof(LocalToWorld));

// 2
Entity entity=entityManager.CreateEntity(archetype);

// 3
entityManager.AddComponentData(entity, new Translation { Value=new float3(-3f, 0.5f, 5f) });

entityManager.AddComponentData(entity, new Rotation { Value=quaternion.EulerXYZ(new float3(0f, 45f, 0f)) });

entityManager.AddSharedComponentData(entity, new RenderMesh
{
mesh=enemyMesh,
material=enemyMaterial
});
}

Here’s what this script does:
After getting a reference to the EntityManager, you define an EntityArchetype. This associates certain data types together.
In this case, Translation, Rotation, RenderMesh, RenderBounds and LocalToWorld form the archetype.

Next, you pass the archetype into entityManager.CreateEntity. This initializes the Entity.
You then use AddComponentData and AddSharedComponent to add data and specific values.
In this example, the enemy drone receives a translation of (X: -3, Y:0, Z:5) and a y-rotation of 45 degrees, while also assigning the mesh and material to the RenderMesh data.

Select EnemySpawner in the Hierarchy and fill in the missing mesh and material. Next, drag the RoboDrone mesh from Models into EnemyMesh in the Inspector. Then drag DroneHologramMat from Materials into the EnemyMaterial.

In Play mode you’ll see a single drone appear on-screen.

At runtime, no extra GameObjects appear in the Hierarchy. The enemy drone is also not selectable in the scene view. This is an Entity, so its properties are only visible in the Entity Debugger.
Select Entity1 from the Entity Debugger and pop over to the Inspector to see its data.

In the Entity Debugger, the data types on the right now reflect the EntityArchetype. Unity groups memory chunks with similar archetypes together for faster reading and writing.

ConvertToEntity
Of course, this is a lot of code to make a simple 3D object. Generating an Entity from scratch via script is the pure ECS approach. While valid, it can be a little tedious to repeat each time you want something to appear on-screen.
Unity streamlines this process with a hybrid ECS approach. First, you define some data on a GameObject. Then at runtime an Entity with identical data replaces it.
First, remove most of the logic from Start, leaving only the first line:

private void Start()
{
entityManager=World.DefaultGameObjectInjectionWorld.EntityManager;
}

Then enter Play mode, pause playback and check that Entity 1 no longer appears in the Entity Debugger.

Now, exit Play mode. It’s time to try the hybrid ECS approach!
Navigate to RW/Prefabs and select the EnemyDrone prefab. Click Open Prefab and add a Convert to Entity component by selecting Add Component ► DOTS ► Convert To Entity.

In the Conversion Mode, select Convert And Destroy. This removes the original GameObject and leaves an Entity in its place at runtime.
Save the prefab.
Next, drag the EnemyDrone from RW/Prefabs into the Hierarchy. Position and rotate it anywhere you like on-screen.
Now, enter Play mode. The GameObject mysteriously vanishes from the Hierarchy, but the enemy remains visible in the Game camera. In the EntityDebugger, you now have an Entity named EnemyDrone with parameters matching the GameObject that created it.

If you exit Play mode, the GameObject should reappear in the Hierarchy. Like magic!
In hybrid ECS, your GameObject acts as a placeholder for setting up basic transforms and rendering data. Then at runtime, Unity turns it into an Entity.
MoveForward Component Data
Next, you need to have the enemy fly forward, because, let’s face it, without that being a tank is no fun. You’ll accomplish this by creating some data and reserving a Component to represent the enemy’s forward speed.
First, make a new C# script, MoveForwardComponent.cs, in Scripts/ECS/ComponentData:

using Unity.Entities;

public struct MoveForward : IComponentData
{
public float speed;
}

Instead of inheriting from Monobehaviour, Component data must implement the IComponentData interface. This is an interface for implementing general-purpose components, but it’s important to note that any implementation must be a struct.
You only need one simple public variable here, and Unity recommends grouping fields with data that will almost always be accessed at the same time. It’s ok, and more efficient, to use lots of small separate components rather than building up a few bloated ones.
The struct’s name, MoveForward, doesn’t need to match the filename, MoveForwardComponent.cs. You have more flexibility with ECS scripts than with Monobehaviours. Feel free to store more than one struct or class in each file.
Authoring
ConvertToEntity converts the EnemyDrone’s transform and rendering information into an equivalent Entity. However, the conversion isn’t automatic for the custom-defined MoveForward. For that you need to add an authoring component.
First, edit the EnemyDrone prefab, (Open Prefab).
Try to drag the MoveForwardComponent onto the prefab. Unity prompts you with an error message.

Now add a [GenerateAuthoringComponent] attribute to the top of the MoveForward struct:

[GenerateAuthoringComponent]

Next, drag the modified script onto the EnemyDrone again. This time Unity attaches a Monobehaviour called MoveForwardAuthoring. Any public fields from MoveForward will now appear in the Inspector.
Change speed to 5 and save changes to the prefab.

Now, enter Play mode and confirm that the authoring component set the Component data default value in the EntityDebugger. Your Entity should have a MoveForward data type with a speed value of 5.

Movement System
Your Entity has some Component data now, but data can’t do anything by itself. To make it move, you need to create a System.
In Scripts/ECS/Systems, make a new C# script called MovementSystem:

using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;

// 1
public class MovementSystem : ComponentSystem
{
// 2
protected override void OnUpdate()
{
// 3
Entities.WithAll().ForEach((ref Translation trans, ref Rotation rot, ref MoveForward moveForward)=>
{
// 4
trans.Value +=moveForward.speed * Time.DeltaTime * math.forward(rot.Value);
});
}
}

This represents a basic System in ECS:

MovementSystem inherits from ComponentSystem, which is an abstract class needed to implement a system.
This must implement protected override void OnUpdate(), which invokes every frame.
Use Entities with a static ForEach to run logic over every Entity in the World.
The WithAll works as a filter. This restricts the loop to Entities that have MoveForward data. You only have one Entity at the moment, but this will be significant later.
The argument for the ForEach is a lambda function, which takes the form of:
(input parameters)=> {expression}

Use the ref keyword in front of input parameters. In this example, you pass in references to the Translation, Rotation and MoveForward component data.

The lambda expression calculates the speed relative to one frame, moveForward.speed * Time.DeltaTime. Then, it multiplies that by the local forward vector, (math.forward(rot.Value).
Each Entity increments its position by this amount and voila! It moves in its local positive z-direction.

Once you save the file, the System is active. There’s no need to attach it to anything in the Hierarchy. It runs whether you want it to or not!
Now, enter Play mode and… success! Your drone flies in a straight line!

Experiment with different y-rotation values and speed values on your EnemyDrone.
Making an Entity Prefab
So far, you’ve created a single enemy Entity, but eventually, you’ll want to make more. You can define an Entity as a reusable prefab. Then at runtime, you can create as many Entities as you see fit.
First, add these fields to the top of EnemySpawner.cs:

[SerializeField] private GameObject enemyPrefab;

private Entity enemyEntityPrefab;

Then drop these lines at the bottom of Start:

var settings=GameObjectConversionSettings.FromWorld(World.DefaultGameObjectInjectionWorld, null);

enemyEntityPrefab=GameObjectConversionUtility.ConvertGameObjectHierarchy(enemyPrefab, settings);

Wow, that’s a mouthful! Though the syntax is a bit verbose, this merely sets up some default conversion settings. Then it passes those settings into GameObjectConversionUtility.ConvertGameObjectHierarchy.
The result is an Entity prefab that you can build at runtime.
To test this, follow these steps. First, delete EnemyDrone from the Hierarchy by selecting right-click ► Delete and then select EnemySpawner. In the Inspector, drag the EnemyDrone from Prefabs into EnemyPrefab.
At runtime, nothing appears yet. You must instantiate the Entity prefab to see it.
So, append this line to Start:

entityManager.Instantiate(enemyEntityPrefab);

Now enter Play mode. Once again, your single enemy drone reappears and flies forward. This time, it starts from the prefab’s default position.

Now you can create instances of an Entity, as you would with GameObject prefabs. Your goal is to create more than one enemy, so comment out this line and invoke SpawnWave instead.

// entityManager.Instantiate(enemyEntityPrefab);
SpawnWave();

Next, you’ll fill out the logic for SpawnWave and create an enemy swarm.
Spawning an Enemy Wave With NativeArray
While one drone is fun, a whole swarm of drones is better. :] To create your swarm, first add a new using line to the top of EnemySpawner.cs:

using Unity.Collections;

This gives you access to a special collection type called NativeArray. NativeArrays can loop through Entities with less memory overhead.
Then, fill in SpawnWave with the following code to randomly place an array of Entities in a circle formation:

private void SpawnWave()
{
// 1
NativeArray enemyArray=new NativeArray(spawnCount, Allocator.Temp);

// 2
for (int i=0; i
{
// 5
float3 direction=playerPos – trans.Value;
direction.y=0f;

// 6
rot.Value=quaternion.LookRotation(direction, math.up());
});
}
}

Here:
The System inherits from ComponentSystem. This expects an OnUpdate to run every frame.
If the game is no longer active, GameManager.IsGameOver returns straight away.
You store the player’s location using GameManager.GetPlayerPosition.

Again you use an Entities.ForEach to loop through all Entities. The lambda argument takes the Entity itself, its Translation and its Rotation as input parameters.
Then, you calculate the vector to the player, ignoring the y.
Finally, you use quaternion.LookRotation to set the correct heading. Pass in the vector and positive y-axis (math.up).
Great! Enemy drones now head toward the Player. Since they can’t die yet, they follow you around.
Unfortunately, this System also breaks your player weapon. If you click the mouse button, the bullets immediately turn around and no longer shoot straight.

Instead, now the glowing bullets and enemies both cluster around the player, which isn’t exactly what you want.
Generating ComponentTags
Player bullets and enemy drones use the same MoveForward for locomotion. Unity thinks of them both as Entities that can move forward, with no distinction between them.
Inspect the Bullet prefab to verify this. Aside from a much faster speed, very little distinguishes a Bullet from an Enemy.
Because FacePlayerSystem works on all Entities in the World by default, it needs something to tell the different Entities apart. Otherwise, ECS treats bullets and drones equally, and both turn to face the player.
This is where you can use Component data to tag Entities, thereby differentiating them.
First, create an EnemyTag.cs in Scripts/ECS/ComponentTags:

using Unity.Entities;

[GenerateAuthoringComponent] public class EnemyTag : IComponentData
{
}

Then, create a BulletTag.cs in Scripts/ECS/ComponentTags:

using Unity.Entities;

[GenerateAuthoringComponent] public class BulletTag : IComponentData
{
}

That’s right, you only need two empty scripts!
Now edit the EnemyDrone in Prefabs. Add EnemyTagAuthoring by dragging and dropping EnemyTag.cs. Save the prefab.
Then, edit the Bullet prefab as well. This time add the BulletTagAuthoring and save the prefab.
Note: Empty Component data is a handy trick to categorize your Entities.

In FacePlayerSystem.cs, add a WithAll query before invoking the ForEach, passing in the EnemyTag:

Entities.WithAll().ForEach((Entity entity, ref Translation trans, ref Rotation rot)=>
// rest of script

This fluent-style query forces the logic to run only on Entities tagged with EnemyTag. You can use constraints like WithAll, WithNone and WithAny. Adding those before the ForEach filters the results.

Your bullets now shoot forward as expected since the FacePlayerSystem no longer affects them.
Now you need some explosions!
Destruction System
Enemies should explode on contact with your bullets. Likewise, your player’s tank should blow up if a drone crashes into it. A simple distance check can simulate collisions for this demo.
Create a DestructionSystem.cs in Scripts/ECS/Systems:

using Unity.Entities;
using Unity.Transforms;
using Unity.Mathematics;

public class DestructionSystem: ComponentSystem
{
// 1
float thresholdDistance=2f;

protected override void OnUpdate()
{
// 2
if (GameManager.IsGameOver())
{
return;
}

// 3
float3 playerPosition=(float3)GameManager.GetPlayerPosition();

// 4
Entities.WithAll().ForEach((Entity enemy, ref Translation enemyPos)=>
{
// 5
playerPosition.y=enemyPos.Value.y;

// 6
if (math.distance(enemyPos.Value, playerPosition)
{
// 10
if (math.distance(enemyPosition, bulletPos.Value)
Read More

Show More

Related Articles

Leave a Reply

Your email address will not be published.

Back to top button
Close
Close