Eu tinha uma pergunta sobre arquitetura de jogos: Qual é a melhor maneira de fazer com que diferentes componentes se comuniquem?
Peço desculpas se essa pergunta já foi feita um milhão de vezes, mas não consigo encontrar nada com exatamente o tipo de informação que estou procurando.
Eu tenho tentado construir um jogo a partir do zero (C ++, se for o caso) e observei alguns softwares de jogos de código aberto como inspiração (Super Maryo Chronicles, OpenTTD e outros). Percebo que muitos desses designs de jogos usam instâncias globais e / ou singletons em todo o lugar (para coisas como filas de renderização, gerenciadores de entidades, gerenciadores de vídeo etc.). Estou tentando evitar instâncias globais e singletons e construindo um mecanismo o mais fracamente possível, mas estou encontrando alguns obstáculos que devem à minha inexperiência em um design eficaz. (Parte da motivação para este projeto é abordar isso :))
Eu criei um design no qual tenho um GameCore
objeto principal que possui membros análogos às instâncias globais que vejo em outros projetos (ou seja, ele possui um gerenciador de entrada, um gerenciador de vídeo, um GameStage
objeto que controla todas as entidades e o jogo para qualquer estágio atualmente carregado, etc). O problema é que, como tudo é centralizado no GameCore
objeto, não tenho uma maneira fácil de diferentes componentes se comunicarem.
Olhando para o Super Maryo Chronicles, por exemplo, sempre que um componente do jogo precisa se comunicar com outro componente (ou seja, um objeto inimigo quer se adicionar à fila de renderização a ser desenhada no estágio de renderização), ele apenas conversa com o instância global.
Para mim, preciso que meus objetos de jogo passem informações relevantes de volta ao GameCore
objeto, para que o GameCore
objeto possa passar essas informações para o (s) outro (s) componente (s) do sistema que precisa (por exemplo: para a situação acima, cada objeto inimigo passaria suas informações de renderização de volta para o GameStage
objeto, que coletaria tudo e passaria para GameCore
, o que por sua vez passaria para o gerenciador de vídeo para renderização). Parece um design realmente horrível, e eu estava tentando pensar em uma solução para isso. Meus pensamentos sobre possíveis projetos:
- Instâncias globais (design de Super Maryo Chronicles, OpenTTD, etc)
- Ter o
GameCore
objeto como intermediário através do qual todos os objetos se comunicam (design atual descrito acima) - Forneça ponteiros de componentes para todos os outros componentes com os quais eles precisarão conversar (por exemplo, no exemplo Maryo acima, a classe inimiga teria um ponteiro para o objeto de vídeo com o qual precisa conversar)
- Divida o jogo em subsistemas - Por exemplo, tenha objetos gerenciadores no
GameCore
objeto que manipulem a comunicação entre objetos em seu subsistema - (Outras opções? ....)
Imagino que a opção 4 acima seja a melhor solução, mas estou tendo alguns problemas para projetá-la ... talvez porque tenha pensado em termos dos projetos que vi que usam globais. Parece que estou pegando o mesmo problema existente no meu design atual e replicando-o em cada subsistema, apenas em uma escala menor. Por exemplo, o GameStage
objeto descrito acima é uma tentativa de fazer isso, mas o GameCore
objeto ainda está envolvido no processo.
Alguém pode oferecer algum conselho de design aqui?
Obrigado!
fonte
Respostas:
Algo que usamos em nossos jogos para organizar nossos dados globais é o padrão de design do ServiceLocator . A vantagem desse padrão comparado ao padrão Singleton é que a implementação de seus dados globais pode mudar durante o tempo de execução do aplicativo. Além disso, seus objetos globais também podem ser alterados durante o tempo de execução. Outra vantagem é que é mais fácil gerenciar a ordem de inicialização de seus objetos globais, o que é muito importante, especialmente em C ++.
por exemplo (código C # que pode ser facilmente traduzido para C ++ ou Java)
Digamos que você tenha uma interface de back-end de renderização que possui algumas operações comuns para renderizar coisas.
E que você tenha a implementação de back-end de renderização padrão
Em alguns projetos, parece legítimo poder acessar o back-end de renderização globalmente. No padrão Singleton, isso significa que cada implementação do IRenderBackend deve ser implementada como instância global exclusiva. Mas o uso do padrão ServiceLocator não exige isso.
Aqui está como:
Para poder acessar seu objeto global, você precisa inicializá-lo primeiro.
Apenas para demonstrar como as implementações podem variar durante o tempo de execução, digamos que seu jogo tenha um minijogo em que a renderização é isométrica e você implemente um IsometricRenderBackend .
Quando você faz a transição do estado atual para o estado do minijogo, basta alterar o backend de renderização global fornecido pelo localizador de serviço.
Outra vantagem é que você também pode usar serviços nulos. Por exemplo, se tivéssemos um serviço ISoundManager e o usuário quisesse desligar o som, poderíamos simplesmente implementar um NullSoundManager que não faz nada quando seus métodos são chamados; portanto, definindo o objeto de serviço do ServiceLocator como um objeto NullSoundManager, podemos obter este resultado com quase nenhuma quantidade de trabalho.
Para resumir, às vezes pode ser impossível eliminar dados globais, mas isso não significa que você não possa organizá-los adequadamente e de maneira orientada a objetos.
fonte
std::unique_ptr<ISomeService>
.Existem muitas maneiras de projetar um mecanismo de jogo e tudo se resume à preferência.
Para tirar o básico do caminho, alguns desenvolvedores preferem projetá-lo como uma pirâmide, onde há algumas classes principais, geralmente chamadas de kernel, core ou framework que cria, possui e inicializa uma série de subsistemas, como como áudio, gráficos, rede, física, IA e gerenciamento de tarefas, entidades e recursos. Geralmente, esses subsistemas são expostos a você por essa classe de estrutura e, geralmente, você passaria essa classe de estrutura para suas próprias classes como um argumento construtor, quando apropriado.
Acredito que você esteja no caminho certo, pensando na opção 4.
Lembre-se de que, quando se trata de comunicação, nem sempre isso implica em uma chamada de função direta. Existem várias maneiras indiretas de comunicação, seja através de algum método indireto usando
Signal and Slots
ou usandoMessages
.Às vezes, nos jogos, é importante permitir que as ações ocorram de forma assíncrona para manter o ciclo do jogo o mais rápido possível, para que as taxas de quadros sejam fluidas a olho nu. Os jogadores não gostam de cenas lentas e agitadas e, portanto, temos que encontrar maneiras de manter as coisas fluindo para eles, mas mantendo a lógica fluindo, mas sob controle e ordenada também. Embora as operações assíncronas tenham seu lugar, elas também não são a resposta para todas as operações.
Saiba que você terá um mix de comunicações síncronas e assíncronas. Escolha o que for apropriado, mas saiba que você precisará suportar os dois estilos entre seus subsistemas. A criação de suporte para ambos o servirá bem no futuro.
fonte
Você só precisa garantir que não haja dependências reversas ou cíclicas. Por exemplo, se você tem uma classe
Core
, e estaCore
possui umaLevel
e aLevel
lista deEntity
, a árvore de dependência deve se parecer com:Portanto, dada essa árvore de dependência inicial, você nunca deve
Entity
depender deLevel
ouCore
, eLevel
nunca deve depender deCore
. Se umLevel
ouEntity
precisar acessar dados mais altos na árvore de dependências, eles devem ser passados como parâmetro por referência.Considere o seguinte código (C ++):
Usando esta técnica, você pode ver que cada um
Entity
tem acesso àLevel
, eaLevel
tem acesso aoCore
. Observe que cadaEntity
um armazena uma referência ao mesmoLevel
, desperdiçando memória. Ao perceber isso, você deve questionar se cada umEntity
realmente precisa de acesso aoLevel
.Na minha experiência, existe A) Uma solução realmente óbvia para evitar dependências reversas, ou B) Não há maneira possível de evitar instâncias globais e singletons.
fonte
Então, basicamente, você deseja evitar um estado mutável global ? Você pode torná-lo local, imutável ou não um estado. Mais tarde é mais eficiente e flexível. É conhecido como ocultação de implementação.
fonte
A questão é, na verdade, sobre como reduzir o acoplamento sem sacrificar o desempenho. Todos os objetos globais (serviços) geralmente formam um tipo de contexto que é mutável durante o tempo de execução do jogo. Nesse sentido, o padrão do localizador de serviço dispersa diferentes partes do contexto em diferentes partes do aplicativo, que podem ou não ser o que você deseja. Outra abordagem do mundo real seria declarar uma estrutura como esta:
E passe-o como um ponteiro bruto não proprietário
sEnvironment*
. Aqui, os ponteiros apontam para interfaces para que o acoplamento seja reduzido de maneira semelhante em comparação com o localizador de serviço. No entanto, todos os serviços estão em um só lugar (o que pode ou não ser bom). Esta é apenas outra abordagem.fonte