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?