Em vez dos meus motores de jogo pesados de herança usual, estou brincando com uma abordagem mais baseada em componentes. No entanto, tenho dificuldade em justificar onde deixar os componentes fazerem o que precisam.
Digamos que eu tenha uma entidade simples que tenha uma lista de componentes. É claro que a entidade não sabe quais são esses componentes. Pode haver um componente presente que dê à entidade uma posição na tela, outro pode estar lá para desenhar a entidade na tela.
Para que esses componentes funcionem, eles precisam atualizar todos os quadros, a maneira mais fácil de fazer isso é percorrer a árvore da cena e, em seguida, para cada entidade, atualizar cada componente. Mas alguns componentes podem precisar de um pouco mais de gerenciamento. Por exemplo, um componente que torna uma entidade colidível deve ser gerenciado por algo que possa supervisionar todos os componentes colidíveis. Um componente que torna uma entidade desenhável precisa de alguém para supervisionar todos os outros componentes desenháveis para descobrir a ordem de desenho, etc.
Portanto, minha pergunta é: onde atualizo os componentes, qual é a maneira mais clara de levá-los aos gerentes?
Eu pensei em usar um objeto gerenciador de singleton para cada um dos tipos de componentes, mas que possui as desvantagens usuais de usar um singleton, uma maneira de aliviar isso é um pouco usando injeção de dependência, mas isso soa como um exagero para esse problema. Eu também poderia andar sobre a árvore da cena e, em seguida, reunir os diferentes componentes em listas usando algum tipo de padrão de observador, mas isso parece um pouco desperdício para todos os quadros.
fonte
Respostas:
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:
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):
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:
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.
fonte
Essa é a abordagem ingênua típica para atualizações de componentes (e não há nada necessariamente errado em ser ingênua, se funcionar para você). Um dos grandes problemas em que você realmente abordou - você está operando através da interface do componente (por exemplo
IComponent
) para não saber nada sobre o que acabou de atualizar. Você provavelmente também não sabe nada sobre a ordem dos componentes dentro da entidade; portanto,Um singleton não é realmente necessário aqui e, portanto, você deve evitá-lo, pois possui as desvantagens mencionadas. A injeção de dependência não é um exagero - o cerne do conceito é que você passa coisas que um objeto precisa para ele, idealmente no construtor. Você não precisa de uma estrutura DI pesada (como o Ninject ) para isso - basta passar um parâmetro extra para um construtor em algum lugar.
Um renderizador é um sistema fundamental e provavelmente suporta a criação e o gerenciamento da vida útil de vários objetos renderizáveis que correspondem a coisas visuais no seu jogo (provavelmente sprites ou modelos). Da mesma forma, um mecanismo de física provavelmente tem controle vitalício sobre coisas que representam as entidades que podem se mover na simulação de física (corpos rígidos). Cada um desses sistemas relevantes deve possuir, de alguma forma, esses objetos e ser responsável por atualizá-los.
Os componentes que você usa no sistema de composição de entidades do jogo devem ser apenas invólucros em torno das instâncias desses sistemas de nível inferior - o componente de posição pode envolver um corpo rígido, o componente visual envolve um sprite ou modelo renderizável etc.
Em seguida, o próprio sistema que possui os objetos de nível inferior é responsável por atualizá-los e pode fazê-lo em massa e de uma maneira que permita multithread dessa atualização, se apropriado. Seu loop principal do jogo controla a ordem bruta em que esses sistemas são atualizados (primeiro a física, depois o renderizador ou o que for). Se você tiver um subsistema que não tenha vida útil ou controle de atualização sobre as instâncias distribuídas, poderá criar um wrapper simples para lidar com a atualização de todos os componentes relevantes para esse sistema em massa e decidir onde colocá-lo. atualização relativa ao restante das atualizações do sistema (acho que isso acontece com os componentes "script").
Essa abordagem é ocasionalmente conhecida como abordagem de componente externo , se você estiver procurando mais detalhes.
fonte