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
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();
//FXManager.Instance.CreateExplosion(playerPosition);
//GameManager.EndGame();