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?
fonte
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 umdelegate
em 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.fonte