É razoável criar aplicativos (não jogos) usando uma arquitetura de componente-entidade-sistema?


24

Sei que, ao criar aplicativos (nativos ou da web), como os da Apple AppStore ou da loja de aplicativos do Google Play, é muito comum usar uma arquitetura Model-View-Controller.

No entanto, é razoável também criar aplicativos usando a arquitetura Component-Entity-System comum nos mecanismos de jogos?


11
Confira a arquitetura da Mesa da Luz: chris-granger.com/2013/01/24/the-ide-as-data
Hakan Deryal

Respostas:


39

No entanto, é razoável também criar aplicativos usando a arquitetura Component-Entity-System comum nos mecanismos de jogos?

Para mim, absolutamente. Trabalho em FX visual e estudei uma ampla variedade de sistemas nesse campo, suas arquiteturas (incluindo CAD / CAM), ávidos por SDKs e quaisquer documentos que me dessem uma idéia dos prós e contras das decisões arquitetônicas aparentemente infinitas que poderia ser feito, mesmo os mais sutis nem sempre causando um impacto sutil.

O VFX é bastante semelhante aos jogos, pois existe um conceito central de uma "cena", com viewports que exibem os resultados renderizados. Também tende a haver muito processamento em loop central girando em torno dessa cena constantemente em contextos de animação, onde pode haver física acontecendo, emissores de partículas gerando partículas, malhas sendo animadas e renderizadas, animações de movimento etc., e finalmente processá-las tudo para o usuário no final.

Outro conceito semelhante aos mecanismos de jogo, pelo menos, muito complexos, foi a necessidade de um aspecto de "designer", no qual os designers pudessem projetar cenas de maneira flexível, incluindo a capacidade de fazer sua própria programação leve (scripts e nós).

Descobri, ao longo dos anos, que a ECS se encaixava melhor. É claro que isso nunca é completamente divorciado da subjetividade, mas eu diria que parece fortemente dar o menor número de problemas. Ele resolveu muito mais problemas importantes com os quais estávamos sempre lutando, enquanto apenas nos dava alguns novos problemas menores em troca.

POO tradicional

As abordagens mais tradicionais de POO podem ser realmente fortes quando você tem uma compreensão firme dos requisitos de projeto antecipadamente, mas não dos requisitos de implementação. Seja através de uma abordagem de interface múltipla mais plana ou de uma abordagem hierárquica ABC mais aninhada, ele tende a consolidar o design e dificulta a mudança, além de tornar a implementação mais fácil e segura. Sempre existe a necessidade de instabilidade em qualquer produto que ultrapasse uma única versão; portanto, as abordagens de OOP tendem a distorcer a estabilidade (dificuldade de mudança e falta de motivos para mudança) em direção ao nível do design e instabilidade (facilidade de mudança e motivos de mudança) ao nível de implementação.

No entanto, contra a evolução dos requisitos do usuário final, tanto o design quanto a implementação podem precisar mudar com frequência. Você pode encontrar algo estranho, como uma forte necessidade do usuário final da criatura analógica que precisa ser vegetal e animal ao mesmo tempo, invalidando completamente todo o modelo conceitual que você construiu. As abordagens normais orientadas a objetos não o protegem aqui e, às vezes, podem tornar essas mudanças imprevistas e inesperadas ainda mais difíceis. Quando áreas muito críticas de desempenho estão envolvidas, os motivos das alterações no projeto se multiplicam ainda mais.

A combinação de várias interfaces granulares para formar a interface em conformidade de um objeto pode ajudar bastante na estabilização do código do cliente, mas não na estabilização dos subtipos que, às vezes, podem diminuir o número de dependências do cliente. Você pode ter uma interface sendo usada apenas por parte do seu sistema, por exemplo, mas com mil subtipos diferentes implementando essa interface. Nesse caso, manter os subtipos complexos (complexo porque eles têm muitas responsabilidades diferentes de interface a cumprir) pode se tornar o pesadelo, e não o código que os utiliza por meio de uma interface. OOP tende a transferir complexidade para o nível do objeto, enquanto o ECS a transfere para o nível do cliente ("sistemas"), e isso pode ser ideal quando existem muito poucos sistemas, mas um monte de "objetos" ("entidades") em conformidade.

insira a descrição da imagem aqui

Uma classe também possui seus dados em particular e, portanto, pode manter os invariantes por conta própria. No entanto, existem invariantes "grosseiros" que ainda podem ser difíceis de manter quando objetos interagem entre si. Para um sistema complexo como um todo estar em um estado válido, muitas vezes é necessário considerar um gráfico complexo de objetos, mesmo que seus invariantes individuais sejam mantidos adequadamente. As abordagens tradicionais no estilo OOP podem ajudar na manutenção de invariantes granulares, mas na verdade podem dificultar a manutenção de invariantes amplos e grosseiros se os objetos se concentrarem em pequenas facetas do sistema.

É aí que esses tipos de abordagens ou variantes do ECS de construção de blocos de lego podem ser tão úteis. Também com sistemas sendo mais grosseiros em design do que o objeto comum, fica mais fácil manter esses tipos de invariantes grosseiros na visão panorâmica do sistema. Muitas interações com objetos pequenos se transformam em um grande sistema focado em uma tarefa ampla, em vez de pequenos objetos focados em pequenas tarefas com um gráfico de dependência que cobre um quilômetro de papel.

No entanto, eu tive que olhar para fora do meu campo, na indústria de jogos, para aprender sobre ECS, embora eu sempre tenha feito parte de uma mentalidade orientada a dados. Além disso, curiosamente, eu quase fui para o ECS por conta própria, apenas percorrendo e tentando criar projetos melhores. Eu não fiz tudo e perdi um detalhe muito crucial, que é a formalização da parte de "sistemas" e esmagar os componentes até os dados brutos.

Vou tentar mostrar como acabei optando pelo ECS e como ele resolveu todos os problemas com as iterações de design anteriores. Eu acho que isso ajudará a destacar exatamente por que a resposta aqui pode ser um forte "sim", que o ECS é potencialmente aplicável muito além da indústria de jogos.

Arquitetura da força bruta dos anos 80

A primeira arquitetura em que trabalhei na indústria de efeitos visuais tinha um longo legado que já passava de uma década desde que entrei na empresa. Era um código bruto de força bruta que codifica todo o caminho (não uma inclinação sobre C, como eu amo C, mas o modo como ele estava sendo usado aqui era realmente bruto). Uma fatia em miniatura e simplista se assemelhava a dependências como esta:

insira a descrição da imagem aqui

E este é um diagrama enormemente simplificado de um pequeno pedaço do sistema. Cada um desses clientes no diagrama ("Rendering", "Physics", "Motion") obteria algum objeto "genérico" através do qual verificaria um campo de tipo, assim:

void transform(struct Object* obj, const float mat[16])
{
    switch (obj->type)
    {
        case camera:
            // cast to camera and do something with camera fields
            break;
        case light:
            // cast to light and do something with light fields
            break;
        ...
    }
}

É claro que com código significativamente mais feio e complexo do que isso. Freqüentemente, funções adicionais seriam chamadas a partir desses casos de comutadores, o que faria recursivamente o comutador novamente e novamente. Este diagrama e código pode quase se parecem com ECS-lite, mas não houve forte distinção entidade de componente ( " é este objeto uma câmera?", Não 'se esse objeto fornecer movimento?'), E sem formalização de 'sistema' ( apenas um monte de funções aninhadas espalhadas por todo o lugar e misturando responsabilidades). Nesse caso, quase tudo era complicado, qualquer função era um potencial para um desastre esperando para acontecer.

Nosso procedimento de teste aqui geralmente tinha que verificar itens como malhas separadas de outros tipos de itens, mesmo que algo idêntico estivesse acontecendo com ambos, pois a natureza da força bruta da codificação aqui (geralmente acompanhada de muita copiar e colar) é muito provável que o que é exatamente a mesma lógica possa falhar de um tipo de item para o outro. Tentar estender o sistema para lidar com novos tipos de itens era bastante inútil, embora houvesse uma necessidade fortemente expressa do usuário final, pois era muito difícil quando estávamos lutando tanto para lidar com os tipos de itens existentes.

Alguns profissionais:

  • Uhh ... não leva nenhuma experiência em engenharia, eu acho? Esse sistema não requer nenhum conhecimento de conceitos básicos, como o polimorfismo, é uma força totalmente bruta, então acho que até um iniciante pode entender parte do código, mesmo que um profissional em depuração mal consiga mantê-lo.

Alguns contras:

  • Pesadelo de manutenção. Nossa equipe de marketing realmente sentiu a necessidade de se gabar de corrigirmos mais de 2000 bugs únicos em um ciclo de 3 anos. Para mim, é algo para se envergonhar com o fato de termos tantos bugs em primeiro lugar, e esse processo provavelmente ainda corrigia apenas 10% do total de bugs que cresciam em número o tempo todo.
  • Sobre a solução mais inflexível possível.

Arquitetura COM dos anos 90

A maior parte da indústria de efeitos visuais usa esse estilo de arquitetura do que reuni, lendo documentos sobre suas decisões de design e olhando para seus kits de desenvolvimento de software.

Pode não ser exatamente COM no nível ABI (algumas dessas arquiteturas só podem ter plug-ins escritos usando o mesmo compilador), mas compartilha muitas características semelhantes com as consultas de interface feitas nos objetos para ver quais interfaces seus componentes suportam.

insira a descrição da imagem aqui

Com esse tipo de abordagem, a transformfunção analógica acima passou a se parecer com esta forma:

void transform(Object obj, const Matrix& mat)
{
    // Wrapper that performs an interface query to see if the 
    // object implements the IMotion interface.
    MotionRef motion(obj);

    // If the object supported the IMotion interface:
    if (motion.valid())
    {
        // Transform the item through the IMotion interface.
        motion->transform(mat);
        ...
    }
}

Essa é a abordagem que a nova equipe da antiga base de código adotou, para eventualmente refatorar. E foi uma melhoria dramática em relação ao original em termos de flexibilidade e manutenção, mas ainda há alguns problemas que abordarei na próxima seção.

Alguns profissionais:

  • Muito mais flexível / extensível / sustentável do que a solução anterior de força bruta.
  • Promove uma forte conformidade com muitos princípios do SOLID, tornando todas as interfaces completamente abstratas (sem estado, sem implementação, apenas interfaces puras).

Alguns contras:

  • Muitos boilerplate. Nossos componentes tiveram que ser publicados por meio de um registro para instanciar objetos, as interfaces suportadas exigiam a herança ("implementação" em Java) da interface e o fornecimento de algum código para indicar quais interfaces estavam disponíveis em uma consulta.
  • Lógica duplicada promovida em todo o lugar como resultado das interfaces puras. Por exemplo, todos os componentes implementados IMotionsempre teriam exatamente o mesmo estado e a mesma implementação para todas as funções. Para atenuar isso, começamos a centralizar as classes base e a funcionalidade auxiliar em todo o sistema para as coisas que tendem a ser implementadas de forma redundante da mesma maneira para a mesma interface e, possivelmente, com várias heranças acontecendo por trás do sistema, mas foi bastante confuso, mesmo que o código do cliente tenha sido fácil.
  • Ineficiência: as sessões vtune geralmente mostravam a QueryInterfacefunção básica quase sempre aparecendo como um ponto de acesso médio a superior e, ocasionalmente, até o ponto de acesso número 1. Para atenuar isso, faríamos coisas como renderizar partes do cache da base de código uma lista de objetos já conhecidos por oferecer suporteIRenderable, mas isso aumentou significativamente os custos de complexidade e manutenção. Da mesma forma, isso foi mais difícil de medir, mas notamos algumas lentidões definidas em comparação com a codificação no estilo C que estávamos fazendo antes, quando cada interface exigia um envio dinâmico. Coisas como previsões errôneas de ramificação e barreiras de otimização são difíceis de medir fora de um pequeno aspecto do código, mas os usuários geralmente percebiam a capacidade de resposta da interface do usuário e coisas assim pioravam comparando versões anteriores e mais recentes do software lado a lado. lado para áreas onde a complexidade algorítmica não mudou, apenas as constantes.
  • Ainda era difícil argumentar sobre a correção em um nível mais amplo do sistema. Embora fosse significativamente mais fácil do que a abordagem anterior, ainda era difícil entender as interações complexas entre os objetos em todo este sistema, especialmente com algumas das otimizações que começaram a se tornar necessárias contra ele.
  • Tivemos problemas para corrigir nossas interfaces. Embora possa haver apenas um local amplo no sistema que usa uma interface, os requisitos do usuário mudam em relação às versões, e teríamos que fazer alterações em cascata em todas as classes que implementam a interface para acomodar uma nova função adicionada ao a interface, por exemplo, a menos que houvesse alguma classe base abstrata que já estivesse centralizando a lógica sob o capô (algumas delas se manifestariam no meio dessas mudanças em cascata, na esperança de não repetir isso repetidamente).

insira a descrição da imagem aqui

Resposta Pragmática: Composição

Uma das coisas que estávamos percebendo antes (ou pelo menos eu estava) que estava causando problemas era que IMotionpoderia ser implementado por 100 classes diferentes, mas com exatamente a mesma implementação e estado associados. Além disso, seria usado apenas por alguns sistemas como renderização, movimento de quadro-chave e física.

Portanto, nesse caso, podemos ter um relacionamento 3 para 1 entre os sistemas que usam a interface para a interface e um relacionamento de 100 para 1 entre os subtipos que implementam a interface na interface.

A complexidade e a manutenção seriam drasticamente distorcidas para a implementação e manutenção de 100 subtipos, em vez de três sistemas clientes dos quais dependem IMotion. Isso mudou todas as nossas dificuldades de manutenção para a manutenção desses 100 subtipos, e não dos 3 locais usando a interface. Atualizar 3 lugares no código com poucos ou nenhum "acoplamento eferente indireto" (como em dependências, mas indiretamente por meio de uma interface, não uma dependência direta), não é grande coisa: atualizar 100 locais de subtipo com uma carga de "acoplamentos eferentes indiretos" , grande coisa *.

* Percebo que é estranho e errado estragar a definição de "acoplamentos eferentes" nesse sentido, do ponto de vista da implementação, mas não encontrei uma maneira melhor de descrever a complexidade de manutenção associada tanto à interface quanto às implementações correspondentes de cem subtipos. deve mudar.

Então eu tive que me esforçar muito, mas propus que tentássemos nos tornar um pouco mais pragmáticos e relaxar toda a idéia de "interface pura". Não fazia sentido criar algo IMotioncompletamente abstrato e sem estado, a menos que víssemos um benefício em ter uma rica variedade de implementações. No nosso caso, IMotionter uma grande variedade de implementações se transformaria em um pesadelo de manutenção, pois não queríamos variedade. Em vez disso, estávamos iterando para tentar fazer uma implementação de movimento único que fosse realmente boa contra a alteração dos requisitos do cliente, e muitas vezes estávamos trabalhando com a idéia pura da interface tentando forçar todos os implementadores IMotiona usar a mesma implementação e estado associados, para que não ' t duplicar metas.

As interfaces tornaram-se assim mais amplas Behaviorsassociadas a uma entidade. IMotionsimplesmente se tornaria um Motion"componente" (mudei a maneira como definimos "componente" do COM para outro mais próximo da definição usual, de uma peça que compõe uma entidade "completa").

Em vez disso:

class IMotion
{
public:
    virtual ~IMotion() {}
    virtual void transform(const Matrix& mat) = 0;
    ...
};

Nós evoluímos para algo mais ou menos assim:

class Motion
{
public:
    void transform(const Matrix& mat)
    {
        ...
    }
    ...

private:
    Matrix transformation;
    ...
};

Isso é uma violação flagrante do princípio da inversão de dependência para começar a mudar do abstrato de volta ao concreto, mas para mim esse nível de abstração só é útil se pudermos prever uma necessidade genuína em algum futuro, além de uma dúvida razoável e não exercitar cenários ridículos "e se" completamente desassociados da experiência do usuário (o que provavelmente exigiria uma alteração no design de qualquer maneira), para obter essa flexibilidade.

Então começamos a evoluir para esse design. QueryInterfacetornou-se mais parecido QueryBehavior. Além disso, começou a parecer inútil usar herança aqui. Usamos a composição. Objetos transformados em uma coleção de componentes cuja disponibilidade pode ser consultada e injetada em tempo de execução.

insira a descrição da imagem aqui

Alguns profissionais:

  • No nosso caso, era muito mais fácil manter isso do que o sistema COM anterior, de interface pura. Surpresas imprevistas, como uma mudança nos requisitos ou reclamações sobre o fluxo de trabalho, poderiam ser acomodadas mais facilmente com uma Motionimplementação muito central e óbvia , por exemplo, e não dispersas em cem subtipos.
  • Deu um nível totalmente novo de flexibilidade, do tipo que realmente precisamos. Em nosso sistema anterior, como a herança modela um relacionamento estático, apenas poderíamos definir efetivamente novas entidades em tempo de compilação em C ++. Não conseguimos fazê-lo a partir da linguagem de script, por exemplo, com a abordagem de composição, poderíamos reunir novas entidades em tempo real no tempo de execução, simplesmente anexando componentes a elas e adicionando-as a uma lista. Uma "entidade" se transformou em uma tela em branco sobre a qual poderíamos simplesmente juntar uma colagem de tudo o que precisávamos em tempo real, com sistemas relevantes reconhecendo e processando automaticamente essas entidades como resultado.

Alguns contras:

  • Ainda estávamos com dificuldades no departamento de eficiência e com manutenção nas áreas críticas de desempenho. Cada sistema ainda iria querer armazenar em cache componentes de entidades que forneciam esses comportamentos para evitar repetir todos eles repetidamente e verificar o que estava disponível. Cada sistema que exigia desempenho faria isso de maneira um pouco diferente e estava propenso a um conjunto diferente de erros ao não atualizar essa lista em cache e possivelmente uma estrutura de dados (se alguma forma de pesquisa estivesse envolvida, como seleção de frustum ou rastreamento de raios) em alguns evento obscuro de mudança de cena, por exemplo
  • Ainda havia algo estranho e complexo no qual eu não conseguia identificar o que estava relacionado a todos esses pequenos objetos granulares e simples de comportamento. Ainda geramos muitos eventos para lidar com interações entre esses objetos "comportamentais" que às vezes eram necessários, e o resultado foi um código muito descentralizado. Cada pequeno objeto era fácil de testar a correção e, tomados individualmente, geralmente eram perfeitamente corretos. Ainda assim, parecia que estávamos tentando manter um ecossistema maciço composto de pequenas aldeias e tentando raciocinar sobre o que todos eles individualmente fazem e se somam para criar como um todo. A base de código do estilo C dos anos 80 parecia uma megalópole épica e superpovoada, que era definitivamente um pesadelo de manutenção,
  • Perda de flexibilidade com a falta de abstração, mas em uma área onde nunca encontramos uma necessidade genuína dela, dificilmente um engodo prático (embora definitivamente pelo menos teórico).
  • Preservar a compatibilidade ABI sempre foi difícil, e isso dificultou a exigência de dados estáveis ​​e não apenas de uma interface estável associada a um "comportamento". No entanto, poderíamos facilmente adicionar novos comportamentos e simplesmente depreciar os existentes, se fosse necessária uma mudança de estado, e isso era sem dúvida mais fácil do que fazer retrocessos sob as interfaces no nível de subtipo para lidar com problemas de versão.

Um fenômeno que ocorreu foi que, como perdemos a abstração desses componentes comportamentais, tínhamos mais deles. Por exemplo, em vez de um IRenderablecomponente abstrato , anexaríamos um objeto a um concreto Meshou PointSpritescomponente. O sistema de renderização saberia como renderizar Meshe PointSpritescomponentes e encontraria entidades que fornecem esses componentes e os desenham. Em outros momentos, tínhamos renderizações diversas como as SceneLabelque descobrimos que precisávamos em retrospectiva e, portanto, anexávamos um SceneLabelnesses casos a entidades relevantes (possivelmente além de um Mesh). O implemento do sistema de renderização seria atualizado para saber como renderizar as entidades que as forneceram, e essa foi uma mudança muito fácil de fazer.

Nesse caso, uma entidade composta de componentes também pode ser usada como componente para outra entidade. Construiríamos as coisas dessa maneira conectando blocos de lego.

ECS: Sistemas e componentes de dados brutos

Esse último sistema foi o que eu fiz sozinho, e ainda estávamos bastardizando com o COM. Parecia que estava querendo se tornar um sistema de componente de entidade, mas eu não estava familiarizado com isso na época. Eu estava olhando exemplos do estilo COM que saturavam meu campo, quando eu deveria estar olhando para os mecanismos de jogos AAA para obter inspiração arquitetônica. Eu finalmente comecei a fazer isso.

O que estava faltando eram várias idéias-chave:

  1. A formalização de "sistemas" para processar "componentes".
  2. "Componentes" são dados brutos em vez de objetos comportamentais compostos juntos em um objeto maior.
  3. Entidades como nada além de um ID estrito associado a uma coleção de componentes.

Finalmente deixei a empresa e comecei a trabalhar em um ECS como um indy (ainda trabalhando nele enquanto drena minhas economias), e esse tem sido o sistema mais fácil de gerenciar de longe.

O que notei com a abordagem ECS foi que ela resolveu os problemas com os quais ainda estava lutando acima. Mais importante para mim, parecia que estávamos gerenciando "cidades" de tamanho saudável, em vez de pequenas aldeias com interações complexas. Não era tão difícil de manter como uma "megalópole" monolítica, grande demais em sua população para administrar efetivamente, mas não era tão caótica quanto um mundo cheio de pequenas aldeias interagindo umas com as outras, apenas pensando nas rotas comerciais em entre eles formou um gráfico de pesadelo. O ECS destilou toda a complexidade em direção a "sistemas" volumosos, como um sistema de renderização, uma "cidade" de tamanho saudável, mas não uma "megalópole superpovoada".

Os componentes que se tornaram dados brutos pareceram realmente estranhos para mim no começo, pois quebram até o princípio básico de ocultação de informações do OOP. Foi meio que desafiar um dos maiores valores que eu prezava na OOP, que era sua capacidade de manter invariantes que exigiam encapsulamento e ocultação de informações. Mas isso começou a se tornar uma preocupação, pois rapidamente se tornou óbvio o que estava acontecendo com apenas uma dúzia de sistemas amplos transformando esses dados, em vez de essa lógica ser dispersa por centenas a milhares de subtipos, implementando uma combinação de interfaces. Costumo pensar nisso como no estilo OOP, exceto nos locais onde os sistemas estão fornecendo a funcionalidade e a implementação que acessam os dados, os componentes estão fornecendo os dados e as entidades estão fornecendo os componentes.

Tornou-se ainda mais fácil , contra-intuitivamente, raciocinar sobre os efeitos colaterais causados ​​pelo sistema quando havia apenas alguns sistemas volumosos transformando os dados em passes amplos. O sistema ficou muito "mais plano", minhas pilhas de chamadas ficaram mais rasas do que nunca para cada thread. Eu poderia pensar no sistema naquele nível superior e não encontrar surpresas estranhas.

Da mesma forma, simplificou até as áreas críticas de desempenho em relação à eliminação dessas consultas. Desde que a idéia de "Sistema" se tornou muito formalizada, um sistema podia assinar os componentes nos quais estava interessado e receber apenas uma lista em cache de entidades que atendem a esse critério. Cada indivíduo não precisava gerenciar essa otimização de cache, ela ficou centralizada em um único local.

Alguns profissionais:

  • Parece resolver apenas quase todos os principais problemas de arquitetura que encontrei em minha carreira, sem nunca me sentir preso em um canto do design ao encontrar necessidades imprevistas.

Alguns contras:

  • Eu ainda tenho dificuldade em entender isso algumas vezes, e esse não é o paradigma mais maduro ou bem estabelecido, mesmo na indústria de jogos, onde as pessoas discutem exatamente o que isso significa e como fazer as coisas. Definitivamente, não é algo que eu poderia ter feito com a equipe anterior com a qual trabalhei, que consistia em membros profundamente ligados à mentalidade no estilo COM ou à mentalidade no estilo C nos anos 80 da base de código original. Às vezes, quando me confundo, é como modelar relacionamentos no estilo gráfico entre componentes, mas sempre achei uma solução que não se tornou horrível mais tarde, onde posso apenas tornar um componente dependente de outro ("esse movimento componente depende desse outro como pai e o sistema usará a memorização para evitar repetidamente fazer os mesmos cálculos de movimento recursivo ", por exemplo)
  • A ABI ainda é difícil, mas até agora eu ousaria dizer que é mais fácil do que a abordagem de interface pura. É uma mudança de mentalidade: a estabilidade dos dados se torna o único foco da ABI, em vez da estabilidade da interface, e de certa forma é mais fácil obter estabilidade dos dados do que a estabilidade da interface (ex: não há tentações de alterar uma função apenas porque precisa de um novo parâmetro. Esse tipo de coisa acontece dentro de implementações grosseiras de sistema que não quebram a ABI).

insira a descrição da imagem aqui

No entanto, é razoável também criar aplicativos usando a arquitetura Component-Entity-System comum nos mecanismos de jogos?

Enfim, eu diria absolutamente "sim", com meu exemplo pessoal de efeitos visuais sendo um forte candidato. Mas isso ainda é bastante semelhante às necessidades dos jogos.

Eu não o pratiquei em áreas mais remotas, completamente afastadas das preocupações dos mecanismos de jogo (o VFX é bastante semelhante), mas me parece que muito mais áreas são boas candidatas a uma abordagem de ECS. Talvez até um sistema GUI seja adequado para um, mas ainda uso uma abordagem mais OOP (mas sem herança profunda, diferente do Qt, por exemplo).

É um território amplamente inexplorado, mas me parece adequado sempre que suas entidades podem ser compostas por uma rica combinação de "traços" (e exatamente que combinação de traços que eles fornecem estão sempre sujeitos a alterações) e onde você tem um punhado de informações generalizadas. sistemas que processam entidades que possuem as características necessárias.

Torna-se uma alternativa muito prática nesses casos a qualquer cenário em que você possa ser tentado a usar algo como herança múltipla ou uma emulação do conceito (mixins, por exemplo), apenas para produzir centenas ou mais combos em uma hierarquia profunda de herança ou centenas de combos de classes em uma hierarquia plana que implementa uma combinação específica de interfaces, mas onde seus sistemas são poucos em número (dezenas, por exemplo).

Nesses casos, a complexidade da base de código começa a parecer mais proporcional ao número de sistemas em vez do número de combinações de tipos, já que cada tipo agora é apenas uma entidade que compõe componentes que nada mais são do que dados brutos. Os sistemas GUI se ajustam naturalmente a esses tipos de especificações, onde eles podem ter centenas de possíveis tipos de widgets combinados de outros tipos ou interfaces de base, mas apenas alguns sistemas para processá-los (sistema de layout, sistema de renderização etc.). Se um sistema GUI usasse o ECS, provavelmente seria muito mais fácil argumentar sobre a correção do sistema quando toda a funcionalidade for fornecida por um punhado desses sistemas, em vez de centenas de tipos diferentes de objetos com interfaces herdadas ou classes base. Se um sistema GUI usasse o ECS, os widgets não teriam funcionalidade, apenas dados. Somente um punhado de sistemas que processam entidades de widget teria funcionalidade. A forma como os eventos substituíveis para um widget seriam tratados está além de mim, mas apenas com base em minha experiência limitada até agora, não encontrei um caso em que esse tipo de lógica não pudesse ser transferido centralmente para um determinado sistema de uma maneira que, em retrospectivamente, produziu uma solução muito mais elegante que eu jamais esperaria.

Eu adoraria vê-lo empregado em mais campos, pois era um salva-vidas no meu. É claro que não é adequado se o seu design não for quebrado dessa maneira, desde entidades que agregam componentes a sistemas grossos que processam esses componentes, mas se eles se encaixam naturalmente nesse tipo de modelo, é a coisa mais maravilhosa que já encontrei até agora. .


1) O que seu programa VFX de exemplo fez da perspectiva do usuário? 2) Em que projeto de ECS você está trabalhando agora? ♥ Obrigado por escrever isso! ♥
pup

11
Explicação muito completa, obrigado. Sinto que estou chegando a muitas das mesmas conclusões que você tem em relação à aplicabilidade do ECS além dos jogos; no meu caso, GUIs especificamente complexas. Definitivamente, parece realmente estranho, no início, ir contra o que geralmente é feito (hierarquias profundas de herança são especialmente proeminentes nas estruturas de interface do usuário), mas é animador ver outras pessoas que acham essa abordagem mais eficaz.
Danny Yaroslavski

11
Obrigado por este grande artigo! Para uma GUI baseada em componente, recomendo consultar a UGUI do Unity3d. É incrivelmente flexível e extensível em comparação com os baseados em herança como o CocoaTouch.
Ivan Mir

16

A arquitetura de sistema de entidade componente para mecanismos de jogos funciona para jogos devido à natureza do software de jogos e suas características exclusivas e requisitos de qualidade. Por exemplo, as entidades fornecem um meio uniforme de abordar e trabalhar com as coisas do jogo, que podem ser drasticamente diferentes em sua finalidade e uso, mas precisam ser renderizadas, atualizadas ou serializadas / desserializadas pelo sistema de maneira uniforme. Ao incorporar um modelo de componente nessa arquitetura, você permite que eles mantenham uma estrutura básica simples, enquanto adicionam mais recursos e funcionalidades conforme necessário, com baixo acoplamento de código. Existem vários sistemas de software diferentes que podem se beneficiar das características desse design, como aplicativos CAD, codecs A / V,

TL; DR - Os padrões de design funcionam apenas quando o domínio do problema é adequado aos recursos e desvantagens que eles impõem ao design.


8

Se o domínio do problema é adequado, certamente.

Meu trabalho atual envolve um aplicativo que precisa oferecer suporte a vários recursos, dependendo de vários fatores de tempo de execução. Usar entidades baseadas em componentes para dissociar todos esses recursos e permitir extensibilidade e testabilidade isoladamente foi idílico para nós.

editar: Meu trabalho envolve fornecer conectividade ao hardware proprietário (em C #). Dependendo do fator de forma do hardware, do firmware instalado, do nível de serviço que o cliente adquiriu, etc., etc., precisamos fornecer diferentes níveis de funcionalidade ao dispositivo. Mesmo alguns recursos que têm a mesma interface têm implementações diferentes, dependendo da versão do dispositivo.

As bases de código anteriores aqui tiveram interfaces muito amplas com muitas não implementadas. Alguns tiveram muitas interfaces finas que foram compostas estaticamente em uma classe bestial. Alguns simplesmente usaram dicionários string -> string para modelá-lo. (temos muitos departamentos que acham que podem fazer melhor)

Todos estes têm suas deficiências. Interfaces amplas são um problema e meio para simular / testar efetivamente. Adicionar novos recursos significa alterar a interface pública (e todas as implementações existentes). Muitas interfaces finas levaram a um código de consumo muito feio, mas desde que acabamos passando por um grande objeto de teste de teste ainda sofria. Além disso, as interfaces finas não gerenciavam bem suas dependências. Os dicionários de cordas têm os problemas usuais de análise e existência, além de problemas de desempenho, legibilidade e manutenção.

O que estamos usando agora é uma entidade muito pequena que tem seus componentes descobertos e compostos com base nas informações de tempo de execução. As dependências são feitas declarativamente e resolvidas automaticamente pela estrutura do componente principal. Os componentes em si podem ser testados isoladamente, pois trabalham diretamente com suas dependências, e os problemas com dependências ausentes são encontrados mais cedo - e em um local, em vez do primeiro uso da dependência. Novos componentes (ou teste) podem ser inseridos e nenhum código existente é afetado por ele. Os consumidores solicitam à entidade uma interface para o componente, portanto, estamos livres para brincar com as várias implementações (e como as implementações são mapeadas para dados de tempo de execução) com relativa liberdade.

Para uma situação como essa em que a composição do objeto e suas interfaces podem incluir algum subconjunto (altamente variado) de componentes comuns, ele funciona muito bem.


11
Supondo que você esteja autorizado, você pode fornecer mais detalhes sobre seu trabalho atual? Estou curioso para saber de que maneira o CES tem sido idílico para o que você está construindo.
Andrew De Andrade

Existe algum artigo, artigo ou blog sobre sua experiência? além disso, eu gostaria de ter mais detalhes técnicos sobre isso :) #
user1778770 /

@ user1778770 - não disponível ao público, não. Que tipo de perguntas você teve?
Telastyn

Bem, vamos começar com algo simples: seu conceito abrange toda a pilha de aplicativos (por exemplo, do negócio ao front-end)? ou apenas uma única camada de um único caso de uso?
user1778770

@ user1778770 - na minha implementação, existem entidades / componentes em uma camada. Diferentes entidades podem existir na camada diferente, mas muitas vezes não são 1: 1 (ou então as camadas oferecem nenhum benefício).
Telastyn
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.