Conselhos sobre arquitetura de jogos / padrões de design

16

Estou trabalhando em um 2º RPG há algum tempo e percebi que tomei algumas decisões ruins de design. Existem algumas coisas em particular que estão me causando problemas, então eu queria saber que tipo de design outras pessoas usariam para superá-los ou que usariam.

Para um pouco de experiência, comecei a trabalhar no meu tempo livre no verão passado. Eu estava inicialmente criando o jogo em C #, mas há cerca de três meses, decidi mudar para C ++. Eu queria ter uma boa noção do C ++, já que faz algum tempo desde que o usei muito, e imaginei que um projeto interessante como esse seria um bom motivador. Eu tenho usado extensivamente a biblioteca de impulso e uso SFML para gráficos e FMOD para áudio.

Eu tenho um bom código escrito, mas estou pensando em descartá-lo e começar de novo.

Aqui estão as principais áreas de preocupação que tenho e queria obter algumas opiniões sobre a maneira correta como os outros as resolveram ou resolveriam.

1. Dependências cíclicas Quando eu estava jogando o jogo em C #, eu realmente não precisava me preocupar com isso, pois não é um problema lá. Mudando para C ++, isso se tornou um problema bastante importante e me fez pensar que eu poderia ter projetado as coisas incorretamente. Eu realmente não consigo imaginar como desacoplar minhas aulas e ainda fazê-las fazer o que eu quero. Aqui estão alguns exemplos de uma cadeia de dependência:

Eu tenho uma classe de efeito de status. A classe possui vários métodos (Aplicar / Não Aplicar, Tick, etc.) para aplicar seus efeitos contra um personagem. Por exemplo,

virtual void TickCharacter(Character::BaseCharacter* character, Battles::BattleField *field, int ticks = 1);

Essas funções seriam chamadas toda vez que o personagem infligido com o efeito status mudar. Seria usado para implementar efeitos como Regen, Poison, etc. No entanto, também introduz dependências na classe BaseCharacter e na classe BattleField. Naturalmente, a classe BaseCharacter precisa acompanhar quais efeitos de status estão ativos atualmente, de modo que é uma dependência cíclica. O campo de batalha precisa acompanhar as partes em conflito, e a classe das partes possui uma lista de caracteres base que introduzem outra dependência cíclica.

2 - Eventos

Em C #, fiz uso extensivo de delegados para conectar-se a eventos em personagens, campos de batalha etc. (por exemplo, havia um delegado para quando a saúde do personagem mudava, quando uma estatística mudava, quando um efeito de status era adicionado / removido, etc. .) e os componentes gráficos / do campo de batalha se conectariam a esses delegados para impor seus efeitos. Em C ++, fiz algo semelhante. Obviamente, não há equivalente direto aos delegados em C #, então, em vez disso, criei algo como isto:

typedef boost::function<void(BaseCharacter*, int oldvalue, int newvalue)> StatChangeFunction;

e na minha classe de personagem

std::map<std::string, StatChangeFunction> StatChangeEventHandlers;

sempre que as estatísticas do personagem mudavam, eu repetia e chamava cada StatChangeFunction no mapa. Enquanto isso funciona, estou preocupado que essa seja uma má abordagem para fazer as coisas.

3 - Gráficos

Esta é a grande coisa. Não está relacionado à biblioteca de gráficos que estou usando, mas é mais uma coisa conceitual. Em C #, juntei gráficos com muitas das minhas aulas, o que eu sei que é uma péssima idéia. Querendo fazê-lo desacoplado desta vez, tentei uma abordagem diferente.

Para implementar meus gráficos, eu estava imaginando tudo que estava relacionado ao jogo como uma série de telas. Ou seja, há uma tela de título, uma tela de status de personagem, uma tela de mapa, uma tela de inventário, uma tela de batalha, uma tela de GUI de batalha, e basicamente eu poderia empilhar essas telas umas sobre as outras conforme necessário para criar os gráficos do jogo. Qualquer que seja a tela ativa, possui a entrada do jogo.

Eu projetei um gerenciador de tela que iria enviar e exibir telas com base nas informações do usuário.

Por exemplo, se você estivesse em uma tela de mapa (um manipulador / visualizador de entrada para um Mapa de Ladrilhos) e pressionasse o botão Iniciar, faria uma chamada ao gerente de tela para empurrar uma tela do Menu Principal sobre a tela do mapa e marcar o mapa tela a não ser desenhada / atualizada. O player navegaria pelo menu, o que emitiria mais comandos para o gerenciador de tela, conforme apropriado, para colocar novas telas na pilha de telas e depois exibi-las à medida que o usuário altera as telas / cancela. Finalmente, quando o jogador sai do menu principal, eu o retiro e volto para a tela do mapa, observo que ele é desenhado / atualizado e sai daí.

As telas de batalha seriam mais complexas. Eu teria uma tela para atuar como plano de fundo, uma tela para visualizar cada parte da batalha e uma tela para visualizar a interface do usuário da batalha. A interface do usuário se conectaria aos eventos de caracteres e os utilizaria para determinar quando atualizar / redesenhar os componentes da interface do usuário. Finalmente, todo ataque que possua um script de animação disponível chamaria uma camada adicional para se animar antes de sair da pilha de telas. Nesse caso, cada camada é consistentemente marcada como desenhável e atualizável e recebo uma pilha de telas lidando com meus gráficos de batalha.

Embora ainda não tenha conseguido que o gerenciador de tela funcione perfeitamente, acho que posso fazê-lo há algum tempo. Minha pergunta é: essa é uma abordagem que vale a pena? Se é um design ruim, quero saber agora antes de investir muito mais tempo criando todas as telas de que vou precisar. Como você constrói os gráficos para o seu jogo?

user127817
fonte

Respostas:

15

No geral, eu não diria nada do que você listou deve fazer com que você desmantele o sistema e comece de novo. Isso é algo que todo programador deseja realizar entre 50 e 75% de qualquer projeto em que está trabalhando, mas leva a um ciclo interminável de desenvolvimento e a nunca terminar nada. Então, para esse fim, alguns comentários em cada seção.

  1. Isso pode ser um problema, mas geralmente é mais um aborrecimento do que qualquer outra coisa. Você está usando o #pragma once ou #ifndef MY_HEADER_FILE_H #define MY_HEADER_FILE_H ... #endif na parte superior ou ao redor dos arquivos .h, respectivamente? Dessa forma, o arquivo .h existe apenas uma vez em cada escopo? Se você estiver, minha recomendação será remover todas as instruções #include e compilar, adicionando as necessárias para compilar o jogo novamente.

  2. Sou fã desses tipos de sistemas e não vejo nada de errado com isso. O que é um Evento em C # geralmente é substituído por um Sistema de Eventos ou Sistema de Mensagens (você pode pesquisar as perguntas aqui para obter mais informações). A chave aqui é mantê-las no mínimo quando as coisas precisam acontecer, o que já parece que você está fazendo isso deve ser não para preocupações mínimas aqui.

  3. Isso também parece o caminho certo para mim e é o que faço para meus próprios motores, tanto pessoal quanto profissionalmente. Isso transforma o sistema de menus em um sistema de estado que possui o menu raiz (antes do jogo começar) ou o HUD do jogador como a tela 'raiz' exibida, dependendo de como você o configura.

Então, para resumir, não vejo nada reiniciar digno do que você está enfrentando. Você pode querer uma substituição mais formal do sistema de eventos mais adiante, mas isso chegará a tempo. A inclusão cíclica é um obstáculo que todos os programadores de C / C ++ precisam constantemente percorrer, e trabalhar para separar os gráficos parece um 'próximo passo' lógico.

Espero que isto ajude!

James
fonte
#ifdef não ajuda a incluir problemas circulares.
The Duck Comunista
Estava apenas cobrindo minha base com a expectativa de estar lá antes de rastrear as inclusões cíclicas. Pode ser outra chaleira de peixe quando você tem vários símbolos definidos em oposição a um arquivo que precisa incluir um arquivo que se inclui. (embora pelo que ele descreveu se as inclusões estão nos arquivos .CPP e não nos arquivos .H, ele deve estar bem com dois objetos de base que se conhecem) #
James
Obrigado pelo conselho :) Fico feliz em saber que estou no caminho certo
user127817
4

Suas dependências cíclicas não devem ser um problema, desde que você declare as classes onde pode nos arquivos de cabeçalho e #include-as nos arquivos .cpp (ou o que for).

Para o sistema de eventos, duas sugestões:

1) Se você deseja manter o padrão que está usando agora, considere mudar para um boost :: unordered_map em vez de std :: map. O mapeamento com cadeias de caracteres como chaves é lento, especialmente porque o .NET faz algumas coisas legais sob o capô para ajudar a acelerar as coisas. O uso de unordered_map hashes as seqüências de caracteres, para que as comparações sejam geralmente mais rápidas.

2) Considere mudar para algo mais poderoso, como boost :: signs. Se você fizer isso, poderá fazer coisas legais, como tornar seus objetos de jogo rastreáveis, derivando de boost :: signs :: trackable, e deixar que o destruidor cuide de limpar tudo, em vez de ter que cancelar manualmente o registro no sistema de eventos. Você também pode ter vários sinais que apontam para cada slot (ou vice-versa, não me lembro a nomenclatura exata) por isso é muito semelhante a fazer +=em um delegateem C #. O maior problema com os sinais boost :: é que ele precisa ser compilado, não são apenas cabeçalhos; portanto, dependendo da sua plataforma, pode ser difícil entrar em funcionamento.

Tetrad
fonte