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.
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Sign up/Sign in
With a free Kodeco account you can download source code, track your progress, bookmark, personalise your learner profile and more!
Create accountAlready a member of Kodeco? Sign in
Contents
Entity Component System for Unity: Getting Started
30 mins
- Getting Started
- Stress Testing the Demo
- ECS: Performance by Default
- Removing Non-ECS Code
- Creating an Entity
- Adding Components
- ConvertToEntity
- MoveForward Component Data
- Authoring
- Movement System
- Making an Entity Prefab
- Spawning an Enemy Wave With NativeArray
- Activating the Player
- FacePlayer System
- Generating ComponentTags
- Destruction System
- More Systems and Cleanup
- Where to Go From Here
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.
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.
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.
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
andLocalToWorld
form the archetype. - Next, you pass the archetype into
entityManager.CreateEntity
. This initializes the Entity. - You then use
AddComponentData
andAddSharedComponent
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<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:
-
MovementSystem
inherits fromComponentSystem
, 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 staticForEach
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<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
:
- First, you declare a new NativeArray,
enemyArray
, with up to thespawnCount
elements.Allocator.Temp
indicates the NativeArray won’t need to persist once the setup is complete. - Next, you loop through the array. Each iteration instantiates an Entity and stores it in
enemyArray
. - Then, you find a 3D position using
RandomPointOnCircle
with thespawnRadius.
That plugs into the Translation value withSetComponentData
. - You use
SetComponentData
to set the MoveForward speed to aRandom.Range
betweenminSpeed
andmaxSpeed
. - Once the loop has finished,
NativeArray.Dispose
frees any temporarily allocated memory. - 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.
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.
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.
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:
- The System inherits from
ComponentSystem
. This expects anOnUpdate
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.
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.
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:
- First, you define a minimum value,
thresholdDistance
, to register a collision. - In
OnUpdate
, return ifGameManager.IsGameOver
signals the game is already over. - On each frame, cache the player’s position.
- 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. - Because you're concerned with the xz-plane, you can disregard the player y value.
- Check if the player and enemy are close enough. If so, then use the pre-configured
FXManager
to spawn explosions. Tell theGameManager
to end the game. - 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. - Similarly, check collisions between the enemy with the player bullets. Store the enemy position temporarily.
- 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. - 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. - Finally, generate an explosion and add to the current score.
Enter Play mode to test.
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.
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.
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.
Compare your work with the IntroToECSFinal project.
//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.
All videos. All books.
One low price.
A Kodeco subscription is the best way to learn and master mobile development — plans start at just $19.99/month! Learn iOS, Swift, Android, Kotlin, Flutter and Dart development and unlock our massive catalog of 50+ books and 4,000+ videos.
Learn more