O título é intencionalmente hiperbólico e pode ser apenas a minha inexperiência com o padrão, mas aqui está o meu raciocínio:
A maneira "usual" ou sem dúvida direta de implementar entidades é implementando-as como objetos e subclassificando o comportamento comum. Isso leva ao problema clássico de "é uma EvilTree
subclasse de Tree
ou Enemy
?". Se permitirmos herança múltipla, o problema do diamante surge. Em vez disso, poderíamos extrair a funcionalidade combinada Tree
e Enemy
aumentar a hierarquia que leva às classes de Deus, ou podemos intencionalmente deixar de fora o comportamento em nossas classes Tree
e Entity
(tornando-as interfaces no caso extremo), para que elas EvilTree
possam implementar isso - o que leva a duplicação de código, se alguma vez tivermos um SomewhatEvilTree
.
Os sistemas de componentes de entidade tentam resolver esse problema dividindo o objeto Tree
e Enemy
em componentes diferentes - digamos Position
, Health
e AI
- e implementam sistemas, como um AISystem
que altera a posição de uma entidade de acordo com as decisões da IA. Até aí tudo bem, mas e se EvilTree
puder pegar um powerup e causar dano? Primeiro, precisamos de a CollisionSystem
e a DamageSystem
(provavelmente já os temos). As CollisionSystem
necessidades de comunicação com o DamageSystem
: Toda vez que duas coisas colidem, a CollisionSystem
mensagem é enviada para DamageSystem
que ela possa subtrair a saúde. Os danos também são influenciados pelos upgrades, por isso precisamos armazená-los em algum lugar. Criamos um novo PowerupComponent
que anexamos às entidades? Mas então oDamageSystem
precisa saber sobre algo sobre o qual prefere não saber nada - afinal, também existem coisas que causam danos que não podem aumentar os poderes (por exemplo, a Spike
). Permitimos PowerupSystem
modificar um StatComponent
que também é usado para cálculos de danos semelhantes a esta resposta ? Mas agora dois sistemas acessam os mesmos dados. À medida que nosso jogo se torna mais complexo, ele se torna um gráfico de dependência intangível, onde os componentes são compartilhados entre muitos sistemas. Nesse ponto, podemos apenas usar variáveis estáticas globais e nos livrar de todo o padrão.
Existe uma maneira eficaz de resolver isso? Uma idéia que tive foi permitir que os componentes tivessem certas funções, por exemplo, dê o StatComponent
attack()
que apenas retorna um número inteiro por padrão, mas pode ser composto quando ocorre uma inicialização:
attack = getAttack compose powerupBy(20) compose powerdownBy(40)
Isso não resolve o problema que attack
deve ser salvo em um componente acessado por vários sistemas, mas pelo menos eu poderia digitar as funções corretamente se tiver um idioma que o suporte suficientemente:
// In StatComponent
type Strength = PrePowerup | PostPowerup
type Damage = Int
type PrePowerup = Int
type PostPowerup = Int
attack: Strength = getAttack //default value, can be changed by systems
getAttack: PrePowerup
// these functions can be defined in other components or in PowerupSystems
powerupBy: Strength -> PostPowerup
powerdownBy: Strength -> PostPowerup
subtractArmor: Strength -> Damage
// in DamageSystem
dealDamage: Damage -> () = attack compose subtractArmor compose hurtSomeEntity
Dessa forma, eu pelo menos garanto a ordem correta das várias funções adicionadas pelos sistemas. De qualquer maneira, parece que estou abordando rapidamente a programação reativa funcional aqui, então me pergunto se não deveria ter usado isso desde o início (apenas olhei para o FRP, então posso estar errado aqui). Vejo que o ECS é uma melhoria em relação às hierarquias complexas de classe, mas não estou convencido de que seja ideal.
Existe uma solução para isso? Estou faltando uma funcionalidade / padrão para desacoplar o ECS de maneira mais limpa? O FRP é estritamente mais adequado para esse problema? Esses problemas estão surgindo da complexidade inerente ao que estou tentando programar; ou seja, o FRP teria problemas semelhantes?
fonte
Respostas:
O ECS arruina completamente a ocultação de dados. Este é um trade-off do padrão.
O ECS é excelente na dissociação. Um bom ECS permite que um sistema de movimentação declare que funciona em qualquer entidade que possua um componente de velocidade e posição, sem ter que se preocupar com quais tipos de entidade existem ou com quais outros sistemas acessam esses componentes. Isso é pelo menos equivalente em dissociar o poder de fazer com que os objetos do jogo implementem determinadas interfaces.
Dois sistemas acessando os mesmos componentes é um recurso, não um problema. É totalmente esperado e não combina sistemas de forma alguma. É verdade que os sistemas terão um gráfico de dependência implícito, mas essas dependências são inerentes ao mundo modelado. Dizer que o sistema de danos não deve ter a dependência implícita do sistema de energização é alegar que as energias não afetam os danos e isso provavelmente está errado. No entanto, enquanto a dependência existe, os sistemas não são acoplados - você pode remover o sistema de inicialização do jogo sem afetar o sistema de danos, porque a comunicação ocorreu através do componente stat e estava completamente implícita.
A resolução dessas dependências e sistemas de pedidos pode ser feita em um único local central, semelhante ao funcionamento da resolução de dependências em um sistema DI. Sim, um jogo complexo terá um gráfico complexo de sistemas, mas essa complexidade é inerente e, pelo menos, está contida.
fonte
Quase não há como contornar o fato de que um sistema precisa acessar vários componentes. Para que algo como um VelocitySystem funcione, provavelmente seria necessário acessar um VelocityComponent e PositionComponent. Enquanto isso, o RenderingSystem também precisa acessar esses dados. Não importa o que você faça, em algum momento o sistema de renderização precisa saber para onde renderizar o objeto e o VelocitySystem precisa saber para onde mover o objeto.
O que você precisa para isso é a explicitação de dependências. Cada sistema precisa ser explícito sobre quais dados serão lidos e para quais dados serão gravados. Quando um sistema deseja buscar um componente em particular, ele deve poder fazer isso explicitamente apenas . Na sua forma mais simples, ele simplesmente possui os componentes para cada tipo necessário (por exemplo, o RenderSystem precisa dos RenderComponents e PositionComponents) como seus argumentos e retorna o que foi alterado (por exemplo, apenas os RenderComponents).
Você pode solicitar um design desse tipo. Nada está dizendo que, para o ECS, seus sistemas devem ser independentes da ordem ou de qualquer coisa desse tipo.
O uso desse design de sistema de componente de entidade e o FRP não são mutuamente exclusivos. De fato, os sistemas podem ser vistos como nada além de não terem estado, simplesmente executando transformações de dados (os componentes).
O FRP não resolveria o problema de precisar usar as informações necessárias para executar alguma operação.
fonte