Essa é difícil. Vou tentar abordar algumas das perguntas com base em minhas experiências particulares (YMMV):
Os componentes devem acessar dados de outros componentes. Por exemplo, o método de desenho do componente de renderização deve acessar a posição do componente de transformação. Isso cria dependências no código.
Não subestime a quantidade e a complexidade (não o grau) dos acoplamentos / dependências aqui. Você pode observar a diferença entre isso (e este diagrama já é ridiculamente simplificado para níveis semelhantes a brinquedos, e o exemplo do mundo real teria interfaces entre elas para afrouxar o acoplamento):
... e isto:
... ou isto:
Os componentes podem ser polimórficos, o que introduz ainda mais complexidade. Por exemplo, pode haver um componente de renderização de sprite que substitua o método de desenho virtual do componente de renderização.
Então? O equivalente analógico (ou literal) de um vtable e despacho virtual pode ser chamado pelo sistema, e não pelo objeto que oculta seu estado / dados subjacentes. O polimorfismo ainda é muito prático e viável com a implementação "pura" do ECS quando o vtable analógico ou ponteiro (s) de função se transforma em "dados" das sortes para o sistema chamar.
Como o comportamento polimórfico (por exemplo, para renderização) precisa ser implementado em algum lugar, ele é terceirizado nos sistemas. (por exemplo, o sistema de renderização do sprite cria um nó de renderização do sprite que herda o nó de renderização e o adiciona ao mecanismo de renderização)
Então? Espero que isso não pareça sarcasmo (não é minha intenção, embora eu tenha sido acusado disso com frequência, mas gostaria de poder comunicar emoções melhor através do texto), mas a "terceirização" do comportamento polimórfico nesse caso não implica necessariamente um adicional custo à produtividade.
A comunicação entre sistemas pode ser difícil de evitar. Por exemplo, o sistema de colisão pode precisar da caixa delimitadora que é calculada a partir de qualquer componente de renderização de concreto existente.
Este exemplo parece particularmente estranho para mim. Não sei por que um renderizador retornaria dados à cena (geralmente considero os renderizadores somente leitura neste contexto) ou para um renderizador descobrir AABBs em vez de algum outro sistema para fazer isso tanto para renderizador quanto para renderizador. colisão / física (talvez eu esteja ficando com o nome do "componente de renderização" aqui). No entanto, não quero ficar muito preocupado com esse exemplo, pois percebo que esse não é o ponto que você está tentando fazer. Ainda assim, a comunicação entre sistemas (mesmo na forma indireta de leitura / gravação no banco de dados central do ECS com sistemas que dependem diretamente das transformações feitas por outros) não deve ser frequente, se for necessário. Este'
Isso pode gerar problemas se a ordem de chamada das funções de atualização do sistema não estiver definida.
Isso absolutamente deve ser definido. O ECS não é a solução completa para reorganizar a ordem de avaliação do processamento do sistema de todos os sistemas possíveis na base de código e recuperar exatamente o mesmo tipo de resultado para o usuário final que lida com quadros e FPS. Essa é uma das coisas, ao projetar um ECS, que eu, pelo menos, sugiro fortemente que seja antecipado de alguma forma antecipadamente (embora com muito espaço para respirar e perdoe para mudar de idéia mais tarde, desde que não esteja alterando os aspectos mais críticos da solicitação de invocação / avaliação do sistema).
No entanto, recalcular o mapa de mosaico inteiro a cada quadro é caro. Portanto, seria necessária uma lista para acompanhar todas as alterações feitas para atualizá-las no sistema. Da maneira OOP, isso pode ser encapsulado pelo componente do mapa de blocos. Por exemplo, o método SetTile () atualiza a matriz de vértices sempre que for chamada.
Eu não entendi direito, exceto que é uma preocupação orientada a dados. E não há armadilhas para representar e armazenar dados em um ECS, incluindo memorização, para evitar essas armadilhas de desempenho (as maiores com um ECS tendem a se relacionar a coisas como sistemas que consultam por instâncias disponíveis de tipos de componentes específicos, que é um dos aspectos mais desafiadores da otimização de um ECS generalizado). O fato de a lógica e os dados serem separados em um ECS "puro" não significa que você repentinamente precise recompilar coisas que, de outra forma, poderiam ser armazenadas em cache / memorizadas em uma representação OOP. Esse é um ponto discutível / irrelevante, a menos que eu tenha encoberto algo muito importante.
Com o ECS "puro", você ainda pode armazenar esses dados no componente de mapa de blocos. A única grande diferença é que a lógica para atualizar essa matriz de vértices mudaria para um sistema em algum lugar.
Você pode até se apoiar no ECS para simplificar a invalidação e a remoção desse cache da entidade, se criar um componente separado como TileMapCache
. Nesse ponto, quando o cache é desejado, mas não está disponível em uma entidade com um TileMap
componente, você pode computá-lo e adicioná-lo. Quando é invalidado ou não é mais necessário, você pode removê-lo através do ECS sem precisar escrever mais código especificamente para essa invalidação e remoção.
As dependências entre os componentes ainda existem, embora ocultas nos sistemas
Não há dependência entre componentes em um representante "puro" (não acho certo dizer que as dependências estão sendo ocultadas aqui pelos sistemas). Dados não dependem de dados, por assim dizer. A lógica depende da lógica. E um ECS "puro" tende a promover a lógica a ser escrita de uma maneira que dependa do subconjunto mínimo absoluto de dados e lógica (geralmente nenhum) que um sistema requer para funcionar, o que é diferente de muitas alternativas que geralmente incentivam dependendo da muito mais funcionalidade do que o necessário para a tarefa real. Se você estiver usando o ECS puro, uma das primeiras coisas que você deve apreciar são os benefícios da dissociação, enquanto questiona simultaneamente tudo o que você aprendeu a apreciar no OOP sobre encapsulamento e ocultação de informações específicas.
Ao dissociar, quero dizer especificamente o quão pouca informação seus sistemas precisam para funcionar. Seu sistema de movimento nem precisa saber sobre algo muito mais complexo como um Particle
ou Character
(o desenvolvedor do sistema nem precisa necessariamente saber que essas idéias de entidade existem no sistema). Ele só precisa saber sobre os dados mínimos, como um componente de posição, que pode ser tão simples quanto alguns carros alegóricos em uma estrutura. São ainda menos informações e menos dependências externas do que uma interface pura IMotion
tende a levar consigo. É principalmente devido a esse conhecimento mínimo que cada sistema exige que funcione, o que torna o ECS muitas vezes tão perdoador para lidar com mudanças inesperadas de projeto em retrospectiva, sem enfrentar quebras de interface em cascata por todo o lugar.
A abordagem "impura" que você sugere diminui um pouco esse benefício, já que agora sua lógica não está localizada estritamente em sistemas onde as mudanças não causam quebras em cascata. A lógica agora seria centralizada em algum grau nos componentes acessados por vários sistemas que agora precisam atender aos requisitos de interface de todos os vários sistemas que poderiam usá-la, e agora é como se todo sistema precisasse ter conhecimento de (depender de) mais informações estritamente necessárias para trabalhar com esse componente.
Dependências de dados
Uma das coisas controversas sobre o ECS é que ele tende a substituir o que poderia ser dependências para abstrair interfaces com apenas dados brutos, e isso geralmente é considerado uma forma de acoplamento menos desejável e mais restrita. Mas nos tipos de domínios como jogos nos quais o ECS pode ser muito benéfico, geralmente é mais fácil projetar a representação de dados antecipadamente e mantê-la estável do que projetar o que você pode fazer com esses dados em algum nível central do sistema. Isso é algo que tenho observado dolorosamente, mesmo entre veteranos experientes em bases de código, que utiliza mais uma abordagem de interface pura no estilo COM com coisas como IMotion
.
Os desenvolvedores continuavam encontrando motivos para adicionar, remover ou alterar funções a essa interface central, e cada alteração era horrível e dispendiosa, pois tenderia a quebrar todas as classes implementadas IMotion
e todos os locais do sistema usado IMotion
. Enquanto isso, o tempo todo com tantas mudanças dolorosas e em cascata, os objetos implementados IMotion
estavam apenas armazenando uma matriz 4x4 de flutuadores e toda a interface se preocupava apenas em como transformar e acessar esses flutuadores; a representação dos dados era estável desde o início e muita dor poderia ter sido evitada se essa interface centralizada, tão propensa a mudanças com necessidades imprevistas de projeto, nem sequer existisse.
Tudo isso pode parecer quase tão nojento quanto variáveis globais, mas a natureza de como o ECS organiza esses dados em componentes recuperados explicitamente por tipo através de sistemas faz com que seja, enquanto os compiladores não podem impor algo como ocultar informações, os lugares que acessam e se modificam os dados são geralmente muito explícitos e óbvios o suficiente para manter efetivamente os invariantes e prever que tipo de transformações e efeitos colaterais ocorrem de um sistema para o outro (na verdade, de maneiras que podem ser discutivelmente mais simples e previsíveis do que o OOP em certos domínios, considerando como o sistema se transforma em uma espécie plana de oleoduto).
Por fim, quero fazer a pergunta de como lidaria com a animação em um ECS puro. Atualmente, defini uma animação como um functor que manipula uma entidade com base em algum progresso entre 0 e 1. O componente de animação possui uma lista de animadores que possui uma lista de animações. Em sua função de atualização, aplica as animações atualmente ativas à entidade.
Somos todos pragmáticos aqui. Mesmo em gamedev, você provavelmente terá idéias / respostas conflitantes. Até o mais puro ECS é um fenômeno relativamente novo, território pioneiro, para o qual as pessoas não formularam necessariamente as opiniões mais fortes sobre como esfolar gatos. Minha reação instintiva é um sistema de animação que incrementa esse tipo de progresso da animação em componentes animados para o sistema de renderização exibir, mas isso está ignorando tantas nuances para o aplicativo e o contexto específicos.
Com o ECS, não é uma bala de prata e ainda me encontro com tendências para adicionar novos sistemas, remover alguns, adicionar novos componentes, alterar um sistema existente para pegar esse novo tipo de componente etc. Não entendo coisas certas desde o primeiro momento. Mas a diferença no meu caso é que não estou mudando nada central quando falho em antecipar determinadas necessidades de design com antecedência. Não estou obtendo o efeito ondulante das quebras em cascata que exigem que eu percorra todo o lugar e mude muito código para lidar com alguma nova necessidade que surge, e isso economiza bastante tempo. Também estou achando mais fácil para o meu cérebro, porque quando me sento com um sistema específico, não preciso saber / lembrar muito mais sobre qualquer outra coisa além dos componentes relevantes (que são apenas dados) para trabalhar nele.