Eu sugeriria começar lendo as três grandes mentiras de Mike Acton, porque você viola duas delas. Estou falando sério, isso mudará a maneira como você cria seu código: http://cellperformance.beyond3d.com/articles/2008/03/three-big-lies.html
Então, o que você viola?
Mentira # 3 - Código é mais importante que dados
Você fala sobre injeção de dependência, que pode ser útil em algumas (e apenas algumas) instâncias, mas deve sempre tocar um grande sino de alarme se você a usar, especialmente no desenvolvimento de jogos! Por quê? Porque é uma abstração frequentemente desnecessária. E abstrações nos lugares errados são horríveis. Então você tem um jogo. O jogo tem gerentes para diferentes componentes. Os componentes estão todos definidos. Portanto, faça uma aula em algum lugar do código do loop do jogo principal que "tenha" os gerentes. Gostar:
private CollissionManager _collissionManager;
private BulletManager _bulletManager;
Dê a ele algumas funções getter para obter cada classe de gerente (getBulletManager ()). Talvez essa classe em si seja um Singleton ou seja acessível a partir de um (você provavelmente já tem um Singleton central do Game em algum lugar). Não há nada de errado com dados e comportamento codificados bem definidos.
Não crie um ManagerManager que permita registrar gerentes usando uma chave, que pode ser recuperada usando essa chave por outras classes que desejam usar o gerenciador. É um ótimo sistema e muito flexível, mas onde se fala de um jogo aqui. Você sabe exatamente quais sistemas estão no jogo. Por que fingir que não? Porque este é um sistema para pessoas que pensam que o código é mais importante que os dados. Eles dirão "O código é flexível, os dados apenas o preenchem". Mas código é apenas dados. O sistema que descrevi é muito mais fácil, mais confiável, mais fácil de manter e muito mais flexível (por exemplo, se o comportamento de um gerente for diferente de outros, você precisará alterar algumas linhas em vez de refazer o sistema inteiro)
Mentira # 2 - O código deve ser projetado em torno de um modelo do mundo
Então você tem uma entidade no mundo do jogo. A entidade possui vários componentes que definem seu comportamento. Portanto, você cria uma classe Entity com uma lista de objetos Component e uma função Update () que chama a função Update () de cada componente. Direita?
Não. :) Isso é projetado em torno de um modelo do mundo: você tem bullet no seu jogo e adiciona uma classe Bullet. Então você atualiza cada marcador e passa para o próximo. Isso absolutamente prejudicará seu desempenho e fornecerá uma horrível base de código complicada, com código duplicado em todos os lugares e nenhuma estrutura lógica de código semelhante. (Confira minha resposta aqui para obter uma explicação mais detalhada sobre por que o design tradicional de OO é péssimo ou consulte Design orientado a dados)
Vamos dar uma olhada na situação sem nosso viés de OO. Queremos o seguinte, nem mais nem menos (observe que não há requisito para criar uma classe para entidade ou objeto):
- Você tem um monte de entidades
- As entidades são compostas por vários componentes que definem o comportamento da entidade
- Você deseja atualizar cada componente do jogo em cada quadro, de preferência de forma controlada
- Além de identificar os componentes como pertencentes, não há nada que a própria entidade precise fazer. É um link / ID para alguns componentes.
E vamos olhar para a situação. Seu sistema de componentes atualizará o comportamento de todos os objetos do jogo, todos os quadros. Este é definitivamente um sistema crítico do seu motor. O desempenho é importante aqui!
Se você conhece a arquitetura do computador ou o Data Oriented Design, sabe como obter o melhor desempenho: memória compactada e agrupando a execução do código. Se você executar trechos dos códigos A, B e C como este: ABCABCABC, não obterá o mesmo desempenho de quando é executado da seguinte maneira: AAABBBCCC. Isso não ocorre apenas porque o cache de instruções e dados será usado com mais eficiência, mas também porque se você executar todos os "A" s um após o outro, haverá muito espaço para otimização: remover código duplicado, pré-calcular dados usados por todos os "A", etc.
Portanto, se queremos atualizar todos os componentes, não vamos torná-los classes / objetos com uma função de atualização. Não vamos chamar essa função de atualização para cada componente em cada entidade. Essa é a solução "ABCABCABC". Vamos agrupar todas as atualizações de componentes idênticas. Em seguida, podemos atualizar todos os componentes A, seguidos por B, etc. O que precisamos fazer?
Primeiro, precisamos de gerenciadores de componentes. Para cada tipo de componente no jogo, precisamos de uma classe de gerente. Possui uma função de atualização que atualiza todos os componentes desse tipo. Ele possui uma função de criação que adicionará um novo componente desse tipo e uma função de remoção que destruirá o componente especificado. Pode haver outras funções auxiliares para obter e definir dados específicos para esse componente (por exemplo: defina o modelo 3D para o Componente do modelo). Observe que, de certa forma, o gerente é uma caixa preta para o mundo exterior. Não sabemos como os dados de cada componente são armazenados. Não sabemos como cada componente é atualizado. Não nos importamos, desde que os componentes se comportem como deveriam.
Em seguida, precisamos de uma entidade. Você pode fazer disso uma aula, mas isso não é necessário. Uma entidade pode ser nada mais que um ID inteiro exclusivo ou uma cadeia de caracteres com hash (também um número inteiro). Ao criar um componente para a Entidade, você passa o ID como argumento para o Gerente. Quando você deseja remover o componente, passa o ID novamente. Pode haver algumas vantagens em adicionar um pouco mais de dados à Entidade, em vez de apenas torná-la um ID, mas essas serão apenas funções auxiliares, porque, como listei nos requisitos, todo o comportamento da entidade é definido pelos próprios componentes. É o seu motor, então faça o que faz sentido para você.
O que precisamos é de um Entity Manager. Essa classe gerará IDs exclusivos se você usar a solução somente de ID ou poderá ser usada para criar / gerenciar objetos de Entidade. Também pode manter uma lista de todas as entidades do jogo, se você precisar. O Entity Manager pode ser a classe central do seu sistema de componentes, armazenando as referências a todos os ComponentManagers no seu jogo e chamando suas funções de atualização na ordem correta. Dessa forma, tudo o que o loop do jogo precisa fazer é chamar EntityManager.update () e todo o sistema é bem separado do restante do seu mecanismo.
Essa é a visão geral, vamos dar uma olhada em como os gerentes de componentes funcionam. Aqui está o que você precisa:
- Crie dados do componente quando create (entityID) for chamado
- Excluir dados do componente quando remover (entityID) for chamado
- Atualizar todos os dados do componente (aplicável) quando update () é chamado (ou seja, nem todos os componentes precisam atualizar cada quadro)
O último é onde você define o comportamento / lógica dos componentes e depende completamente do tipo de componente que está escrevendo. O AnimationComponent atualizará os dados da animação com base no quadro em que está. O DragableComponent atualizará apenas um componente que está sendo arrastado pelo mouse. O PhysicsComponent atualizará os dados no sistema de física. Ainda assim, como você atualiza todos os componentes do mesmo tipo de uma só vez, é possível fazer algumas otimizações que não são possíveis quando cada componente é um objeto separado com uma função de atualização que pode ser chamada a qualquer momento.
Observe que eu nunca pedi a criação de uma classe XxxComponent para armazenar dados de componentes. Isso é contigo. Você gosta de design orientado a dados? Em seguida, estruture os dados em matrizes separadas para cada variável. Você gosta de Design Orientado a Objetos? (Eu não recomendaria, ele ainda prejudicará seu desempenho em muitos lugares). Em seguida, crie um objeto XxxComponent que reterá os dados de cada componente.
A grande coisa sobre os gerentes é o encapsulamento. Agora, o encapsulamento é uma das filosofias mais terrivelmente mal utilizadas no mundo da programação. É assim que deve ser usado. Somente o gerente sabe quais dados do componente são armazenados onde, como a lógica de um componente funciona. Existem algumas funções para obter / definir dados, mas é isso. Você pode reescrever o gerente inteiro e suas classes subjacentes e, se não alterar a interface pública, ninguém percebe. Mudou o mecanismo de física? Apenas reescreva PhysicsComponentManager e pronto.
Depois, há uma coisa final: comunicação e compartilhamento de dados entre componentes. Agora isso é complicado e não existe uma solução única para todos. Você pode criar funções get / set nos gerenciadores para permitir, por exemplo, que o componente de colisão obtenha a posição do componente de posição (por exemplo, PositionManager.getPosition (entityID)). Você poderia usar um sistema de eventos. Você pode armazenar alguns dados compartilhados na entidade (a solução mais feia na minha opinião). Você pode usar (geralmente usado) um sistema de mensagens. Ou use uma combinação de vários sistemas! Não tenho tempo nem experiência para entrar em cada um desses sistemas, mas a pesquisa no google e no stackoverflow são seus amigos.