Sistema de entidades y renderizado


11

Okey, lo que sé hasta ahora; La entidad contiene un componente (almacenamiento de datos) que contiene información como; - Textura / sprite - Shader - etc.

Y luego tengo un sistema de representación que dibuja todo esto. Pero lo que no entiendo es cómo debe diseñarse el renderizador. ¿Debo tener un componente para cada "tipo visual". ¿Un componente sin sombreador, uno con sombreador, etc.?

Solo necesito alguna información sobre cuál es la "forma correcta" de hacer esto. Consejos y trampas a tener en cuenta.


2
Intenta no hacer las cosas demasiado genéricas. Parecería extraño tener una entidad con un componente Shader y no un componente Sprite, por lo que quizás el Shader debería ser parte del componente Sprite. Naturalmente, solo necesitará un sistema de renderizado.
Jonathan Connell

Respuestas:


8

Esta es una pregunta difícil de responder porque todos tienen su propia idea sobre cómo debe estructurarse un sistema de componentes de la entidad. Lo mejor que puedo hacer es compartir con ustedes algunas de las cosas que me han resultado más útiles.

Entidad

Tomo el enfoque de clase gorda de ECS, probablemente porque encuentro que los métodos extremos de programación son altamente ineficientes (en términos de productividad humana). Para ese fin, una entidad para mí es una clase abstracta que heredarán clases más especializadas. La entidad tiene varias propiedades virtuales y un indicador simple que me dice si esta entidad debería existir o no. Entonces, en relación con su pregunta sobre un sistema de renderizado, esto es lo que Entityparece:

public abstract class Entity {
    public bool IsAlive = true;
    public virtual SpatialComponent   Spatial   { get; set; }
    public virtual ImageComponent     Image     { get; set; }
    public virtual AnimationComponent Animation { get; set; }
    public virtual InputComponent     Input     { get; set; }
}

Componentes

Los componentes son "estúpidos" porque no hacen ni saben nada. No tienen referencias a otros componentes y, por lo general, no tienen funciones (trabajo en C #, por lo que utilizo propiedades para manejar getters / setters; si tienen funciones, se basan en la recuperación de datos que tienen).

Sistemas

Los sistemas son menos "estúpidos", pero siguen siendo autómatas tontos. No tienen contexto del sistema en general, no tienen referencias a otros sistemas y no tienen datos, excepto por algunos buffers que pueden necesitar para su procesamiento individual. Dependiendo del sistema, que puede tener un especializado Update, o Drawmétodo, o en algunos casos, ambas cosas.

Interfaces

Las interfaces son una estructura clave en mi sistema. Se utilizan para definir qué Systempuede procesar y de qué Entityes capaz. Las interfaces que son relevantes para la representación son: IRenderabley IAnimatable.

Las interfaces simplemente le dicen al sistema qué componentes están disponibles. Por ejemplo, el sistema de representación necesita conocer el cuadro delimitador de la entidad y la imagen a dibujar. En mi caso, ese sería el SpatialComponenty el ImageComponent. Entonces se ve así:

public interface IRenderable {
    SpatialComponent Component { get; }
    ImageComponent   Image     { get; }
}

El sistema de renderizado

Entonces, ¿cómo dibuja el sistema de representación una entidad? En realidad es bastante simple, así que solo te mostraré la clase simplificada para darte una idea:

public class RenderSystem {
    private SpriteBatch batch;
    public RenderSystem(SpriteBatch batch) {
        this.batch = batch;
    }
    public void Draw(List<IRenderable> list) {
        foreach(IRenderable obj in list) {
            this.batch.draw(
                obj.Image.Texture,
                obj.Spatial.Position,
                obj.Image.Source,
                Color.White);
        }
    }
}

Mirando la clase, el sistema de renderizado ni siquiera sabe qué Entityes un . Todo lo que sabe es IRenderabley simplemente se le da una lista de ellos para dibujar.

Cómo funciona todo

Puede ser útil comprender también cómo creo nuevos objetos de juego y cómo los alimento a los sistemas.

Crear entidades

Todos los objetos del juego heredan de Entity, y cualquier interfaz aplicable que describa lo que ese objeto del juego puede hacer. Casi todo lo que se anima en la pantalla se ve así:

public class MyAnimatedWidget : Entity, IRenderable, IAnimatable {}

Alimentando los sistemas

Mantengo una lista de todas las entidades que existen en el mundo del juego en una sola lista llamada List<Entity> gameObjects. Cada cuadro, luego examino esa lista y copio referencias de objetos a más listas basadas en el tipo de interfaz, como List<IRenderable> renderableObjects, y List<IAnimatable> animatableObjects. De esta manera, si diferentes sistemas necesitan procesar la misma entidad, pueden hacerlo. Luego simplemente entrego esas listas a cada uno de los sistemas Updateo Drawmétodos y dejo que los sistemas hagan su trabajo.

Animación

Quizás tengas curiosidad por saber cómo funciona el sistema de animación. En mi caso, es posible que desee ver la interfaz IAnimatable:

public interface IAnimatable {
    public AnimationComponent Animation { get; }
    public ImageComponent Image         { get; set; }
}

La clave para notar aquí es que el ImageComponentaspecto de la IAnimatableinterfaz no es de solo lectura; Tiene un setter .

Como habrás adivinado, el componente de animación solo contiene datos sobre la animación; una lista de fotogramas (que son componentes de imagen), el fotograma actual, el número de fotogramas por segundo que se dibujarán, el tiempo transcurrido desde el último incremento de fotogramas y otras opciones.

El sistema de animación aprovecha el sistema de representación y la relación del componente de imagen. Simplemente cambia el componente de imagen de la entidad a medida que incrementa el marco de la animación. De esa manera, la animación se representa indirectamente por el sistema de representación.


Probablemente debería tener en cuenta que realmente no sé si esto está incluso cerca de lo que las personas llaman un sistema de componente de entidad . En mi intento de implementar un diseño basado en la composición, me encontré cayendo en este patrón.
Cypher

¡Interesante! No estoy demasiado interesado en la clase abstracta para su entidad, ¡pero la interfaz IRenderable es una buena idea!
Jonathan Connell

5

Vea esta respuesta para ver el tipo de sistema del que estoy hablando.

El componente debe contener los detalles de qué dibujar y cómo dibujarlo. El sistema de representación tomará esos detalles y dibujará la entidad en la forma especificada por el componente. Solo si usara tecnologías de dibujo significativamente diferentes tendría componentes separados para estilos separados.


3

La razón clave para separar la lógica en componentes es crear un conjunto de datos que, cuando se combinan en una entidad, producen un comportamiento útil y reutilizable. Por ejemplo, separar una Entidad en un Componente de Física y un Componente de Render tiene sentido, ya que es probable que no todas las entidades tengan Física y algunas entidades no tengan Sprite.

Para responder a su pregunta, debe mirar su arquitectura y hacerse dos preguntas:

  1. ¿Tiene sentido tener un Shader sin textura?
  2. ¿Separar Shader de Texture me permitirá evitar la duplicación de código?

Al dividir un componente es importante hacer esta pregunta, si la respuesta a 1. es sí, entonces probablemente tenga un buen candidato para crear dos componentes separados, uno con un Shader y otro con Texture. La respuesta a 2. generalmente es sí para componentes como Position, donde varios componentes pueden usar position.

Por ejemplo, tanto Physics como Audio pueden usar la misma posición, en lugar de que ambos componentes almacenen posiciones duplicadas, las refactorice en un PositionComponent y requiera que las entidades que usan PhysicsComponent / AudioComponent también tengan un PositionComponent.

Según la información que nos ha proporcionado, no parece que su RenderComponent sea un buen candidato para dividirse en un TextureComponent y un ShaderComponent, ya que los sombreadores dependen totalmente de Texture y nada más.

Suponiendo que esté usando algo similar a T-Machine: Entity Systems, una implementación de muestra de RenderComponent & RenderSystem en C ++ se vería así:

struct RenderComponent {
    Texture* textureData;
    Shader* shaderData;
};

class RenderSystem {
    public:
        RenderSystem(EntityManager& manager) :
            m_manager(manager) {
            // Initialize Window, rendering context, etc...
        }

        void update() {
            // Get all the entities with RenderComponent
            std::vector<RenderComponent>& components = m_manager.getComponents<RenderComponent>();

            for(auto component = components.begin(); entity != components.end(); ++components) {
                // Do something with the texture
                doSomethingWithTexture(component->textureData);

                // Do something with the shader if it's not null
                if(component->shaderData != nullptr) {
                    doSomethingWithShader(component->shaderData);
                }
            }
        }
    private:
        EntityManager& m_manager;
}

Eso está completamente mal. El objetivo de los componentes es separarlos de las entidades, no hacer que los sistemas de renderizado busquen en las entidades para encontrarlos. Los sistemas de procesamiento deben controlar completamente sus propios datos. PD No ponga std :: vector (especialmente con datos de instancia) en bucles, eso es horrible (lento) C ++.
serpiente5

@ snake5 tienes razón en ambos aspectos. Escribí el código en la parte superior de mi cabeza y hubo algunos problemas, gracias por señalarlos. He arreglado el código afectado para que sea menos lento y use correctamente las expresiones idiomáticas del sistema de entidades.
Jake Woods, el

2
@ snake5 No está recalculando datos en cada cuadro, getComponents devuelve un vector propiedad de m_manager que ya se conoce y solo cambia cuando agrega / elimina componentes. Es una ventaja cuando tiene un sistema que quiere usar múltiples componentes de la misma entidad, por ejemplo, un PhysicsSystem que quiere usar PositionComponent y PhysicsComponent. Es probable que otros sistemas deseen la posición y, al tener un Componente de posición, no tiene datos duplicados. Principalmente resuelve el problema de cómo se comunican los componentes.
Jake Woods, el

55
@ snake5 La pregunta no se trata de cómo se debe diseñar el sistema de la CE o de su rendimiento. La pregunta es sobre la configuración del sistema de renderizado. Hay varias formas de estructurar un sistema EC, no se deje atrapar por los problemas de rendimiento de uno sobre otro aquí. Es probable que el OP esté usando una estructura EC completamente diferente a cualquiera de sus respuestas. El código provisto en esta respuesta solo pretende mostrar mejor el ejemplo, no ser criticado por su rendimiento. Si la pregunta fuera sobre el rendimiento, tal vez esto haría que la respuesta "no sea útil", pero no lo es.
MichaelHouse

2
Prefiero el diseño presentado en esta respuesta que en Cyphers. Es muy similar al que yo uso. Los componentes más pequeños son mejores imo, incluso si tienen solo una o dos variables. Deben definir un aspecto de una entidad, como mi componente "Damagable" tendría 2, tal vez 4 variables (máximo y actual para cada salud y armadura). Estos comentarios se están haciendo largos, pasemos a chatear si quieres discutir más.
John McDonald

2

Peligro # 1: código sobrediseñado Piense si realmente necesita todo lo que implementa porque va a tener que vivir con él durante bastante tiempo.

Peligro # 2: demasiados objetos. No usaría un sistema con demasiados objetos (uno para cada tipo, subtipo y lo que sea) porque solo dificulta el procesamiento automatizado. En mi opinión, es mucho mejor que cada objeto controle un determinado conjunto de características (en lugar de una característica). Por ejemplo, hacer componentes para cada bit de datos incluidos en el renderizado (componente de textura, componente de sombreado) está demasiado dividido; de todos modos, normalmente tendría que tener todos esos componentes juntos, ¿no estaría de acuerdo?

Peligro # 3: control externo demasiado estricto. Prefiere cambiar los nombres a objetos de sombreado / textura porque los objetos pueden cambiar con el renderizador / tipo de textura / formato de sombreador / lo que sea. Los nombres son identificadores simples: depende del procesador decidir qué hacer con ellos. Es posible que algún día desee tener materiales en lugar de sombreadores simples (agregue sombreadores, texturas y modos de fusión a partir de datos, por ejemplo). Con una interfaz basada en texto, es mucho más fácil implementar eso.

En cuanto al renderizador, puede ser una interfaz simple que crea / destruye / mantiene / renderiza objetos creados por componentes. La representación más primitiva de la misma podría ser algo como esto:

class Renderer {
    function Draw() { ... }
    function AddSprite( ... ) { ... return sprite; }
    function RemoveSprite( sprite ) { ... }
    ...
};

Esto le permitiría administrar estos objetos desde sus componentes y mantenerlos lo suficientemente lejos como para que pueda renderizarlos de la manera que desee.

Al usar nuestro sitio, usted reconoce que ha leído y comprende nuestra Política de Cookies y Política de Privacidad.
Licensed under cc by-sa 3.0 with attribution required.