Entity Component System for Unity: Getting Started

In this Unity tutorial you’ll learn how to efficiently leverage the Entity Component System for more performant gameplay code. By Wilmer Lin.

4.8 (47) · 4 Reviews

Download materials
Save for later
Share

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.

Tank and drone video demo

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.

Unity package manager window

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.

Unity asset folder structure

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.

Tank shooting at drones

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.

Unity stats window in Play mode

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.

Enemy Non ECS script component

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

Tank destroyed after enemies can now collide.

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:

Object-oriented programming memory layout diagram

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).

Data-oriented design memory layout diagram

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.

Entity, Component and System diagram

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.

Disabling objects in the Hierarchy panel

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!

Empty scene with plane and grid texture

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();
}
  1. 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.
  2. 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.

Video of Entity Debugger window opening

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.

Entity Debugger window in Play mode

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.

Game clock shown in Inspector

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

Shows created Entity in Inspector

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:

  1. 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.

  2. Next, you pass the archetype into entityManager.CreateEntity. This initializes the Entity.
  3. 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.

Drag and drop objects from Hierarchy to Inspector

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

Single drone in Game view

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.

Entity data in Inspector

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.

Entity Debugger window with Entity 1 selected

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.

Entity Debugger window with WorldTime selected

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.

Convert To Entity Component added to EnemyDrone prefab

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.

EnemyDrone entity in Entity Debugger window

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.

Can't add script error message window

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.

Move Forward Authoring component added in Inspector

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.

Entity component fields in Inspector

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<MoveForward>().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:

  1. MovementSystem inherits from ComponentSystem, which is an abstract class needed to implement a system.
  2. This must implement protected override void OnUpdate(), which invokes every frame.
  3. 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.

  4. 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).
  5. 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!

Drone moving in the z direction video

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-clickDelete 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.

Video of drone moving forward offscreen

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<Entity> enemyArray = new NativeArray<Entity>(spawnCount, Allocator.Temp);

    // 2
    for (int i = 0; i < enemyArray.Length; i++)
    {
        enemyArray[i] = entityManager.Instantiate(enemyEntityPrefab);

        // 3
        entityManager.SetComponentData(enemyArray[i], new Translation { Value = RandomPointOnCircle(spawnRadius) }); 

        // 4
        entityManager.SetComponentData(enemyArray[i], new MoveForward { speed = Random.Range(minSpeed, maxSpeed) }); 
    }

    // 5
    enemyArray.Dispose();

    // 6
    spawnCount += difficultyBonus;
}

Here's whats happening in SpawnWave:

  1. First, you declare a new NativeArray, enemyArray, with up to the spawnCount elements. Allocator.Temp indicates the NativeArray won’t need to persist once the setup is complete.
  2. Next, you loop through the array. Each iteration instantiates an Entity and stores it in enemyArray.
  3. Then, you find a 3D position using RandomPointOnCircle with the spawnRadius. That plugs into the Translation value with SetComponentData.
  4. You use SetComponentData to set the MoveForward speed to a Random.Range between minSpeed and maxSpeed.
  5. Once the loop has finished, NativeArray.Dispose frees any temporarily allocated memory.
  6. Finally, you can increment the spawnCount on each wave to make the game progressively harder. Keep your players on their toes!

In Play mode, the Scene view now shows a ring of enemy drones. The enemies spawn and move in the positive z-direction.

Ring formation of drones moving offscreen

Adjust the Enemy Spawner’s Spawn Count, and Spawn Radius to control the timing and density of the drones.

Activating the Player

Re-enable the PlayerTank and DemoManagers in the Hierarchy.

In Play mode, this restores some basic game logic. You can drive, but bullets don't shoot properly. They freeze in place without any forward motion.

Player tank moving and shooting

The Bullet already has ConvertToEntity. Thus, Unity will convert it to an Entity at runtime. Each bullet needs a little push to get going.

Edit the Bullet prefab and add the MoveForwardComponent. Set a speed of 50 and save.

Now you can shoot in Play mode. PlayerWeapon.cs instantiates bullet Entities that travel forward. They pass right through the enemies, but it's a start.

Player tank moving and shooting properly

FacePlayer System

Since the drones currently ignore you, it's your job to make them face you head-on and come straight at you!

In Scripts/ECS/Systems, create a new System called FacePlayerSystem.cs:

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

public class FacePlayerSystem : ComponentSystem
{
    // 1
    protected override void OnUpdate()
    {
        // 2
        if (GameManager.IsGameOver())
        {
            return;
        }

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

        // 4
        Entities.ForEach((Entity entity, ref Translation trans, ref Rotation rot) =>
        {
            // 5
            float3 direction = playerPos - trans.Value;
            direction.y = 0f;

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

Here:

  1. The System inherits from ComponentSystem. This expects an OnUpdate to run every frame.
  2. If the game is no longer active, GameManager.IsGameOver returns straight away.
  3. You store the player’s location using GameManager.GetPlayerPosition.
  4. 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.
  5. Then, you calculate the vector to the player, ignoring the y.
  6. 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.

Bullets and drones seeking out player

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<EnemyTag>().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.

Player tank shooting correctly

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<EnemyTag>().ForEach((Entity enemy, ref Translation enemyPos) =>
        {
            // 5
            playerPosition.y = enemyPos.Value.y;

            // 6
            if (math.distance(enemyPos.Value, playerPosition) <= thresholdDistance)
            {
                FXManager.Instance.CreateExplosion(enemyPos.Value);
                FXManager.Instance.CreateExplosion(playerPosition);
                GameManager.EndGame();

                // 7
                PostUpdateCommands.DestroyEntity(enemy);
            }

            // 8
            float3 enemyPosition = enemyPos.Value;

            // 9
            Entities.WithAll<BulletTag>().ForEach((Entity bullet, ref Translation bulletPos) =>
            {
                // 10
                if (math.distance(enemyPosition, bulletPos.Value) <= thresholdDistance)
                {
                    PostUpdateCommands.DestroyEntity(enemy);
                    PostUpdateCommands.DestroyEntity(bullet);

                    //11
                    FXManager.Instance.CreateExplosion(enemyPosition);
                    GameManager.AddScore(1);
                }
            });
        });
    }
}

This is the longest System yet:

  1. First, you define a minimum value, thresholdDistance, to register a collision.
  2. In OnUpdate, return if GameManager.IsGameOver signals the game is already over.
  3. On each frame, cache the player’s position.
  4. Loop through all Entities. Again, use the WithAll query with the EnemyTag. This time, the enemy Entity and Translation are input parameters for the lambda.
  5. Because you're concerned with the xz-plane, you can disregard the player y value.
  6. Check if the player and enemy are close enough. If so, then use the pre-configured FXManager to spawn explosions. Tell the GameManager to end the game.
  7. Use the PostUpdateCommands.DestroyEntity to remove the Entity. This is an Entity Command Buffer that waits for a safe time to remove any Entities or data.
  8. Similarly, check collisions between the enemy with the player bullets. Store the enemy position temporarily.
  9. Then, start a second Entities.ForEach. This time you loop through all Entities with a BulletTag. Use the bullet and its Translation as input parameters for the lambda.
  10. Check the distance between the enemy position and each bullet. If they're within the distance threshold, then boom! Invoke PostUpdateCommands.DestroyEntity to remove both.
  11. Finally, generate an explosion and add to the current score.

Enter Play mode to test.

Enemies exploding on being hit by player bullet

Now you can gun down enemy drones with a satisfying explosion each time. The game also ends if you crash into a drone by mistake.

If you die, you may need to exit Play mode. The leftover enemies make it difficult to restart. You can, however, clean that up with a few extra Systems.

More Systems and Cleanup

Your game demo is nearly complete. The final addition is the ability to remove any bullets that travel off-screen. Otherwise, they'll gradually eat memory as you keep shooting.

Import the IntroToECSExtras.unitypackage from the downloaded materials to add some scripts:

  • A Lifetime component data to define an active duration.
  • A TimeoutSystem to remove bullets or anything with an expired Lifetime.
  • A ClearOnRestartSystem to destroy any leftover enemy Entities before the game restarts.

Asset importing window

Peruse those scripts to see how they work. Or write them yourself. You should be an expert by now. :]

Now, edit the Bullet prefab, then drag and drop the LifetimeAuthoring component onto it.

Use a Value of 3. This gives the bullet enough time to clear frame. Save the prefab.

That should do it! Finally, your demo is in working order.

Enemies exploding around player tank

Adjust the number and frequency of the enemy attackers with the EnemySpawner settings. See how long you can withstand the alien onslaught!

Your game should run a pretty steady FPS even when hundreds of Entities are active.

Video showing gameplay with lots of active objects

Compare your work with the IntroToECSFinal project.

Note: You can enable invincibility to stress test your ECS demo. Simply comment out these two lines in the DestructionSystem.cs:
    //FXManager.Instance.CreateExplosion(playerPosition);
    //GameManager.EndGame();

Where to Go From Here

Use the Download Materials button at the top or bottom of this tutorial to download the final project.

ECS is one pillar of DOTS. To really get the benefits of performance by default, you need to build on it further using the Job System and Burst Compiler. Together they form a trifecta of efficiency. :]

To learn more about DOTS check these links:

Congratulations on completing the tutorial! If you have any questions, feel free to ask in the forums or to leave a comment below.