Product roadmap

En DOTS: Sistema de componentes de entidad

LUCAS MEIJER / UNITY TECHNOLOGIESContributor
Mar 8, 2019|9 minutos
En DOTS: Sistema de componentes de entidad
Para tu comodidad, tradujimos esta página mediante traducción automática. No podemos garantizar la precisión ni la confiabilidad del contenido traducido. Si tienes alguna duda sobre la precisión del contenido traducido, consulta la versión oficial en inglés de la página web.

Esta es una de varias entradas sobre nuestra nueva pila tecnológica orientada a los datos (DOTS), en la que compartimos algunas ideas sobre cómo y por qué hemos llegado hasta donde estamos hoy, y hacia dónde nos dirigimos ahora.

En mi último post, hablé de HPC# y Burst como tecnologías fundacionales de bajo nivel para Unity de cara al futuro. Me gusta referirme a este nivel de nuestra pila como el "motor del juego". Cualquiera puede utilizar esta pila para escribir un motor de juego. Podemos. Lo haremos. Tú también puedes. ¿No te gusta la nuestra? Escriba el suyo o modifique el nuestro a su gusto.

Sistema de componentes de Unity

La siguiente capa que estamos construyendo encima es un nuevo sistema de componentes. Unity siempre se ha centrado en los conceptos de componentes. Añades un componente Rigidbody a un GameObject y empezará a caer. Añades un componente Light a un GameObject y empezará a emitir luz. Añade un componente AudioEmitter y el GameObject empezará a producir sonido.

Es un concepto muy natural tanto para programadores como para no programadores, y fácil de crear interfaces de usuario intuitivas. La verdad es que me sorprende lo bien que ha envejecido este concepto. Tan bien que queremos conservarlo.

Lo que no ha envejecido bien es cómo implantamos nuestro sistema de componentes. Se escribió con una mentalidad orientada a objetos. Components y GameObjects son objetos "heavy c++". Crearlos/destruirlos requiere un bloqueo mutex para modificar la lista global de id->objectpointers. Todos los GameObjects tienen un nombre. Cada uno recibe un objeto envoltorio C# que apunta al objeto C++. Ese objeto de C# puede estar en cualquier parte de la memoria. El objeto de C++ también puede estar en cualquier parte de la memoria. Muchos fallos de caché. Intentamos mitigar los síntomas lo mejor que podemos, pero no se puede hacer mucho.

Con una mentalidad orientada a los datos, podemos hacerlo mucho mejor. Podemos mantener las mismas propiedades agradables desde el punto de vista del usuario (añade un componente Rigidbody, y la cosa se caerá), pero también obtener un rendimiento y un paralelismo asombrosos con nuestro nuevo sistema de componentes.

Este nuevo sistema de componentes es nuestro Entity Component System (ECS). A grandes rasgos, lo que se hace con un GameObject hoy se hace con una Entidad en el nuevo sistema. Los componentes siguen llamándose componentes. ¿Cuál es la diferencia? La disposición de los datos.

Veamos algunos patrones habituales de acceso a datos
Un componente típico que usted escribiría en Unity de la manera tradicional podría verse así:

clase Órbita : MonoComportamiento
{
public Transformar _objetoAlrededorDeOrbita;

void Actualizar()
{
//por favor, ignora esta matemática está todo roto, ese no es el punto aquí :)
var currentPos = GetComponent<Transform>().position;
var targetPos = _objectToOrbitAround.position;
GetComponent<RigidBody>().velocity += SomehowSteerTowards(currentPos,targetPos)
}
}

Este patrón se repite una y otra vez. Un componente tiene que encontrar uno o más componentes en el mismo GameObject y leer/escribir algunos valores en él.

Hay muchas cosas mal en esto:

  • Se llama al método Update() para un único componente de la órbita. La siguiente llamada a Update() podría ser para un componente completamente diferente, causando probablemente que este código sea desalojado de la caché la próxima vez que tenga que ejecutar esta trama para otro componente Orbit.
  • Update() tiene que usar GetComponent() para ir y encontrar su Rigidbody. (Podría almacenarse en caché en su lugar, pero entonces habría que tener cuidado de que no se destruya el componente Rigidbody).
  • Los otros componentes con los que operamos están en lugares completamente distintos de la memoria.

La disposición de datos que utiliza ECS reconoce que se trata de un patrón muy común y optimiza la disposición de la memoria para que este tipo de operaciones sean rápidas.

Disposición de datos ECS

ECS agrupa en memoria todas las entidades que tienen exactamente el mismo conjunto de componentes. Llama arquetipo a un conjunto de este tipo. Un ejemplo de arquetipo es: "Posición, velocidad, cuerpo rígido y colisionador". ECS asigna la memoria en trozos de 16k. Cada chunk sólo contendrá los datos de los componentes de las entidades de un único arquetipo.

En lugar de tener el método de actualización de usuario buscando otros componentes para operar en tiempo de ejecución, por instancia de Orbit, en ECS land tienes que declarar estáticamente "Quiero ejecutar algunas operaciones en todas las entidades que tienen tanto una velocidad como un Rigidbody y un componente Orbit. Para encontrar todas esas entidades, basta con encontrar todos los arquetipos que coincidan con una "consulta de búsqueda de componentes" específica. Cada arquetipo tiene una lista de Chunks donde se almacenan las entidades de ese arquetipo. Hacemos un bucle sobre todos esos chunks, y dentro de cada uno de los chunks, estamos haciendo un bucle lineal de memoria apretada, para leer y escribir los datos del componente. Este bucle lineal que ejecuta el mismo código en cada entidad también supone una probable oportunidad de vectorización para Burst.

En muchos casos, este proceso puede dividirse trivialmente en varios trabajos, haciendo que el código que opera el componente ECS se ejecute con una utilización de núcleos cercana al 100%.

ECS hace todo este trabajo por usted, sólo tiene que proporcionar el código que desea ejecutar en cada entidad. (Aunque puedes hacer la iteración de trozos manualmente si quieres).

Cuando se añade/elimina un componente de una Entidad, ésta cambia de arquetipo. Lo movemos de su trozo actual a un trozo del nuevo arquetipo, y volvemos a intercambiar la última entidad del trozo anterior para "rellenar el hueco".

En ECS, también se declara estáticamente lo que se pretende hacer con los datos del componente. Sólo lectura o Lectura-Escritura. Al prometer (la promesa se verifica) que sólo leerá del componente Posición, ECS puede conseguir una programación más eficiente de sus trabajos. Otros trabajos que también quieran leer del componente Posición no tendrán que esperar.

Esta disposición de los datos también nos permite hacer frente a una antigua frustración que hemos tenido, que son los tiempos de carga y el rendimiento de la serialización. Cargar/transmitir datos ECS para una escena grande no es mucho más que cargar bytes sin procesar desde el disco y utilizarlos tal cual.

Por eso la demo de Megacity se carga en unos segundos en un teléfono.

Accidentes" felices

Aunque las entidades pueden hacer lo mismo que los objetos de juego hoy en día, pueden hacer más porque son muy ligeras. De hecho, ¿qué es realmente una Entidad? En un borrador anterior de este post escribí "almacenamos entidades en trozos", y más tarde lo cambié por "almacenamos datos de componentes para entidades en trozos". Es importante distinguir que una entidad no es más que un número entero de 32 bits. No hay nada que almacenar o asignarle, salvo los datos de sus componentes. Como son tan baratos, puedes utilizarlos para escenarios para los que los objetos del juego no eran adecuados. Como usar una entidad para cada partícula individual en un sistema de partículas.

HPC#, Burst, ECS. Impresionante, pero ¿dónde está mi motor de juego?

La siguiente capa que tenemos que construir es muy grande. Es la capa del "motor del juego", compuesta por funciones como "renderizador", "física", "red", "entrada", "animación", etc. En eso estamos hoy. Hemos empezado a trabajar en estas piezas, pero no estarán listas de la noche a la mañana.

Eso puede parecer un fastidio. En cierto modo lo es, pero en otro, no. Como ECS y todo lo que se construye sobre él está escrito en C#, puede ejecutarse dentro del Unity tradicional. Como se ejecuta dentro de Unity, puedes escribir componentes ECS que utilicen funcionalidades pre-ECS. Ahora mismo no existe un sistema de dibujo de malla ECS puro. Sin embargo, puedes escribir un ECS MeshRenderSystem que utilice la API pre-ECS Graphics.DrawMeshIndirect como implementación, mientras esperas a que salga una versión ECS pura. Esta es exactamente la técnica que utiliza nuestra demo Megacity. La carga/reproducción/elaboración/LODding/animación se realiza con sistemas ECS puros, pero el dibujo final no.

Para que puedas mezclar y combinar. Lo bueno de esto es que ya puedes aprovechar las ventajas de Burst codegen y el rendimiento ECS para tu código de juego, en lugar de tener que esperar a que lancemos versiones ECS puras de todos los subsistemas. Lo que no es bueno es que, en esta fase de transición, puedes ver y sentir esa fricción que supone "utilizar dos mundos diferentes que están pegados".

Enviaremos todo el código fuente de nuestros subsistemas ECS HPC# en paquetes. Puedes inspeccionar, depurar y modificar cada subsistema, así como tener un control más preciso sobre cuándo quieres actualizar qué subsistema. Por ejemplo, puede actualizar el paquete del subsistema de Física sin actualizar nada más.

¿Qué pasará con los objetos de juego?

Los Game Objects no van a ninguna parte. Durante más de una década se han distribuido con éxito juegos increíbles en él. Esa fundación no va a ninguna parte.

Lo que cambiará es que, con el tiempo, nuestra energía para introducir mejoras dejará de dirigirse exclusivamente al mundo de los objetos de juego para dirigirse al mundo del ECS.

Usabilidad de la API / Boilerplate

Un punto común, muy válido, que la gente menciona al mirar ECS, es que hay un montón de mecanografía. Un montón de código repetitivo que se interpone entre usted y lo que está tratando de lograr.

Hay muchas mejoras en el horizonte que pretenden eliminar la necesidad de la mayor parte de la repetición de textos y simplificar la expresión de intenciones. Aún no hemos implementado muchos de ellos, ya que nos hemos centrado en el rendimiento básico, pero creemos que no hay ninguna buena razón para que el código del juego ECS tenga mucho código repetitivo, o sea particularmente más trabajoso de escribir que escribir un MonoBehaviour.

El proyecto Tiny ya ha implementado algunas de estas mejoras (como una API de iteración basada en lambda). Hablando de eso..

¿Cómo encaja en todo esto el ECS del Proyecto Tiny?

Project Tiny se desarrollará sobre el mismo ECS de C# del que se ha hablado en esta entrada del blog. El Proyecto Tiny será un gran hito de ECS para nosotros en varios sentidos:

  • Podrá funcionar en un entorno completo de sólo ECS. Un nuevo jugador sin cargas del pasado.
  • Eso significa que también es un ECS puro y tiene que incluir todos los subsistemas ECS que necesita un juego (diminuto) real.
  • Adoptaremos el soporte del Editor del Proyecto Tiny para la edición de Entidades para todos los escenarios ECS, no sólo tiny.
¿Te unes a nosotros?

Tenemos ofertas de trabajo para todas las partes de la pila DOTS, sobre todo en Burbank y Copenhague, echa un vistazo a careers.unity.com.

Además, asegúrate de unirte a nosotros en el foro de Unity Entity Component System y C# Job System para darnos tu opinión y obtener información sobre funciones experimentales y preliminares.