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 3 of 5 of this article. Click here to view the first page.

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:

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:

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

Each Entity increments its position by this amount and voila! It moves in its local positive z-direction.

  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.
    (input parameters) => {expression}
  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).
(input parameters) => {expression}

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