Separando o Game Engine do código do jogo em jogos semelhantes, com controle de versão

15

Eu tenho um jogo terminado, que quero recusar em outras versões. Seriam jogos semelhantes, com mais ou menos o mesmo tipo de design, mas nem sempre, basicamente as coisas podem mudar, às vezes pequenas, às vezes grandes.

Gostaria que o código principal fosse versionado separadamente do jogo, para que, se digamos que eu corrija um bug encontrado no jogo A, a correção estará presente no jogo B.

Estou tentando encontrar a melhor maneira de gerenciar isso. Minhas idéias iniciais são:

  • Crie um enginemódulo / pasta / qualquer que seja, que contenha tudo o que possa ser generalizado e seja 100% independente do resto do jogo. Isso incluiria algum código, mas também ativos genéricos que são compartilhados entre os jogos.
  • Coloque esse mecanismo em seu próprio gitrepositório, que será incluído nos jogos como umgit submodule

A parte com a qual estou lutando é como gerenciar o restante do código. Digamos que você tenha sua cena de menu, esse código é específico do jogo, mas também a maioria tende a ser genérico e pode ser reutilizado em outros jogos. Eu não posso colocá-lo noengine , mas recodificá-lo para cada jogo seria ineficiente.

Talvez o uso de algum tipo de variação do Git branches possa ser eficaz para gerenciar isso, mas não acho que esse seja o melhor caminho a percorrer.

Alguém tem algumas idéias, experiência para compartilhar ou algo sobre isso?

Malharhak
fonte
Em que idioma está o seu mecanismo? Algumas linguagens têm gerenciadores de pacotes dedicados que podem fazer mais sentido do que usar sub-módulos git. Por exemplo, o NodeJS possui npm (que pode direcionar repositórios Git como fontes).
Dan Pantry
Sua pergunta é sobre qual a melhor forma de gerenciar o código genérico de configuração ou como gerenciar o código "semi-genérico" ou como arquitetar o código, como projetar o código ou o quê?
Dunk
Isso pode variar em cada ambiente da linguagem de programação, mas você pode considerar não apenas o software da versão de controle, mas também saber como dividir o mecanismo de jogo do código do jogo (como pacotes, pastas e API) e mais tarde , aplique a versão de controle.
umlcat
Como ter um histórico limpo de uma pasta em uma ramificação: Refatorar seu mecanismo para que repositórios separados (futuros) estejam em pastas separadas, esse é o seu último commit. Em seguida, crie uma nova ramificação, exclua tudo fora dessa pasta e confirme. Em seguida, vá para o primeiro commit do repositório e mescle isso com sua nova ramificação. Agora você tem uma ramificação apenas com essa pasta: puxe-a em outros projetos e / ou mescle-a novamente com o projeto existente. Isso me ajudou muito na separação de mecanismos em ramificações, se o seu código já estiver separado. Eu não preciso de módulos git.
Barry Staes

Respostas:

13

Crie um módulo / pasta de mecanismo / qualquer que seja, que contenha tudo o que possa ser generalizado e seja 100% independente do resto do jogo. Isso incluiria algum código, mas também ativos genéricos que são compartilhados entre os jogos.

Coloque esse mecanismo em seu próprio repositório git, que será incluído nos jogos como um submódulo git

É exatamente o que eu faço e funciona muito bem. Eu tenho uma estrutura de aplicativos e uma biblioteca de renderização, e cada uma delas é tratada como submódulo dos meus projetos. Acho que o SourceTree é útil quando se trata de submódulos, pois os gerencia bem e não permite que você esqueça nada; por exemplo, se você atualizou o submódulo do mecanismo no projeto A, ele notificará você para fazer as alterações no projeto B.

Com a experiência, vem o conhecimento de qual código deve estar no mecanismo versus o que deve ser por projeto. Sugiro que, se você estiver inseguro, mantenha-o em cada projeto por enquanto. Com o passar do tempo, você verá em seus vários projetos o que permanece o mesmo e, gradualmente, poderá levar isso em consideração no código do seu motor. Em outras palavras: duplique o código até que você tenha quase 100% de certeza de que não está sendo alterado discretamente por projeto e generalize-o.

Nota sobre controle de origem e binários

Lembre-se de que, se você espera que seus ativos binários mudem com frequência, convém colocá-los no controle de fonte baseado em texto, como o git. Apenas dizendo ... existem melhores soluções para binários. A coisa mais simples que você pode fazer por enquanto para ajudar a manter seu repositório "engine-source" limpo e com bom desempenho é ter um repositório separado "binários de mecanismo" que contém apenas binários, que você também inclui como submódulo em seu projeto. Dessa forma, você reduz o dano ao desempenho causado ao seu repositório "fonte do mecanismo", que está mudando o tempo todo e no qual você precisa de iterações rápidas: commit, push, pull etc. Sistemas de gerenciamento de controle de fonte como o git operam em deltas de texto , e assim que você introduz os binários, você introduz deltas maciços a partir de uma perspectiva de texto - o que acaba custando seu tempo de desenvolvimento.Anexo GitLab . O Google é seu amigo.

Engenheiro
fonte
Eles realmente não mudam com frequência, mas estou interessado nisso. Não sei nada sobre controle de versão binário. Que soluções existem?
Malharhak 21/09/2015
@Malharhak Editado para responder ao seu comentário.
Engineer
@Malharak Aqui estão algumas informações interessantes sobre esse tópico.
Engineer
11
+1 para manter as coisas em projeto pelo maior tempo possível. Código comum concede maior complexidade. Deve ser evitado até que seja absolutamente necessário.
Gusdor 21/09/2015
11
@ Malharhak Não, particularmente porque seu objetivo é apenas manter "cópias" até o momento em que você notar que o código é imutável e pode ser considerado comum. Gusdor reiterou isso - esteja avisado - é fácil perder muito tempo fatorando as coisas muito cedo, e depois tentando encontrar maneiras de manter esse código geral o suficiente para permanecer comum, mas adaptável o suficiente para atender a vários projetos ... você acaba com uma série de parâmetros e chaves e ele se transforma em uma confusão feia que ainda não é o que você precisa, porque você acaba mudando-per novo projeto de qualquer maneira . Não fator muito cedo . Tenha paciência.
Engenheiro de
6

Em algum momento, um mecanismo DEVE se especializar e saber coisas sobre o jogo. Vou sair pela tangente aqui.

Pegue recursos em um RTS. Um jogo pode ter Creditse Crystaloutro MetalePotatoes

Você deve usar os conceitos de OO adequadamente e usar o máx. reutilização de código. É claro que Resourceexiste um conceito aqui.

Portanto, decidimos que os recursos têm o seguinte:

  1. Um gancho no loop principal para incrementar / diminuir
  2. Uma maneira de obter o valor atual (retorna um int)
  3. Uma maneira de subtrair / adicionar arbitrariamente (jogadores transferindo recursos, compras ...)

Observe que essa noção de a Resourcepode representar mortes ou pontos em um jogo! Não é muito poderoso.

Agora vamos pensar em um jogo. Podemos ter moeda negociando moedas de um centavo e adicionando um ponto decimal à saída. O que não podemos fazer são recursos "instantâneos". Como dizer "geração de rede elétrica"

Digamos que você adicione um InstantResource classe com métodos semelhantes. Agora você está (começando a) poluir seu mecanismo com recursos.


O problema

Vamos pegar o exemplo do RTS novamente. Suponha que o jogador doe algo Crystalpara outro jogador. Você quer fazer algo como:

if(transfer.target == engine.getPlayerId()) {
    engine.hud.addIncoming("You got "+transfer.quantity+" of "+
        engine.resourceDictionary.getNameOf(transfer.resourceId)+
        " from "+engine.getPlayer(transfer.source).name);
}
engine.getPlayer(transfer.target).getResourceById(transfer.resourceId).add(transfer.quantity)
engine.getPlayer(transfer.source).getResourceById(transfer.resourceId).add(-transfer.quantity)

No entanto, isso é realmente muito confuso. É de uso geral, mas confuso. Já que impõe um resourceDictionaryque significa que agora seus recursos precisam ter nomes! E é por jogador, então você não pode mais ter recursos de equipe.

Esta é uma abstração "demais" (não é um exemplo brilhante, eu admito). Em vez disso, você deve chegar a um ponto em que aceita que seu jogo tem jogadores e cristais; então, você pode apenas ter (por exemplo)

engine.getPlayer(transfer.target).crystal().receiveDonation(transfer)
engine.getPlayer(transfer.source).crystal().sendDonation(transfer)

Com uma classe Playere uma classe em CurrentPlayerque CurrentPlayero crystalobjeto mostrará automaticamente o material no HUD para a transferência / envio de doações.

Isso polui o mecanismo com cristal, a doação de cristal, as mensagens no HUD para os jogadores atuais e tudo mais. É mais rápido e fácil ler / gravar / manter (o que é mais importante, pois não é significativamente mais rápido)


Considerações finais

O caso do recurso não é brilhante. Espero que você ainda possa ver o ponto. Se alguma coisa demonstrei que "os recursos não pertencem ao mecanismo", como o que um jogo específico precisa e o que é aplicável a todas as noções de recursos são MUITO diferentes. O que você normalmente encontrará são 3 (ou 4) "camadas"

  1. O "Núcleo" - esta é a definição de mecanismo do livro, é um gráfico de cena com ganchos de eventos, lida com shaders e pacotes de rede e uma noção abstrata de jogadores
  2. O "GameCore" - Isso é bastante genérico para o tipo de jogo, mas não para todos os jogos - por exemplo, recursos em RTS ou munição em FPSs. A lógica do jogo começa a se infiltrar aqui. É aqui que estaria nossa noção anterior de recursos. Adicionamos coisas que fazem sentido para a maioria dos recursos de RTS.
  3. "GameLogic" MUITO específico ao jogo que está sendo feito. Você encontrará variáveis ​​com nomes como creatureou shipou squad. Usando herança você vai ter aulas que abrangem todos os 3 camadas (por exemplo, Crystal é um Resource que é um GameLoopEventListener exemplo)
  4. "Ativos" são inúteis para qualquer outro jogo. Tomemos, por exemplo, os scripts de combinação de IA na meia-vida 2, eles não serão usados ​​em um RTS com o mesmo mecanismo.

Criando um novo jogo a partir de um mecanismo antigo

Isso é MUITO comum. A fase 1 é eliminar as camadas 3 e 4 (e 2 se o jogo for do tipo TOTALMENTE diferente) Suponha que estamos criando um RTS a partir de um RTS antigo. Ainda temos recursos, não apenas cristais e outras coisas - portanto, as classes base nas camadas 2 e 1 ainda fazem sentido, todo o cristal mencionado em 3 e 4 pode ser descartado. Então nós fazemos. No entanto, podemos verificá-lo como uma referência para o que queremos fazer.


Poluição na camada 1

Isso pode acontecer. Abstração e desempenho são inimigos. O UE4, por exemplo, fornece muitos casos otimizados de composição (portanto, se você deseja X e Y, alguém escreveu um código que faz X e Y muito rápido - sabe que está fazendo as duas coisas) e, como resultado, é MUITO grande. Isso não é ruim, mas é demorado. A Camada 1 decidirá coisas como "como você passa dados para shaders" e como você anima as coisas. Fazer da melhor maneira para o seu projeto é SEMPRE bom. Apenas tente planejar o futuro, reutilizar o código é seu amigo, herdar para onde faz sentido.


Classificando camadas

Ultimamente (prometo) não tenha muito medo de camadas. Motor é um termo arcaico dos velhos tempos de dutos de função fixa, nos quais os motores funcionavam da mesma maneira graficamente (e, como resultado, tinha muito em comum), o duto programável virou isso de cabeça para baixo e, assim, a "camada 1" ficou poluída com quaisquer efeitos que os desenvolvedores desejassem alcançar. A IA era a característica distintiva (por causa da infinidade de abordagens) dos motores, agora é AI e gráficos.

Seu código não deve ser arquivado nessas camadas. Até o famoso motor Unreal tem MUITAS versões diferentes, cada uma específica para um jogo diferente. Existem poucos arquivos (exceto estruturas de dados semelhantes, talvez) que não teriam sido alterados. Isto é bom! Se você quiser fazer um novo jogo com outro, levará mais de 30 minutos. A chave é planejar, saber quais bits copiar e colar e o que deixar para trás.

Alec Teal
fonte
1

Minha sugestão pessoal de como lidar com o conteúdo que é uma mistura de genérico e específico é torná-lo dinâmico. Tomarei sua tela de menu como exemplo. Se eu entendi mal o que você estava pedindo, deixe-me saber o que você queria saber e eu adaptarei minha resposta.

Há três coisas que estão (quase) sempre presentes em uma cena do menu: o plano de fundo, o logotipo do jogo e o próprio menu. Essas coisas geralmente são diferentes com base no jogo. O que você pode fazer com esse conteúdo é criar um MenuScreenGenerator em seu mecanismo, que usa três parâmetros de objeto: BackGround, Logo e Menu. A estrutura básica dessas três partes também faz parte do seu mecanismo, mas seu mecanismo não diz exatamente como essas partes são geradas, apenas quais parâmetros você deve fornecer.

Em seguida, no código do jogo, você cria objetos para um BackGround, um Logo e um Menu e passa para o MenuScreenGenerator. Novamente, seu próprio jogo não lida com a forma como o menu é gerado, isso é para o mecanismo. Seu jogo precisa apenas informar ao mecanismo como deve ser e onde deve estar.

Essencialmente, seu mecanismo deve ser uma API que o jogo diz o que exibir. Se feito corretamente, seu mecanismo deve fazer o trabalho duro e seu próprio jogo deve apenas informar ao mecanismo quais recursos usar, quais ações executar e como o mundo se parece.

Nzall
fonte