Como se pode implementar módulos C ++ hot-swappable?

39

Tempos de iteração rápidos são essenciais para o desenvolvimento de jogos, muito mais do que gráficos e mecanismos sofisticados com muitos recursos em minha opinião. Não é de admirar que muitos pequenos desenvolvedores escolham linguagens de script.

A maneira do Unity 3D de pausar um jogo e modificar os ativos E o código, depois continuar e fazer com que as alterações entrem em vigor imediatamente é absolutamente boa para isso. Minha pergunta é: alguém implementou um sistema semelhante em mecanismos de jogos em C ++?

Ouvi dizer que alguns motores realmente super sofisticados funcionam, mas estou mais interessado em descobrir se existe uma maneira de fazê-lo em um motor ou jogo caseiro.

Obviamente, haveria trocas, e não consigo imaginar que você possa recompilar seu código enquanto o jogo estiver em pausa, recarregá-lo e funcionar em todas as circunstâncias ou plataformas.

Mas talvez seja possível para programação de IA e módulos lógicos de nível simples. Não é como se eu quisesse fazer isso em qualquer projeto a curto (ou longo prazo), mas fiquei curioso.

Evil Engel
fonte

Respostas:

26

Definitivamente, é possível, embora não com o seu código C ++ típico; você precisará criar uma biblioteca de estilo C que possa ser vinculada e recarregada dinamicamente em tempo de execução. Para tornar isso possível, a biblioteca deve conter todo o estado em um ponteiro opaco que pode ser fornecido à biblioteca após a recarga.

Timothy Farrar discute sua abordagem:

"Para o desenvolvimento, o código é compilado como uma biblioteca, um pequeno programa de carregador faz uma cópia da biblioteca e carrega a cópia da biblioteca para executar o programa. O programa aloca todos os dados na inicialização e usa um ponteiro para referenciar qualquer dado. Não uso nada global, exceto os dados somente leitura. Enquanto o programa está executando, a biblioteca original pode ser recompilada. Depois, com o pressionar de uma tecla, o programa retorna ao carregador e retorna esse ponteiro único de dados. O carregador faz uma cópia da nova biblioteca, carrega a cópia e passa o ponteiro dos dados para o novo código. Em seguida, o mecanismo continua de onde parou. " ( fonte original , agora disponível no arquivo da web )

Jason Kozak
fonte
Prefiro isso à resposta de Blair, como você realmente responde à pergunta.
23612 Jonathan Dickinson
15

A troca a quente é um problema muito, muito difícil de resolver com código binário. Mesmo se seu código for colocado em bibliotecas de vínculo dinâmico separadas, ele precisará garantir que não haja referências diretas a funções na memória (pois o endereço das funções pode mudar com uma recompilação). Isso significa essencialmente não usar funções virtuais, e qualquer coisa que use ponteiros de função faz isso através de algum tipo de tabela de despacho.

Coisas peludas e constrangedoras. É melhor usar uma linguagem de script para código que precisa de iteração rápida.

No trabalho, nossa base de código atual é uma mistura de C ++ e Lua. Lua também não é uma parte pequena do nosso projeto - é quase uma divisão de 50/50. Implementamos o recarregamento on-the-fly do Lua, para que você possa alterar uma linha de código, recarregar e continuar. De fato, você pode fazer isso para corrigir erros de falha que ocorrem no código Lua, sem reiniciar o jogo!

Blair Holloway
fonte
3
Outro ponto importante é que, mesmo que você não pretenda manter um sistema em script, se você deseja iterar muito cedo, ainda pode ser completamente razoável iniciar em Lua, depois vá para C ++ no caminho quando precisar desempenho extra e a taxa de iteração diminuiu. A desvantagem óbvia aqui é que você acaba portando o código, mas muitas vezes acertar a lógica é a parte mais difícil - o código quase se escreve quando você descobre essa parte.
Logan Kincaid
15

(Você pode querer saber sobre o termo "remendo de macaco" ou "soco de pato" por nada além da imagem mental bem-humorada.)

Além disso: se seu objetivo é diminuir o tempo de iteração para alterações no "comportamento", tente algumas abordagens que o levem ao longo do caminho e combine bem para permitir mais disso no futuro.

(Isso sairá um pouco tangente, mas eu prometo que retorne!)

  • Comece com dados e comece com pequenos: recarregue nos limites ("níveis" ou similares) e, em seguida, use a funcionalidade do SO para obter notificações de alteração de arquivo ou simplesmente pesquisar regularmente.
  • (Para obter pontos de bônus e tempos de carregamento mais baixos (novamente, diminuindo o tempo de iteração), consulte o processo de cozimento de dados .)
  • Scripts são dados e permitem iterar o comportamento. Se você usa uma linguagem de script, agora tem as notificações / capacidade de recarregar esses scripts, interpretados ou compilados. Você também pode conectar seu intérprete a um console do jogo, a um soquete de rede ou similar para aumentar a flexibilidade do tempo de execução.
  • O código também pode ser dados : seu compilador pode suportar sobreposições , bibliotecas compartilhadas, DLLs ou similares. Agora você pode escolher um momento "seguro" para descarregar e recarregar uma sobreposição ou DLL, manual ou automática. As outras respostas entram em detalhes aqui. Observe que algumas variantes disso podem interferir na verificação de assinatura criptográfica, no NX (sem execução) ou em mecanismos de segurança semelhantes.
  • Considere um sistema profundo de salvamento / carregamento com versão . Se você pode salvar e restaurar seu estado de maneira robusta, mesmo diante das alterações de código, pode encerrar o jogo e reiniciá-lo com uma nova lógica no mesmo ponto. É mais fácil falar do que fazer, mas é factível e é notavelmente mais fácil e mais portátil do que mexer na memória para alterar as instruções.
  • Dependendo da estrutura e do determinismo do seu jogo, você poderá gravar e reproduzir . Se essa gravação tiver apenas "comandos do jogo" (pense em um jogo de cartas, por exemplo), você poderá alterar todo o código de renderização desejado e reproduzir a gravação novamente para ver suas alterações. Para alguns jogos, isso é tão "fácil" quanto gravar alguns parâmetros iniciais (por exemplo, uma semente aleatória) e depois as ações do usuário. Para alguns, é muito mais complicado.
  • Faça esforços para reduzir o tempo de compilação . Em combinação com os sistemas de salvar / carregar ou gravar / reproduzir acima mencionados, ou mesmo com sobreposições ou DLLs, isso pode diminuir sua recuperação mais do que qualquer outra coisa.

Muitos desses pontos são benéficos, mesmo se você não conseguir recarregar dados ou códigos.

Anedotas de suporte:

Em um PC RTS grande (equipe de aproximadamente 120 pessoas, principalmente C ++), havia um sistema de economia de estado incrivelmente profundo, usado para pelo menos três propósitos:

  • Um salvamento "superficial" foi alimentado não ao disco, mas a um mecanismo de CRC para garantir que os jogos multiplayer permanecessem na simulação de etapa de bloqueio, um CRC a cada 10 a 30 quadros; isso garantiu que ninguém estava trapaceando e pegou bugs de dessincronização alguns quadros depois
  • Se e quando ocorreu um erro de dessincronização para vários jogadores, um salvamento extra-profundo era executado a cada quadro e alimentado novamente ao mecanismo CRC, mas desta vez o mecanismo CRC geraria muitos CRCs, cada um para lotes menores de bytes. Dessa maneira, poderia dizer exatamente qual parte do estado havia começado a divergir no último quadro. Percebemos uma desagradável diferença no "modo de ponto flutuante padrão" entre os processadores AMD e Intel usando isso.
  • Um salvamento em profundidade normal pode não salvar, por exemplo, o quadro exato de animação que sua unidade estava reproduzindo, mas obteria a posição, a saúde etc. de todas as suas unidades, permitindo salvar e retomar a qualquer momento durante o jogo.

Desde então, usei registro / reprodução determinística em um jogo de cartas em C ++ e Lua para o DS. Nós nos conectamos à API que projetamos para a IA (no lado C ++) e registramos todas as ações do usuário e da IA. Usamos essa funcionalidade no jogo (para fornecer uma repetição para o jogador), mas também para diagnosticar problemas: quando houve uma falha ou comportamento estranho, tudo o que precisávamos era pegar o arquivo salvo e reproduzi-lo em uma compilação de depuração.

Desde então, também usei sobreposições mais de algumas vezes, e o combinamos com o nosso sistema "cria automaticamente este diretório e carrega novo conteúdo no computador de mão". Tudo o que precisamos fazer é deixar a cena / nível / o que quer que seja e voltar e não apenas os novos dados (sprites, layout de nível etc.) carregariam, mas também qualquer novo código na sobreposição. Infelizmente, isso está ficando muito mais difícil com os computadores de mão mais recentes, devido à proteção contra cópias e mecanismos anti-hacking que tratam o código especialmente. Ainda o fazemos para scripts lua.

Por último, mas não menos importante: você pode (e eu tenho, em várias circunstâncias específicas muito pequenas) dar um soco no pato, remendando diretamente os códigos de instrução das instruções. Porém, isso funciona melhor se você estiver em uma plataforma e compilador fixos, e porque é quase impossível de manter, muito propenso a erros e limitado no que você pode realizar rapidamente, na maioria das vezes só o uso para redirecionar o código durante a depuração. Ele faz ensinar-lhe um inferno de um monte sobre o seu conjunto de instruções com pressa, no entanto.

leander
fonte
6

Você pode fazer algo como módulos de troca a quente em tempo de execução, implementando os módulos como bibliotecas de vínculo dinâmico (ou bibliotecas compartilhadas no UNIX) e usando dlopen () e dlsym () para carregar funções dinamicamente da biblioteca.

Para Windows, os equivalentes são LoadLibrary e GetProcAddress.

Este é um método C e tem algumas armadilhas ao usá-lo em C ++, você pode ler sobre isso aqui .

Matias Valdenegro
fonte
que guia é a chave para fazer bibliotecas hot-swappable, obrigado
JqueryToAddNumbers
4

Se você estiver usando o Visual Studio C ++, poderá de fato pausar e recompilar seu código em determinadas circunstâncias. O Visual Studio suporta Editar e Continuar . Anexe ao seu jogo no depurador, faça-o parar em um ponto de interrupção e modifique o código após o seu ponto de interrupção. Se você deseja salvar e continuar, o Visual Studio tentará recompilar o código, reinsira-o no executável em execução e continue. Se tudo der certo, a alteração feita será aplicada ao jogo em execução sem a necessidade de um ciclo inteiro de compilação, construção e teste. No entanto, o seguinte impedirá que isso funcione:

  1. Alterar as funções de retorno de chamada não funcionará corretamente. O E&C funciona adicionando um novo pedaço de código e alterando a tabela de chamadas para apontar para o novo bloco. Para retornos de chamada e qualquer coisa que use ponteiros de função, ele ainda estará executando as chamadas antigas e não modificadas. Para corrigir isso, você pode ter uma função de retorno de chamada de invólucro que chama uma função estática
  2. A modificação de arquivos de cabeçalho quase nunca funciona. Isso foi projetado para modificar chamadas de funções reais.
  3. Várias construções de linguagem farão com que ela falhe misteriosamente. Pela minha experiência pessoal, pré-declarar coisas como enums geralmente faz isso.

Eu usei o Edit and Continue para melhorar drasticamente a velocidade de coisas como a iteração da interface do usuário. Digamos que você tenha uma interface de usuário funcional construída inteiramente em código, exceto que você acidentalmente trocou a ordem de desenho de duas caixas para não ver nada. Ao alterar duas linhas de código ao vivo, você pode salvar um ciclo de compilação / construção / teste de 20 minutos para verificar uma correção trivial da interface do usuário.

Essa NÃO é uma solução completa para um ambiente de produção, e eu achei a melhor solução para mover o máximo de sua lógica possível para arquivos de dados e, em seguida, tornar esses arquivos recarregáveis.

Ben Zeigler
fonte
qual ciclo de compilação leva 20 minutos?!? eu prefiro atirar em mim mesmo
JqueryToAddNumbers
3

Como outros já disseram, é um problema difícil, vinculando dinamicamente C ++. Mas é um problema resolvido - você pode ter ouvido falar da COM ou de um dos nomes de marketing que foram aplicados ao longo dos anos: ActiveX.

COM tem um pouco de mau nome do ponto de vista do desenvolvedor, porque pode ser um grande esforço para implementar componentes C ++ que expõem sua funcionalidade usando-o (embora isso seja facilitado com ATL - a biblioteca de modelos ActiveX). Do ponto de vista do consumidor, ele tem um nome ruim porque os aplicativos que o usam, por exemplo, para incorporar uma planilha do Excel em um documento do Word ou um diagrama do Visio em uma planilha do Excel, tendem a travar um pouco no passado. E isso se resume aos mesmos problemas - mesmo com todas as orientações que a Microsoft oferece, o COM / ActiveX / OLE foi / é difícil de acertar.

Vou enfatizar que a tecnologia do COM não é, por si só, inerentemente ruim. Primeiro, o DirectX usa interfaces COM para expor sua funcionalidade e isso funciona bem o suficiente, assim como vários aplicativos que incorporam o Internet Explorer usando seu controle ActiveX. Em segundo lugar, é uma das maneiras mais simples de vincular dinamicamente o código C ++ - uma interface COM é essencialmente apenas uma classe virtual pura. Embora ele tenha um IDL como o CORBA, você não é obrigado a usá-lo, especialmente se as interfaces definidas são usadas apenas dentro do seu projeto.

Se você não está escrevendo para Windows, não pense que COM não vale a pena considerar. A Mozilla o reimplementou em sua base de código (usada no navegador Firefox) porque eles precisavam de uma maneira de componente seu código C ++.

U62
fonte
2

Há uma implementação de C ++ compilado em tempo de execução para o código de jogo aqui . Além disso, eu sei que há pelo menos um mecanismo de jogo proprietário que faz o mesmo. É complicado, mas factível.

Laurent Couvidou
fonte