ECS (Entity Component System)

Curso de Unity 3D

21.  
40.  

ECS es una nueva forma de programar videojuegos en Unity.

ECS es un patrón de arquitectura generalmente usado en desarrollo de videojuegos que utiliza un diseño orientado a datos (DOTS) para optimizar el uso de CPU.

Unity mezcla, básicamente, dos paradigmas de programación:

  • Programación orientada a objetos. C# es un lenguaje de programación orientado a objetos y Unity utiliza C#.
  • Programación orientada a componentes. Desde la interfaz gráfica de Unity básicamente trabajas añadiendo y modificando componentes.

Con ECS, dividiremos nuestro juego en 3 partes:

  • Entities. Agrupan componentes. Son similares a los GameObjects, pero más ligeros.
  • Components. Contienen datos. Al contrario que los MonoBehaviour, no tienen lógica.
  • Systems. Definen el comportamiento de los componentes. Son los únicos componentes que tienen lógica.

Instalación

ECS esta disponible a partir de la versión 2019, aunque en esta versión todavía no estaba preparado para producción, habrá que instalarlo como Preview Package (paquetes Entities y Hybrid Renderer).

En la versión 2020, en el Package Manager habrá que seleccionar la opción Add package from git URL…

  • com.unity.entities
  • com.unity.rendering.hybrid

Para que Unity detecte correctamente el código fuente que vamos a codificar debemos asegurarnos de tener activa la última versión de la API de compatibilidad de .NET Framework (FileBuild SettingsPlayer SettingsOther SettingsApi Compatibility Level*).

Ejemplo: rotar un cubo

Debemos asociar a un cubo el siguiente Script, así como el componente ConvertToEntity.

Fíjate que todos los scripts siguientes utilizan Unity.Entities.

EntityData.cs

Este fichero contiene los datos que van a utilizar nuestras entidades. Estos datos deben deben ser un struct (como una clase, pero sin métodos, y sin poder ser nulos). Este struct debe implementar la interfaz IComponentData:

using System;
using Unity.Entities;

[Serializable]
public struct EntityData : IComponentData
{
    public float radiansPerSecond;

}

ScriptAuthoring.cs

using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;

[RequiresEntityConversion]
public class ScriptAuthoring : MonoBehaviour, IConvertGameObjectToEntity
{
    public float degreesPerSecond;
    public void Convert(Entity entity, EntityManager manager, GameObjectConversionSystem conversion)
    {
        EntityData data = new EntityData { radiansPerSecond = math.radians (degreesPerSecond)};
        manager.AddComponentData(entity, data);
    }
}

El objeto manager contiene todas las entidades de nuestra escena. Estas entidades estan vinculadas a unos datos (variable data).

ECSSystem.cs

Es el código que realmente lleva a cabo las acciones.

El método mul incrementa el valor de la rotación actual con la cantidad de radianes por segundo recogida.

using Unity.Entities;
using Unity.Mathematics;
using Unity.Transforms;

public class ECSSystem : ComponentSystem
{
protected override void OnUpdate()
    {
        Entities.ForEach((ref EntityData rotationSpeed, ref Rotation rotation) =>
        {
            float deltaTime = Time.DeltaTime;
            rotation.Value = math.mul(math.normalize(rotation.Value),
                quaternion.AxisAngle(math.up(), rotationSpeed.radiansPerSecond * deltaTime));
        });
    }
}

Las entidades no son visualizadas en el panel de Jerarquía. Para visualizarlas, debes ir al menú Window Analysis Entity Debugger. Es posible seleccionar la entidad en el panel Entity Debugger y ver sus datos en el panel inspector.

Ejercicio

Añadir al cubo una nueva propiedad llamada velocity que aplicaremos en el System mediante la propiedad ref Translation translation.

Manipulando múltiples entidades

FallCubeData.cs

using Unity.Entities;

public struct FallCubeData : IComponentData
{
    public float fallSpeed;
}

ECSMain.cs

using System.ComponentModel.Design;
using Unity.Collections;
using UnityEngine;
using Unity.Entities;
using Unity.Mathematics;
using Unity.Rendering;
using Unity.Transforms;
using UnityEngine.UI;
using Random = UnityEngine.Random;

public class ECSMain : MonoBehaviour
{

    [SerializeField]
    private int _createNum = 100;

    private EntityManager entityManager;
    private EntityArchetype fallCubeArch;
    
    
    void Start()
    {
        entityManager = World.Active.EntityManager;

        // Creamos la estructura de datos que va a tener cada entidad
        // Todas son nativas de Unity (y necesarias), menos FallCubeData, que es un tipo de dato que hemos definido nosotros
        fallCubeArch = entityManager.CreateArchetype(
            ComponentType.ReadWrite<LocalToWorld>(), 
            ComponentType.ReadWrite<Translation>(),
            ComponentType.ReadWrite<Rotation>(),
            ComponentType.ReadWrite<FallCubeData>(),
            ComponentType.ReadOnly<RenderMesh>());
            
        Create();
    }

    void Create()
    {
        // Creamos un array de 100 entidades (_createNum ) utilizando una dirección de memoria temporal para almacenarlas (Allocator.TempoJob)
        var array = new NativeArray<Entity>(_createNum, Allocator.TempJob);
        entityManager.CreateEntity(fallCubeArch, array);
        
        var cube = GameObject.CreatePrimitive(PrimitiveType.Cube);
        
        foreach (var entity in array)
        {
            // A cada entidad le pasamos la velocidad de caída
            entityManager.SetComponentData(entity, new FallCubeData
            {
                fallSpeed = Random.Range(1f, 10f)
            });
            
            // Ubicamos cada entidad en una posición diferente de la pantalla
            entityManager.SetComponentData(
                entity,
                new Translation
                {
                    Value = new float3(Random.Range(-100, 100), Random.Range(-100, 100), Random.Range(-100, 100))
                });


          
            // A todas las entidades les pondremos la misma malla compartida
            entityManager.SetSharedComponentData(entity, new RenderMesh
            {
                mesh = cube.GetComponent<MeshFilter>().sharedMesh,
                material = cube.GetComponent<MeshRenderer>().sharedMaterial,
                subMesh = 0,
                castShadows = UnityEngine.Rendering.ShadowCastingMode.Off,
                receiveShadows = false
            });
        }

        // Borramos la memoria reservada temporalmente para el array
        array.Dispose();

        // Eliminamos el GameObject para quedarnos solamente con la entidad
        Destroy(cube);
    }

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Space) )
        {
            Create();
        }
    }

}

FallCubeJob.cs

using Unity.Entities;
using Unity.Jobs;
using Unity.Transforms;

public class FallCubeJob : JobComponentSystem
{
    protected override JobHandle OnUpdate(JobHandle inputDeps)
    {
        // Instanciamos una estructura con un método Execute que será llamado automáticamente y que se encargará de desplazar cada cubo
        var job = new JobFall {dt = Time.DeltaTime};
        return  job.Schedule(this, inputDeps);
    }

    struct JobFall : IJobForEach<Translation, Rotation, FallCubeData>
    {
        public float dt;
        public void Execute(ref Translation pos, ref Rotation rot, ref FallCubeData data)
        {
            pos.Value.y -= data.fallSpeed * dt;
          
        }
    }
}

Ejercicio. Añadiendo rotación a los cubos

Utilizando los siguientes bloques de código en los lugares correctos, lograr que los cubos, además de caer, roten.

entityManager.SetComponentData(entity, new Rotation
{
  Value = Quaternion.AngleAxis(Random.Range(1, 360), Vector3.up)
});
rot.Value = math.mul(Quaternion.Euler(data.rotationSpeed* dt,data.rotationSpeed* dt,data.rotationSpeed* dt), rot.Value);
rotationSpeed = Random.Range(-50f, 50f)
Descargar ejemplo
← Hacer un terreno
Cómo hacer un terreno con Unity →