Ponto V!

Home Arquitetura Programação Data Driven Design
Bruno Crivelari Sanches
Data Driven DesignImprimir
Escrito por Bruno Crivelari Sanches

Neste artigo iremos explorar como utilizar um design data driven no desenvolvimento de jogos, permitindo assim que designers possam modificar e controlar o jogo sem que seja preciso a intervenção de um programador, além disso veremos como isso ajuda no re-uso do código permitindo que este seja aproveitado em projetos futuros.

Primeiramente vamos entender os termos que vão ser empregados neste texto, em especial hardcoded e data driven. O termo hardcode numa tradução direta significa algo como “código duro”, que poderíamos entender como algo que foi escrito diretamente código e é difícil de se mudar. Um exemplo dessa situação é um jogo onde a energia inicial do jogador esta inserida diretamente no código do jogo, nesse caso, para um designer fazer testes com diversos valores de energia na tentativa de balancear o jogo, este vai precisar solicitar a um programador que modifique o código a cada novo valor, depois esperar uma compilação, esperar que o programador envie os arquivos binários para o designer finalmente jogar. Sendo que é provável que o designer jogue por alguns minutos e conclua que o valor não ficou bom, então este deve começar o processo todo novamente, um grande desperdício de tempo de ambos profissionais.

Por data driven, podemos fazer uma tradução livre que poderia ser “conduzido pelos dados”, neste estilo, valores como energia de um personagem ficam fora do código fonte, sendo armazenados em arquivos de configuração ou até mesmo em uma linguagem de script. Dessa forma, apenas utilizando um aplicativo como o bloco de notas o jogador pode modificar os valores, salvar e testar, quantas vezes for preciso sem precisar da ajuda de outros profissinais, em alguns motores não é preciso nem mesmo sair do jogo.

Podemos perceber que é muito mais vantajoso remover certos valores do código fonte do jogo, assim um designer (e até mesmo os programadores) podem testar diversas combinações de uma maneira simples e rápida, aumentando em muito a produtividade da equipe. Outro fator importante é que com este sistema o mesmo código pode ser re-usado em diferentes projetos dependendo apenas de ajustes nos arquivos de configuração.

Com base nas vantagens que este tipo de construção proporciona, podemos começar a imaginar como implementar um sistema desse tipo. Primeiro deve-se ter em mente que não existe uma única resposta para esse problema ou uma fórmula magica para se resolver isso, com isso em mente vou então apresentar uma forma de se criar um design data driven para um jogo.

Criando uma Estrutura para um Design Data Driven

No motor de jogo chamado Phobos 3d que estou desenvolvendo foi criado um sistema de dicionários, onde um gerenciador de dicionários possui uma lista de grupos de tabelas. Cada grupo é usado para armazenar diferentes tipos de objetos do jogo, sendo possível criar um grupo para objetos do jogo, outro para objetos do sistema de física, para configuração dos sons, etc. Cada grupo possui uma coleção de tabelas, sendo que cada tabela representa um objeto do jogo ou um gabarito para criação de um objeto, como se fosse uma classe, mas ao invés de representar a estrutura do objeto, esta tabela representa os valores iniciais que o objeto deve possuir.

Como figuras valem por 1000 palavras, vamos a um exemplo de definição de um monstro:

EntityDef Monster
{
    health = 100;
    damage = 10;
    renderModel = monster.mesh;
}

No código acima esta sendo criada uma nova tabela, chamada de “Monster”, que vai ser armazenada no grupo “EntityDef”, sendo este nome uma abreviação para “Entity Definitions” (Definições de entidades), que é o grupo onde todos objetos do jogo são armazenados. Note que ali definimos a energia inicial do monstro (health = 100), o dano que este causa (damage = 10) e o modelo 3d que este deve utilizar (renderModel = monster.mesh). Este código fica definido em um arquivo texto e é acessado pelo motor do jogo toda vez que este roda, sendo processado e carregado na memória, assim um designer pode modificar as propriedades de qualquer objeto sem precisar modificar o código fonte do jogo.

Agora, vamos supor que o designer decidiu criar um monstro um pouco mais forte, que cause mais estragos ao jogador quando atacar este, ele poderia criar outra definição igual a anterior e modificar apenas o atributo damage, mas seria um grande perda de tempo ter que copiar tudo, então foi adicionado ao sistema um suporte a herança (semelhante ao sistema de qualquer linguagem orientada a objetos), dessa forma, para criamos o tal monstro poderoso precisamos apenas definir:

EntityDef SuperMonster: Monster
{
    damage = 25;
}

E assim temos um nova classe de objeto, que possui todos os atributos do classe Monster, mas com o atributo “damage” com um valor diferente. Detalhe que podemos inclusive criar novos atributos na classe “SuperMonster” sem qualquer tipo de restrição.

Com este sistema de script simples, podemos definir estruturas de dados que permitem uma grande flexibilidade na configuração jogo. Um detalhe importante é que este tipo de definição pode ser feita de varias formas, pode-se usar XML, JSon, Lua, etc, que tipo de ferramenta / linguagem utilizar fica a cargo do desenvolvedor encontrar a que melhor se adapta as suas necessidades.

Implementação Exemplo

Para implementar o sistema acima em C++ precisaremos de três classes principais, sendo estas:

  • DictionaryManager: responsável por gerenciar todos os dicionários e porta de entrada do sistema. Através desta classe que o programador pode solicitar que o sistema carregue um arquivo de definições ou então um determinado dicionário.
  • DictionaryHive: o hive é utilizado para armazenar uma coleção de dicionários, assim podemos criar os grupos (hives) para cada tipo de objeto, como por exemplo um grupo para objetos do jogo, um grupo para corpos rígidos, etc.
  • DictionaryTable: por fim temos a tabela, que nada mais é uma classe que armazena uma coleção de chaves e valores que armazenam os dados, para a definição do monstro mostrada acima, teríamos uma instância de DictionaryTable para “Monster” e outra para “SuperMonster”, sendo que a primeira vai conter 3 valores (health, damage e renderModel) e a segunda um único valor: damage.

Essas classes são representadas abaixo:

Diagrama de classes do sistema de dicionários

Além das três classes já citadas, temos duas auxiliares usadas na implementação, a classe Node é apenas uma classe usada para armazenar o nome de objetos e coleção de objetos, já a classe Parser é utlizada para processar o script. A partir do DictionaryManager o programador pode então invocar o método GetDictionary para obter um determinado dicionário, se quiser por exemplo, acessar os dados de Monster e descobrir qual sua energia inicial, pode invocar:

void exemplo()
{
    const DictionaryTable_c &table = DictionaryManager_c::GetInstance().GetTable("EntityDef", "Monster");

    cout << "Life do monstro: " << table.GetValue("health");
}

Sabendo como acessar as informações, vamos ver na sequência como utilizar este sistema dentro jogo.

Utilizando os Dados no Código Fonte

Como já sabemos como definir as propriedades dos objetos do jogo, temos que decidir como encaixar ou utilizar estas no motor do jogo. Vou assumir aqui que para cada tipo de objeto do jogo exista uma classe na linguagem escolhida que possa ser usada para representar os objetos do jogo. Vamos supor que existam as classes listadas abaixo:

  • LifeItem: item que o jogador coleta para ganhar energia
  • Monster: um inimigo qualquer
  • SuperMonster: o mesmo inimigo, mas com mais força
  • AmmoItem: uma munição genérica

Esses objetos podem ser representados pelo script abaixo:

EntityDef Monster
{
    health = 100;
    damage = 10;
    renderModel = monstro.mesh;
    spawnClass = MonsterEntity;
}

EntityDef SuperMonster: Monster
{
    damage = 25;
}

EntityDef LifeItem
{
    health = 50;
    renderModel = lifeItem.mesh;
    spawnClass = LifeItemEntity;
}

EntityDef AmmoItem
{
    ammo = 25;
    renderModel = ammoBox.mesh;
    spawnClass = AmmoBoxEntity;
}

No script acima definimos 4 tipos de objetos com suas propriedades, note que foi adicionada a propriedade “spawnClass”, que é quem indica o nome da classe que o motor do jogo deve instanciar para representar esse objeto no jogo. Aqui assumimos que o motor do jogo utiliza apenas uma hierarquia simples de objetos de jogo para representar seus objetos (mas o sistema pode ser facilmente adaptado a um sistema de componentes). Os objetos do exemplo poderiam ser representados pelas classes abaixo:

Hierárquia de classes dos objetos exemplo

Por exemplo, para o LifeItem, é esperado que no código (C++ nesse caso) possua em algum ponto uma declaração do tipo:

class LifeItemEntity_c: public Entity_c
{
    public:
        LifeItemEntity_c();

        void Load(const DictionaryTable_c &dictionary);
    
        //demais métodos e atributos
    private:
  //atributo que indica quanto de health o item fornece ao jogador
  float fpHealth;
};

Esta é classe concreta que o motor do jogo vai instanciar toda vez que precisar criar um LifeItem. Observe que este sistema é de certa forma intimamente ligado a definição de um nível do jogo e agora vemos outra vantagem desse sistema. O Editor de Níveis do jogo pode ler os arquivos de definições e assim construir uma lista dos objetos que o level designer pode usar na construção do jogo, assim acabamos resolvendo dois problemas com uma unica solução!

Agora podemos supor que o motor do jogo ao carregar um nível encontre uma descrição do tipo: Crie um “LifeItem” na posição x. O motor a principio não saberia dizer o que é isso, mas ele pode então consultar o dicionário de dados e vai acabar encontrando a definição de “LifeItem”, encontrada a definição este pode então ler a chave “spawnClass” e obter o nome da classe a ser usada para se instanciar um objeto, para instanciar o objeto pode-se usar uma factory.

Chegamos no ponto onde o motor do jogo já possui um mecanismo para criar objetos e supondo que este esta carregando um nível do jogo que possui instâncias de LifeItem, podemos assumir que vai ser criada uma instância da classe LifeItemEntity_c para cada um e que cada instância vai ter seu método Load chamado, sendo que o método Load pode ser construído de forma similar ao exemplo abaixo:

void LifeItemEntity_c::Load(const DictionaryTable_c &dictionary)
{
    fpHealth = dictionary.GetValue("health");
    
    this->SetRenderModel(dictionary.GetValue("renderModel"));
}

Assim, finalmente podemos ver que os valores usados pelos objetos do jogo vão sempre ser carregados de arquivos externos e não vão estar hardcoded dentro do código fonte. Observe que o designer pode facilmente mudar a quantidade de energia que o item fornece ou até mesmo mudar o modelo 3d que este utiliza apenas utilizando o bloco de notas ou um outro editor de textos qualquer.

Como comentário final é importante destacar que os valores armazenados nas tabelas dos dicionários são compartilhados por todos objetos, devendo ficar claro que mesmo que existam 100 instâncias de um LifeItem, todas vão referenciar a mesma tabela do dicionário, sendo que as características únicas de cada instância (como, por exemplo, a posição delas dentro um cenário), devem ser armazenadas no arquivo que descreve o nível do jogo.

Considerações Finais

Neste artigo vimos como podemos através de um sistema de script simples, criar uma poderosa ferramenta que ajuda em muito a produtividade dos desenvolvedores de jogos, sendo estes programadores ou designers. Em artigos futuros iremos explorar mais detalhes deste sistema, em especial detalhes de como funciona a implementação e o sistema de factory utilizado para criar os objetos do jogo.

Referências


Comentários (10)
  • Daniel "NeoStrider" Monteiro  - Isso me lembrou...
    avatar

    Isso me lembrou de dois artigos muito bons no gamasutra, que por acaso estava lendo esses dias, sobre dois jogos da Looking Glass, feitos com mesma engine: Thief - the dark project e System Shock 2.

    Talvez tenham alguma diferença mais substancial em relação ao seu artigo, mas de passada de olho, me pareceu. Pretendo ainda ler seu artigo mais tarde.

    []s

  • Bruno Crivelari Sanches
    avatar

    O Thief é um bom exemplo desse tipo de arquitetura, mas o enfoque maior deles era no sistema de componentes, que era extremamente flexível e acabava permitindo uma construção data-driven também. Mas o enfoque maior dos artigos que eu vi era sempre no sistema de componentes.

    []s

  • Vinícius Godoy de Mendonça  - NFS
    avatar

    Eu me lembro que nos Need for Speed, usaram uma abordagem data-driven para colocar dados sobre como policiais atuavam para perseguir bandidos.

    O mais interessante, é que criaram um dispositivo para coletar esses dados diretamente do carro da polícia, medindo como dobravam o volante, o quando pisavam ou soltavam o acelerador, e como se posicionavam na hora de perseguir um meliante.

    Depois, reproduziram os arquivos de dado no jogo e os game designers tiveram apenas que fazer ajustes finos no balanceamento.

  • Bruno Crivelari Sanches
    avatar

    Interessante isso, não conhecia isso sobre o NFS. Se lembrar do artigo e conseguir o link poste aqui :).

  • Bruno  - boost.Spirit
    avatar

    Daria para implementar algo usando Boost.Spirit não daria ?

  • Bruno Crivelari Sanches
    avatar

    Não vejo impedimentos, ao menos para fazer o parser.

    T+

  • Jonathan  - Otimo
    avatar

    Gostei muito desse artigo, tanto que to reescrevendo todo meu game deixando maioria das configurações.

    To usando o Ogre também, usando o Ogre Parser http://www.ogre3d.org/tikiwiki/All-purpose+script+parser&structure=Coo kbook

  • Bruno Crivelari Sanches
    avatar

    Interessante o parser do Ogre, eu não usei porque:
    - não lembrava dele :)
    - quero minimizar as dependências com Ogre no meu projeto

    Eu em breve devo publicar um próximo artigo me aprofudando mais nesse assunto, mostrando como fiz meu parser e como o sistema funciona internamente.

    Abraços

  • Emerson Max  - muito bom =D
    avatar

    Estava procurando sobre data driven design e olha só onde parei, Ponto V xD aliás, muito bom o artigo, meus parabéns!

  • Bruno Crivelari Sanches
    avatar

    opa! Obrigado Emerson!

Escrever um comentário
Your Contact Details:
Gravatar enabled
Comentário:
[b] [i] [u] [url] [quote] [code] [img]   
:angry::0:confused::cheer:B):evil::silly::dry::lol::kiss::D:pinch::(:shock:
:X:side::):P:unsure::woohoo::huh::whistle:;):S:!::?::idea::arrow:
Security
Por favor coloque o código anti-spam que você lê na imagem.
LAST_UPDATED2  

Busca

Linguagens

Twitter