Como evitar a codificação embutida nos mecanismos de jogos

22

Minha pergunta não é uma questão de codificação; isso se aplica a todo o design de mecanismos de jogos em geral.

Como você evita a codificação codificada?

Esta questão é muito mais profunda do que parece. Digamos, se você deseja executar um jogo que carrega os arquivos necessários para a operação, como evitar dizer algo parecido load specificfile.wadno código do mecanismo? Além disso, quando o arquivo é carregado, como você evita dizer isso load aspecificmap in specificfile.wad?

Esta pergunta se aplica a praticamente todo o design do mecanismo e o mínimo possível de mecanismo deve ser codificado. Qual é a melhor maneira de conseguir isso?

Marcus Cramer
fonte

Respostas:

42

Codificação baseada em dados

Tudo o que você menciona é algo que pode ser especificado nos dados. Por que você está carregando aspecificmap? Como a configuração do jogo indica que é o primeiro nível quando um jogador inicia um novo jogo, ou porque esse é o nome do ponto de salvamento atual no arquivo de salvamento que ele acabou de carregar, etc.

Como você encontra aspecificmap? Porque está em um arquivo de dados que lista os IDs do mapa e seus recursos em disco.

É necessário apenas um conjunto particularmente pequeno de recursos "essenciais" que sejam legitimamente rígidos ou impossíveis de evitar a codificação. Com um pouco de trabalho, isso pode ser limitado a um único nome de ativo padrão codificado como main.wadou semelhante. Este arquivo pode ser potencialmente alterado em tempo de execução, passando um argumento de linha de comando para o jogo, também conhecido como game.exe -wad mymain.wad.

A escrita de código orientado a dados se baseia em alguns outros princípios. Por exemplo, é possível evitar que sistemas ou módulos solicitem um recurso específico e inverter essas dependências. Ou seja, não faça DebugDrawercarregamento debug.fontno seu código de inicialização; em vez disso, DebugDraweruse um identificador de recurso em seu código de inicialização. Esse identificador pode ser carregado a partir do arquivo de configuração principal do jogo.

Como exemplos concretos de nossa base de código, temos um objeto "dados globais" carregado do banco de dados de recursos (que por si só é a ./resourcespasta, mas pode ser sobrecarregado com um argumento de linha de comando). O ID do banco de dados de recurso desses dados globais é o único nome de recurso codificado necessário na base de código (temos outros porque às vezes os programadores ficam preguiçosos, mas geralmente acabamos corrigindo / removendo esses eventualmente). Esse objeto de dados global está cheio de componentes cujo único objetivo é fornecer dados de configuração. Um dos componentes é o componente Dados globais da interface do usuário, que contém identificadores de recursos para todos os principais recursos da interface do usuário (fontes, arquivos Flash, ícones, dados de localização etc.), entre vários outros itens de configuração. Quando um desenvolvedor de interface do usuário decide renomear o principal recurso da interface do usuário de /ui/mainmenu.swfpara/ui/lobby.swfeles apenas atualizam essa referência de dados globais; nenhum código do mecanismo precisa mudar.

Usamos esses dados globais para tudo. Todos os personagens jogáveis, todos os níveis, interface do usuário, áudio, ativos principais, configuração de rede, tudo. (bem, não tudo , mas essas outras coisas são bugs a serem corrigidos.)

Essa abordagem tem muitas outras vantagens. Por um lado, torna a compactação e agregação de recursos parte integrante de todo o processo. Os caminhos de codificação embutida no mecanismo também tendem a significar que esses mesmos caminhos devem ser codificados em quaisquer scripts ou ferramentas que agrupam os ativos do jogo, e esses caminhos podem ficar fora de sincronia. Contando com um único núcleo de ativos e cadeias de referência a partir daí, podemos criar um pacote de ativos com um único comando bundle.exe -root config.data -out main.wade saber que ele incluirá todos os ativos necessários. Além disso, como o empacotador seguiria apenas as referências de recursos, sabemos que incluirá apenas os ativos necessários e pularemos toda a penugem restante que inevitavelmente se acumula durante a vida de um projeto (além disso, podemos gerar automaticamente listas dessa cotão para poda).

Um caso difícil dessa coisa toda está nos scripts. Tornar o mecanismo controlado por dados é fácil conceitualmente, mas já vi muitos projetos (hobby da AAA) em que os scripts são considerados dados e, portanto, "é permitido" usar apenas caminhos de recursos indiscriminadamente. Não faça isso. Se um arquivo Lua precisar de um recurso e chamar apenas uma função textures.lua("/path/to/texture.png"), o pipeline de ativos terá muitos problemas ao saber que o script precisa /path/to/texture.pngoperar corretamente e pode considerar que a textura não é usada e desnecessária. Os scripts devem ser tratados como qualquer outro código: todos os dados necessários, incluindo recursos ou tabelas, devem ser especificados em uma entrada de configuração que o mecanismo e o pipeline de recursos possam inspecionar quanto a dependências. Os dados que dizem "carregar script foo.lua" devem dizer "foo.luae forneça esses parâmetros ", onde os parâmetros incluem todos os recursos necessários. Se um script gerar aleatoriamente inimigos, por exemplo, passe a lista de possíveis inimigos para o script a partir desse arquivo de configuração. O mecanismo poderá pré-carregar os inimigos com o nível ( pois ele conhece a lista completa de possíveis spawns) e o pipeline de recursos sabe agrupar todos os inimigos no jogo (já que eles são definitivamente referenciados pelos dados de configuração) .Se os scripts geram sequências de nomes de caminhos e apenas chama uma loadfunção, o mecanismo nem o pipeline de recursos têm como saber especificamente quais ativos o script pode tentar carregar.

Sean Middleditch
fonte
Boa resposta, muito prática e também explica as armadilhas e erros que as pessoas cometem ao implementar isso! +1
whn
+1. Acrescentaria que seguir o padrão de apontar para recursos que contêm dados de configuração também é muito útil se você deseja ativar a modificação. É muitíssimo mais difícil e arriscado modificar jogos que exigem que você altere os arquivos de dados originais em vez de criar os seus próprios e apontá-los. Ainda melhor se você puder apontar para vários arquivos com uma ordem de prioridade definida.
Jeutnarg
12

Da mesma maneira que você evita a codificação em funções gerais.

Você passa parâmetros e mantém suas informações em arquivos de configuração.

Nessa situação, não há absolutamente nenhuma diferença na engenharia de software entre escrever um mecanismo e escrever uma classe.

MgrAssets
public:
  errorCode loadAssetFromDisk( filePath )
  errorCode getMap( mapName, map& )

private:
  maps[name, map]

Em seguida, o código do cliente lê um arquivo de configuração "mestre" ( este é codificado ou passado como um argumento de linha de comando) que contém as informações que informam onde estão os arquivos de ativos e o mapa que eles contêm.

A partir daí, tudo é direcionado pelo arquivo de configuração "mestre".

Vaillancourt
fonte
1
Sim, isso mais algum tipo de mecanismo para trazer lógica personalizada. Funcionalidade pode ser incorporando uma linguagem como C #, Python etc., a fim de estender os recursos do núcleo do motor pelo usuário definido
qCring
3

Eu gosto das outras respostas, então vou ser um pouco contrário. ;)

Você não pode evitar codificar o conhecimento sobre seus dados no seu mecanismo. De onde quer que a informação venha, o mecanismo deve saber para procurá-la. No entanto, você pode evitar a codificação das informações reais em seu mecanismo.

Uma abordagem orientada a dados "puros" faria com que você iniciasse o executável com os parâmetros de linha de comando necessários para carregar a configuração inicial, mas o mecanismo precisará ser codificado para saber como interpretar essas informações. Por exemplo, se seus arquivos de configuração são JSON, você precisa codificar as variáveis que você procurar, por exemplo, o motor terá que saber procurar "intro_movies"e "level_list"e assim por diante.

No entanto, um mecanismo "bem construído" pode funcionar para muitos jogos diferentes apenas trocando os dados de configuração e os dados que ele faz referência.

Portanto, o mantra não é tanto para evitar a codificação rígida, mas também para garantir que você possa fazer alterações com o mínimo de esforço possível.

Para contrastar com a abordagem dos arquivos de dados (que eu apoio sinceramente), pode ser que você esteja bem compilando os dados em seu mecanismo. Se o "custo" de fazer isso for menor, não haverá nenhum dano real; se você é o único que trabalha nisso, pode adiar o tratamento de arquivos para uma data posterior e não necessariamente se ferrar. Meus primeiros projetos de jogos tinham grandes tabelas de dados codificadas no próprio jogo, por exemplo, uma lista de armas e seus dados variados:

struct Weapon
{
    enum IconID icon;
    enum ModelID model;
    int damage;
    int rateOfFire;
    // etc...
};

const struct Weapon g_weapons[] =
{
    { ICON_PISTOL, MODEL_PISTOL, 5, 6 },
    { ICON_RIFLE, MODEL_RIFLE, 10, 20 },
    // etc...
};

Então você coloca esses dados em algum lugar fácil de referenciar e é fácil editar, conforme necessário. O ideal seria colocar essas coisas em um arquivo de configuração de algum tipo, mas você precisará analisar e traduzir e todo esse jazz, além de conectar referências entre estruturas pode se tornar uma dor adicional que você realmente não deseja. Lide com.

traço-tom-bang
fonte
Não é muito difícil analisar o json. O único "custo" envolvido é o aprendizado. (Especificamente, aprendendo a usar o módulo ou biblioteca adequada Go tem um bom suporte JSON, por exemplo..)
Wildcard
Não é terrivelmente difícil, mas exige fazê-lo além do aprendizado. Por exemplo, eu sei analisar tecnicamente o JSON, escrevi analisadores para muitos outros formatos de arquivo, mas eu precisaria encontrar e instalar uma solução de terceiros (e descobrir dependências e como construí-la) ou rodar a minha própria. Leva mais tempo do que não fazê-lo.
traço-tom-bang
4
Tudo leva mais tempo do que não fazê-lo. Mas as ferramentas que você precisa já foram escritas. Assim como você não precisa projetar um compilador para escrever um jogo, ou mexer no código da máquina, mas precisa aprender um idioma para a plataforma com a qual está trabalhando. Portanto, aprenda a usar um analisador json também.
Wildcard
Não tenho certeza de qual é o seu argumento. Nesta resposta, estou defendendo o YAGNI; se você não precisa gastar / perder tempo fazendo algo que não vai ajudá-lo, não precisa. Se você quer gastar tempo com isso, então ótimo. Talvez você precise gastar o tempo mais tarde, talvez não, mas fazê-lo com antecedência apenas o distrai da tarefa de realmente fazer o jogo. O desenvolvimento de jogos é trivial; todas as tarefas necessárias para criar um jogo são simples. É que a maioria dos jogos tem um milhão de tarefas simples e um desenvolvedor responsável escolhe os que atingem esse objetivo mais rapidamente.
traço-tom-bang
2
Na verdade, votei sua resposta; nenhum argumento real como tal. Eu só queria observar que o JSON não é difícil de analisar. Lendo novamente, suponho que estava principalmente respondendo ao trecho ", mas você precisa analisar e traduzir e todo esse jazz". Mas eu concordo que, para jogos de projetos pessoais, YAGNI. :)
Wildcard