A really quick and dirty look at ECS and Entities.ForEach

I decided to investigate ECS during some Personal Development Time with the objective of diving into ECS and creating something quick and dirty.

I grabbed a project I had been using as a testbed previously which already had a character running around.

I imported the entities package and took a jump into various tutorials.

Here is the fun part. Most ECS tutorials, including parts of Unity Learn are out of date!

The best place to look on how to use ECS is the actual ECS repo from unity that can be found here:

https://github.com/Unity-Technologies/EntityComponentSystemSamples

I baked a navmesh, then created a capsule with a navmesh agent on it.

Diving into code, I first created an EnemyData script:

using UnityEngine;
using UnityEngine.AI;

public enum EnemyState
{
    Chase,
    Attack
}

public sealed class EnemyData : MonoBehaviour
{
    public Vector3 Position => transform.position;
    public float Speed;
    public float Health;
    public NavMeshAgent NavMeshAgent;
    public EnemyState EnemyState { private set; get; } = EnemyState.Chase;
    public void SetState(EnemyState enemyState)
    {
        EnemyState = enemyState;
    }
}

I then attached this MonoBehaviour to my enemy along with a Game Object Entity component.

I then created a GameManager singleton that would keep track of various aspects of the game, but most notably in this part, the player position.

using UnityEngine;

public class GameManager : MonoBehaviour
{
    public static GameManager Instance { private set; get; }

    [SerializeField]
    private GameObject _playerObject;
    public Vector3 PlayerPosition => _playerObject.transform.position;

    private void Awake()
    {
        Instance = this;
    }
}

The next part was actually creating the system to drive the enemies. I created a script called EnemySystem.cs

using Unity.Entities;
using UnityEngine;

public class EnemySystem : ComponentSystem
{
    private EntityQuery query;
    private const float kDistanceFromPlayer = 2f;

    protected override void OnCreate()
    {
        query =
            GetEntityQuery(
                ComponentType.ReadOnly<EnemyData>());
    }

    protected override void OnUpdate()
    {
        Entities.With(query).ForEach((Entity e, EnemyData enemy) =>
        {
            switch(enemy.EnemyState)
            {
                case EnemyState.Chase:
                    {
                        UpdateChaseState(enemy);
                        break;
                    }
                case EnemyState.Attack:
                    {
                        UpdateAttackState(enemy);
                        break;
                    }
            }
        });
    }

    private void UpdateChaseState(EnemyData enemyData)
    {
        enemyData.NavMeshAgent.isStopped = false;
        enemyData.NavMeshAgent.speed = enemyData.Speed;
        enemyData.NavMeshAgent.SetDestination(GameManager.Instance.PlayerPosition);
        if (Vector3.Distance(GameManager.Instance.PlayerPosition, enemyData.Position) < kDistanceFromPlayer)
        {
            enemyData.SetState(EnemyState.Attack);
        }
    }

    private void UpdateAttackState(EnemyData enemyData)
    {
        Debug.Log("I ATTACK YOU!!!!!");
        enemyData.NavMeshAgent.isStopped = true;

        if (Vector3.Distance(GameManager.Instance.PlayerPosition, enemyData.Position) > kDistanceFromPlayer)
        {
            enemyData.SetState(EnemyState.Chase);
        }
    }
}

Let’s take a look a bit deeper into the above.

A lot of older tutorials wil tell you to use GetEntities<>. To my knowledge, this no longer exists, or at least not in the form the tutorials explain it to you. Instead, when the system is created we create a query. We look for all entities with EnemyData.

Then in our update loop we use the ForEach to essentially loop over our objects and update them.

After adding a few more enemies in I ran the system and voila:

This is not the most optimal way of doing things however, as Entities.ForEach processes each set of data on the main thread.

It does however show how we are seperating the systems and components out.

In order to get this to run on multiple threads this will need further refactoring, however, ifyou want to start moving towards a more ECS architecture and start to get into the ECS pattern mentality