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 (48) · 5 Reviews

Download materials
Save for later
Share
You are currently viewing page 4 of 5 of this article. Click here to view the first page.

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