Hola a todos!
Aquí comienza la cuarta transmisión
"C ++ Developer" , uno de los cursos más activos en nuestro país, a juzgar por reuniones reales, donde no solo los "cruzados" vienen a hablar con
Dima Shebordaev :) En general, el curso ya ha crecido uno de los más grandes en nuestro país, se ha mantenido sin cambios que
Dima conduce lecciones abiertas y seleccionamos materiales interesantes antes del comienzo del curso.
Vamos!
Entrada
El Sistema de componentes de la entidad (ECS, "sistema de componentes de la entidad") está ahora en la cima de la popularidad como una alternativa arquitectónica que enfatiza el principio de Composición sobre la herencia. En este artículo, no entraré en los detalles del concepto, ya que ya hay suficientes recursos sobre este tema. Hay muchas formas de implementar ECS, y, con mucha frecuencia, elijo otras bastante complejas que pueden confundir a los principiantes y tomar mucho tiempo.
En esta publicación, describiré una forma muy simple de implementar ECS, cuya versión funcional casi no requiere código, pero sigue completamente el concepto.

ECS
Hablando de ECS, las personas a menudo quieren decir cosas diferentes. Cuando hablo de ECS, me refiero a un sistema que le permite definir entidades que tienen cero o más componentes de datos puros. Estos componentes son procesados selectivamente por sistemas lógicos puros. Por ejemplo, la posición, la velocidad, el hitbox y la salud de un componente están vinculados a la entidad E. Simplemente almacenan datos en sí mismos. Por ejemplo, un componente de salud puede almacenar dos enteros: uno para el estado actual y otro para el máximo. Un sistema puede ser un sistema de regeneración de salud que encuentra todas las instancias de un componente de salud y las aumenta en 1 cada 120 cuadros.
Implementación típica de C ++
Hay muchas bibliotecas que ofrecen implementaciones de ECS. Por lo general, incluyen uno o más elementos de la lista:
- Herencia del componente / sistema base de la clase
GravitySystem : public ecs::System
; - Uso activo de plantillas;
- Tanto eso como otro en un aspecto CRTP ;
- La clase
EntityManager
, que controla la creación / almacenamiento de entidades de manera implícita.
Algunos ejemplos rápidos de google:
Todos estos métodos tienen derecho a la vida, pero hay algunos inconvenientes en ellos. La forma en que procesan los datos de manera opaca significa que será difícil entender lo que está sucediendo dentro y si se ha producido una desaceleración del rendimiento. También significa que debe estudiar toda la capa de abstracción y asegurarse de que se ajuste bien al código existente. No se olvide de los errores ocultos, que probablemente están ocultos mucho en la cantidad de código que debe depurar.
Un enfoque basado en plantillas puede afectar en gran medida el tiempo de compilación y la frecuencia con la que tendrá que reconstruir la compilación. Si bien los conceptos basados en la herencia pueden degradar el rendimiento.
La razón principal por la que creo que estos enfoques son excesivos es que el problema que resuelven es demasiado simple. Al final, estos son solo componentes de datos adicionales asociados con la entidad y su procesamiento selectivo. A continuación, mostraré una forma muy simple de cómo se puede implementar esto.
Mi enfoque simple
EsenciaEn algunos enfoques, la clase Entity está definida, en otros, trabajan con entidades como ID / handle. En un enfoque por componentes, una entidad no es más que los componentes asociados con ella, y para esto no se necesita una clase. Una entidad existirá explícitamente en función de sus componentes relacionados. Para hacer esto, defina:
using EntityID = int64_t;
Componentes de entidadLos componentes son diferentes tipos de datos asociados con entidades existentes. Podemos decir que para cada entidad e, e tendrá cero y más tipos de componentes accesibles. En esencia, esta es una relación clave-valor explotada y, afortunadamente, existen herramientas de biblioteca estándar en forma de tarjetas para esto.
Entonces, defino los componentes de la siguiente manera:
struct Position { float x; float y; }; struct Velocity { float x; float y; }; struct Health { int max; int current; }; template <typename Type> using ComponentMap = std::unordered_map<EntityID, Type>; using Positions = ComponentMap<Position>; using Velocities = ComponentMap<Velocity>; using Healths = ComponentMap<Health>; struct Components { Positions positions; Velocities velocities; Healths healths; };
Esto es suficiente para indicar entidades a través de componentes, como se esperaba de ECS. Por ejemplo, para crear una entidad con una posición y salud, pero sin velocidad, necesita:
Para destruir una entidad con una ID dada, simplemente
.erase()
de cada tarjeta.
SistemasEl último componente que necesitamos son los sistemas. Esta es la lógica que funciona con componentes para lograr un comportamiento específico. Como me gusta simplificar las cosas, uso funciones normales. El sistema de regeneración de salud mencionado anteriormente puede ser simplemente la siguiente función.
void updateHealthRegeneration(int64_t currentFrame, Healths& healths) { if(currentFrame % 120 == 0) { for(auto& [id, health] : healths) { if(health.current < health.max) ++health.current; } } }
Podemos poner la llamada a esta función en un lugar apropiado en el bucle principal y transferirla al almacenamiento del componente de salud. Dado que el repositorio de salud contiene solo registros de entidades que tienen salud, puede procesarlos de forma aislada. También significa que la función toma solo los datos necesarios y no toca lo irrelevante.
Pero, ¿qué pasa si el sistema funciona con más de un componente? Digamos un sistema físico que cambia de posición según la velocidad. Para hacer esto, necesitamos intersectar todas las claves de todos los tipos de componentes involucrados e iterar sobre sus valores. En este punto, la biblioteca estándar ya no es suficiente, pero escribir ayudantes no es tan difícil. Por ejemplo:
void updatePhysics(Positions& positions, const Velocities& velocities) {
O puede escribir un ayudante más compacto que permita un acceso más eficiente a través de la iteración en lugar de buscar.
void updatePhysics(Positions& positions, const Velocities& velocities) {
Por lo tanto, nos familiarizamos con la funcionalidad básica de un ECS regular.
Los beneficios
Este enfoque es muy efectivo, ya que está construido desde cero sin restringir la abstracción. No tiene que integrar bibliotecas externas o adaptar la base del código a las ideas predefinidas de lo que deberían ser las Entidades / Componentes / Sistemas.
Y dado que este enfoque es completamente transparente, sobre la base puede crear cualquier utilidad y ayuda. Esta implementación crece con las necesidades de su proyecto. Lo más probable es que para prototipos simples o juegos para el juego jam'ov, tenga suficiente de la funcionalidad descrita anteriormente.
Por lo tanto, si es nuevo en todo este campo de ECS, un enfoque tan sencillo ayudará a comprender las ideas principales.
Limitaciones
Pero, como con cualquier otro método, hay algunas limitaciones. En mi experiencia, es precisamente una implementación de este tipo que utiliza
unordered_map
en cualquier juego no trivial lo que provocará problemas de rendimiento.
La iteración de intersecciones clave en varias instancias de
unordered_map
con múltiples entidades no se escala bien porque en realidad está haciendo búsquedas
N*M
, donde N es el número de componentes superpuestos, M es el número de entidades coincidentes y
unordered_map
no
unordered_map
muy bueno para el almacenamiento en caché. Este problema se puede solucionar mediante el uso de un almacén de valores clave más adecuado para la iteración en lugar de
unordered_map
.
Otra limitación es la calderería. Dependiendo de lo que haga, identificar nuevos componentes puede volverse tedioso. Es posible que deba agregar un anuncio no solo en la estructura Componentes, sino también en la función de generación, serialización, la utilidad de depuración, etc. Me encontré con esto y resolví el problema generando código: definí componentes en archivos json externos y luego generé componentes C ++ y funciones auxiliares en la etapa de construcción. Estoy seguro de que puede encontrar otros métodos basados en plantillas para solucionar cualquier problema repetitivo que encuentre.
El fin
Si tiene preguntas y comentarios, puede dejarlos aquí o ir a
una lección abierta con
Dima , escucharlo y preguntar por ahí.