Sistema de entidades e renderização

10

Okey, o que eu sei até agora; A entidade contém um componente (armazenamento de dados) que contém informações como; - Textura / sprite - Shader - etc

E então eu tenho um sistema de renderização que desenha tudo isso. Mas o que não entendo é como o renderizador deve ser projetado. Devo ter um componente para cada "tipo visual". Um componente sem shader, outro com shader, etc?

Só preciso de algumas informações sobre qual é a "maneira correta" de fazer isso. Dicas e armadilhas para observar.

hayer
fonte
2
Tente não tornar as coisas muito genéricas. Parece estranho ter uma entidade com um componente Shader e não um componente Sprite, portanto, talvez o Shader deva fazer parte do componente Sprite. Naturalmente, você precisará apenas de um sistema de renderização.
Jonathan Connell

Respostas:

7

Essa é uma pergunta difícil de responder, porque todos têm sua própria idéia sobre como um sistema de componentes de entidade deve ser estruturado. O melhor que posso fazer é compartilhar com você algumas das coisas que considero mais úteis para mim.

Entidade

Eu adoto a abordagem de classe gorda do ECS, provavelmente porque acho que métodos extremos de programação são altamente ineficientes (em termos de produtividade humana). Para esse fim, uma entidade para mim é uma classe abstrata a ser herdada por classes mais especializadas. A entidade possui várias propriedades virtuais e um sinalizador simples que informa se essa entidade deve ou não existir. Portanto, em relação à sua pergunta sobre um sistema de renderização, é Entityassim:

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

Os componentes são "estúpidos", pois não fazem nem sabem nada. Eles não têm referências a outros componentes e normalmente não têm funções (trabalho em C #, então uso propriedades para manipular getters / setters - se eles tiverem funções, serão baseados na recuperação de dados que possuem).

Sistemas

Os sistemas são menos "estúpidos", mas ainda são autômatos estúpidos. Eles não têm contexto do sistema geral, não têm referências a outros sistemas e não mantêm dados, exceto por alguns buffers de que talvez precisem para fazer seu processamento individual. Dependendo do sistema, ele pode ter um método especializado Updateou Draw, em alguns casos, ambos.

Interfaces

As interfaces são uma estrutura importante no meu sistema. Eles são usados ​​para definir o que um Systemprocesso pode e o que um Entityé capaz. As interfaces que são relevantes para a renderização são: IRenderablee IAnimatable.

As interfaces simplesmente informam ao sistema quais componentes estão disponíveis. Por exemplo, o sistema de renderização precisa conhecer a caixa delimitadora da entidade e a imagem a desenhar. No meu caso, isso seria oe SpatialComponento ImageComponent. Então fica assim:

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

O sistema Rendering

Então, como o sistema de renderização desenha uma entidade? Na verdade, é bastante simples, então mostrarei a classe simplificada para ter uma idéia:

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);
        }
    }
}

Olhando para a classe, o sistema de renderização nem sabe o que Entityé. Tudo o que sabe é IRenderablee é simplesmente dada uma lista deles para desenhar.

Como Tudo Funciona

Também pode ajudar a entender como eu crio novos objetos de jogo e como os alimento para os sistemas.

Criando entidades

Todos os objetos de jogo herdam da Entidade e quaisquer interfaces aplicáveis ​​que descrevem o que esse objeto de jogo pode fazer. Quase tudo o que é animado na tela fica assim:

public class MyAnimatedWidget : Entity, IRenderable, IAnimatable {}

Alimentando os sistemas

Eu mantenho uma lista de todas as entidades que existem no mundo do jogo em uma única lista chamada List<Entity> gameObjects. Cada quadro, então, vasculho essa lista e copio as referências de objetos para mais listas com base no tipo de interface, como List<IRenderable> renderableObjects, e List<IAnimatable> animatableObjects. Dessa forma, se sistemas diferentes precisarem processar a mesma entidade, eles poderão. Depois, simplesmente entrego essas listas a cada um dos sistemas Updateou Drawmétodos e deixo que os sistemas façam seu trabalho.

Animação

Você pode estar curioso para saber como o sistema de animação funciona. No meu caso, você pode querer ver a interface IAnimatable:

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

O principal a notar aqui é o ImageComponentaspecto da IAnimatableinterface não é somente leitura; tem um levantador .

Como você deve ter adivinhado, o componente de animação contém apenas dados sobre a animação; uma lista de quadros (que são componentes da imagem), o quadro atual, o número de quadros por segundo a ser desenhado, o tempo decorrido desde o último incremento do quadro e outras opções.

O sistema de animação tira proveito do sistema de renderização e da relação do componente de imagem. Simplesmente altera o componente de imagem da entidade à medida que incrementa o quadro da animação. Dessa forma, a animação é renderizada indiretamente pelo sistema de renderização.

Cifra
fonte
Provavelmente, devo observar que realmente não sei se isso é parecido com o que as pessoas estão chamando de sistema de componente de entidade . Na minha tentativa de implementar um design baseado em composição, me vi caindo nesse padrão.
Cypher
Interessante! Não gosto muito da classe abstrata de sua Entidade, mas a interface IRenderable é uma boa ideia!
Jonathan Connell
5

Veja esta resposta para ver o tipo de sistema que estou falando.

O componente deve conter os detalhes sobre o que desenhar e como desenhá-lo. O sistema de renderização pega esses detalhes e desenha a entidade da maneira especificada pelo componente. Somente se você usasse tecnologias de desenho significativamente diferentes, teria componentes separados para estilos separados.

MichaelHouse
fonte
3

O principal motivo para separar a lógica em componentes é criar um conjunto de dados que, quando combinados em uma entidade, produzem um comportamento útil e reutilizável. Por exemplo, separar uma entidade em um PhysicsComponent e RenderComponent faz sentido, pois é provável que nem todas as entidades tenham Física e algumas entidades talvez não tenham Sprite.

Para responder sua pergunta, você precisa examinar sua arquitetura e fazer duas perguntas a si mesmo:

  1. Faz sentido ter um Shader sem textura
  2. Separar Shader de Texture permitirá evitar a duplicação de código?

Ao dividir um componente, é importante fazer esta pergunta, se a resposta para 1. for sim, você provavelmente terá um bom candidato para criar dois componentes separados, um com um sombreador e outro com textura. A resposta para 2. geralmente é sim para componentes como Posição, onde vários componentes podem usar a posição.

Por exemplo, Física e Áudio podem usar a mesma posição; em vez de ambos os componentes que armazenam posições duplicadas, você as refatorar em um PositionComponent e exigir que as entidades que usam PhysicsComponent / AudioComponent também tenham um PositionComponent.

Com base nas informações que você nos forneceu, não parece que seu RenderComponent seja um bom candidato para se dividir em TextureComponent e ShaderComponent como shader's são totalmente dependentes de Texture e nada mais.

Supondo que você esteja usando algo semelhante ao T-Machine: Entity Systems, uma implementação de amostra de um RenderComponent & RenderSystem em C ++ seria algo como isto:

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;
}
Jake Woods
fonte
Isso está completamente errado. O objetivo principal dos componentes é separá-los das entidades, não fazer com que os sistemas de renderização pesquisem pelas entidades para encontrá-los. Os sistemas de renderização devem controlar totalmente seus próprios dados. PS Não coloque std :: vector (especialmente com dados da instância) em loops, isso é horrível (lento) em C ++.
snake5
@ snake5 você está correto em ambos os aspectos. Eu digitei o código no topo da minha cabeça e houve alguns problemas, obrigado por apontá-los. Corrigi o código afetado para ser menos lento e usar corretamente os idiomas do sistema de entidades.
Jake Woods,
2
@ snake5 Você não está recalculando dados a cada quadro, getComponents retorna um vetor de propriedade de m_manager que já é conhecido e só muda quando você adiciona / remove componentes. É uma vantagem quando você tem um sistema que deseja usar vários componentes da mesma entidade, por exemplo, um PhysicsSystem que deseja usar PositionComponent e PhysicsComponent. Outros sistemas provavelmente desejarão a posição e, ao ter um PositionComponent, você não terá dados duplicados. Principalmente, resolve o problema de como os componentes se comunicam.
Jake Woods,
5
@ snake5 A questão não é sobre como o sistema CE deve ser organizado ou sobre o desempenho. A questão é sobre a configuração do sistema de renderização. Existem várias maneiras de estruturar um sistema EC, não fique preso nos problemas de desempenho um do outro aqui. Provavelmente, o OP está usando uma estrutura de CE completamente diferente de suas respostas. O código fornecido nesta resposta serve apenas para mostrar melhor o exemplo, não para ser criticado por seu desempenho. Se a pergunta fosse sobre desempenho, talvez isso tornasse a resposta "não útil", mas não é.
Michaelhouse
2
Eu prefiro o design apresentado nesta resposta do que no Cyphers. É muito parecido com o que eu uso. Os componentes menores são melhores na imo, mesmo que tenham apenas uma ou duas variáveis. Eles deveriam definir um aspecto de uma entidade, como se meu componente "Danificável" tivesse 2, talvez 4 variáveis ​​(máxima e atual para cada vida e armadura). Esses comentários estão ficando longos, vamos para o bate - papo, se você quiser discutir mais.
31512 John Mc
2

Armadilha # 1: código superprojetado. Pense se você realmente precisa de tudo o que implementa, porque terá que conviver com isso por algum tempo.

Armadilha # 2: muitos objetos. Eu não usaria um sistema com muitos objetos (um para cada tipo, subtipo e qualquer outro), porque isso dificulta o processamento automatizado. Na minha opinião, é muito melhor ter cada objeto controlar um determinado conjunto de recursos (em oposição a um recurso). Por exemplo, a criação de componentes para cada bit de dados incluído na renderização (componente de textura, componente de sombreamento) é muito dividida - você normalmente precisa ter todos esses componentes juntos de qualquer maneira, não concorda?

Armadilha 3: controle externo muito estrito. Prefira alterar nomes a objetos de sombreamento / textura, pois os objetos podem mudar com o renderizador / tipo de textura / formato de sombreamento / qualquer outra coisa. Os nomes são identificadores simples - cabe ao representante decidir o que fazer com eles. Um dia, você pode querer ter materiais em vez de shaders simples (adicione shaders, texturas e modos de mesclagem dos dados, por exemplo). Com uma interface baseada em texto, é muito mais fácil implementar isso.

Quanto ao renderizador, pode ser uma interface simples que cria / destrói / mantém / renderiza objetos criados por componentes. A representação mais primitiva poderia ser algo como isto:

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

Isso permitiria gerenciar esses objetos a partir de seus componentes e mantê-los longe o suficiente para permitir que você os processasse da maneira que desejar.

snake5
fonte