Qual é uma boa maneira de armazenar dados do tilemap?

12

Estou desenvolvendo um jogo de plataformas 2D com alguns amigos da uni. Nós o baseamos no XNA Platformer Starter Kit, que usa arquivos .txt para armazenar o mapa de blocos. Embora isso seja simples, não nos dá controle e flexibilidade suficientes com o design de níveis. Alguns exemplos: para várias camadas de conteúdo, são necessários vários arquivos, cada objeto é fixado na grade, não permite a rotação de objetos, número limitado de caracteres etc. Por isso, estou pesquisando como armazenar os dados de nível e arquivo de mapa.

Isso diz respeito apenas ao armazenamento do sistema de arquivos dos mapas de blocos, não à estrutura de dados a ser usada pelo jogo enquanto estiver em execução. O mapa de blocos é carregado em uma matriz 2D, portanto, esta pergunta é sobre qual fonte a qual preencher a matriz.

Raciocínio para DB: Na minha perspectiva, vejo menos redundância de dados usando um banco de dados para armazenar os dados do bloco. Os ladrilhos na mesma posição x, y com as mesmas características podem ser reutilizados de nível para nível. Parece que seria simples o suficiente escrever um método para recuperar todos os blocos que são usados ​​em um nível específico do banco de dados.

Raciocínio para JSON / XML: arquivos editáveis ​​visualmente, as alterações podem ser rastreadas via SVN muito mais facilmente. Mas há conteúdo repetido.

Você tem alguma desvantagem (tempos de carregamento, tempos de acesso, memória etc.) em comparação com os outros? E o que é comumente usado na indústria?

Atualmente, o arquivo se parece com isso:

....................
....................
....................
....................
....................
....................
....................
.........GGG........
.........###........
....................
....GGG.......GGG...
....###.......###...
....................
.1................X.
####################

1 - Ponto inicial do jogador, Saída do nível X,. - Espaço vazio, # - Plataforma, G - Gem

Stephen Tierney
fonte
2
Qual "formato" existente você está usando? Apenas dizer "arquivos de texto" significa apenas que você não está salvando dados binários. Quando você diz "controle e flexibilidade insuficientes", quais são os problemas especificamente nos quais você está se deparando? Por que o lançamento entre XML e SQLite? Isso determinará a resposta. blog.stackoverflow.com/2011/08/gorilla-vs-shark
Tetrad
Eu optaria pelo JSON, pois é mais legível.
Den
3
Por que as pessoas pensam em usar o SQLite para essas coisas? É um banco de dados relacional ; por que as pessoas pensam que um banco de dados relacional cria um bom formato de arquivo?
Nicol Bolas
1
@StephenTierney: Por que essa pergunta passou repentinamente de ser XML vs. SQLite para JSON vs. qualquer tipo de banco de dados? Minha pergunta é especificamente por que não é apenas "Qual é uma boa maneira de armazenar dados de mapa de mosaico?" Esta questão X vs. Y é arbitrária e sem sentido.
Nicol Bolas
1
@Kylotan: Mas não funciona muito bem. É incrivelmente difícil de editar, porque você só pode editar o banco de dados via comandos SQL. É difícil de ler, pois as tabelas não são realmente uma maneira eficaz de observar um mapa de blocos e entender o que está acontecendo. E, embora possa fazer pesquisas, o procedimento de pesquisa é incrivelmente complicado. Conseguir que algo funcione é importante, mas se tornar o processo de desenvolvimento do jogo muito mais difícil, não vale a pena seguir o caminho mais curto.
Nicol Bolas

Respostas:

13

Portanto, lendo sua pergunta atualizada, parece que você está mais preocupado com "dados redundantes" no disco, com algumas preocupações secundárias sobre o tempo de carregamento e o que o setor usa.

Primeiro, por que você está preocupado com dados redundantes? Mesmo para um formato inchado como XML, seria bastante trivial implementar a compactação e diminuir o tamanho dos seus níveis. É mais provável que você ocupe espaço com texturas ou sons do que seus dados de nível.

Segundo, é mais provável que um formato binário seja carregado mais rápido que um formato baseado em texto que você precisa analisar. Duplamente, se você pode simplesmente deixá-lo na memória e ter suas estruturas de dados ali mesmo. No entanto, existem algumas desvantagens nisso. Por um lado, os arquivos binários são quase impossíveis de depurar (especialmente para pessoas de criação de conteúdo). Você não pode diferenciá-los ou versioná-los. Mas eles serão menores e carregarão mais rapidamente.

O que alguns mecanismos fazem (e o que considero uma situação ideal) é implementar dois caminhos de carregamento. Para o desenvolvimento, você usa algum tipo de formato baseado em texto. Realmente não importa qual formato específico você usar, desde que você use uma biblioteca sólida. Para a liberação, você alterna para carregar uma versão binária (carregamento menor e mais rápido, possivelmente sem entidades de depuração). Suas ferramentas para editar os níveis cospem os dois. É muito mais trabalhoso do que apenas um formato de arquivo, mas você pode obter o melhor dos dois mundos.

Tudo isso dito, acho que você está pulando um pouco a arma.

O primeiro passo para esses problemas é sempre descrever o problema que você está enfrentando. Se você sabe quais dados você precisa, você sabe quais dados você precisa armazenar. A partir daí, é uma questão de otimização testável.

Se você tiver um caso de uso para testar, poderá testar alguns métodos diferentes de armazenamento / carregamento de dados para medir o que considerar importante (tempo de carregamento, uso de memória, tamanho do disco etc.). Sem saber nada disso, é difícil ser mais específico.

Tetrad
fonte
8
  • XML: fácil de editar manualmente, mas depois que você começa a carregar muitos dados, ele fica mais lento.
  • SQLite: isso pode ser melhor se você quiser recuperar muitos dados de uma só vez ou apenas pequenos pedaços. No entanto, a menos que você esteja usando isso em outro lugar do seu jogo, acho que é um exagero para seus mapas (e provavelmente muito complicado).

Minha recomendação é usar um formato de arquivo binário personalizado. No meu jogo, só tenho um SaveMapmétodo que percorre e salva cada campo usando a BinaryWriter. Isso também permite que você escolha compactá-lo, se desejar, e oferece mais controle sobre o tamanho do arquivo. ou seja, salve um em shortvez de um intse você souber que não será maior que 32767. Em um loop grande, salvar algo como um em shortvez de um intpode significar um tamanho de arquivo muito menor.

Além disso, se você seguir esse caminho, recomendo que sua primeira variável no arquivo seja um número de versão.

Considere, por exemplo, uma classe de mapa (muito simplificada):

class Map {
    private const short MapVersion = 1;
    public string Name { get; set; }

    public void SaveMap(string filename) {
        //set up binary writer
        bw.Write(MapVersion);
        bw.Write(Name);
        //close/dispose binary writer
    }
    public void LoadMap(string filename) {
        //set up binary reader
        short mapVersion = br.ReadInt16();
        Name = br.ReadString();
        //close/dispose binary reader
    }
}

Agora, digamos que você queira adicionar uma nova propriedade ao seu mapa, digamos, um Listdos Platformobjetos. Eu escolhi isso porque é um pouco mais envolvido.

Primeiro de tudo, você incrementa MapVersione adiciona o List:

private const short MapVersion = 2;
public string Name { get; set; }
public List<Platform> Platforms { get; set; }

Atualize o método save:

public void SaveMap(string filename) {
    //set up binary writer
    bw.Write(MapVersion);
    bw.Write(Name);
    //Save the count for loading later
    bw.Write(Platforms.Count);
    foreach(Platform plat in Platforms) {
        //For simplicities sake, I assume Platform has it's own
        // method to write itself to a file.
        plat.Write(bw);
    }
    //close/dispose binary writer
}

Então, e é aqui que você realmente vê o benefício, atualize o método de carregamento:

public void LoadMap(string filename) {
    //set up binary reader
    short mapVersion = br.ReadInt16();
    Name = br.ReadString();
    //Create our platforms list
    Platforms = new List<Platform>();
    if (mapVersion >= 2) {
        //Version is OK, let's load the Platforms
        int mapCount = br.ReadInt32();
        for (int i = 0; i < mapCount; i++) {
            //Again, I'm going to assume platform has a static Read
            //  method that returns a Platform object
            Platforms.Add(Platform.Read(br));
        }
    } else {
        //If it's an older version, give it a default value
        Platforms.Add(new Platform());
    }
    //close/dispose binary reader
}

Como você pode ver, o LoadMapsmétodo pode não apenas carregar a versão mais recente do mapa, mas também versões mais antigas! Quando carrega os mapas mais antigos, você tem controle sobre os valores padrão que ele usa.

Richard Marskell - Drackir
fonte
8

História curta

Uma alternativa interessante para esse problema é armazenar seus níveis em um bitmap, um pixel por bloco. O uso do RGBA permite facilmente que quatro dimensões diferentes (camadas, IDs, rotação, tonalidade etc.) sejam armazenadas em uma única imagem.

Longa história

Essa pergunta me lembrou quando Notch transmitiu sua entrada no Ludum Dare há alguns meses e gostaria de compartilhar caso você não saiba o que ele fez. Eu pensei que era realmente interessante.

Basicamente, ele usou bitmaps para armazenar seus níveis. Cada pixel no bitmap correspondia a um "bloco" no mundo (não um bloco no seu caso, pois era um jogo transmitido por raios, mas perto o suficiente). Um exemplo de um de seus níveis:

insira a descrição da imagem aqui

Portanto, se você usar RGBA (8 bits por canal), poderá usar cada canal como uma camada diferente e até 256 tipos de blocos para cada um deles. Isso seria suficiente? Ou, por exemplo, um dos canais pode manter a rotação do bloco como você mencionou. Além disso, a criação de um editor de níveis para trabalhar com este formato também deve ser bastante trivial.

E o melhor de tudo: você nem precisa de um processador de conteúdo personalizado, pois você pode carregá-lo no XNA como qualquer outro Texture2D e ler os pixels em loop.

No IIRC, ele usou um ponto vermelho puro no mapa para sinalizar para um inimigo e, dependendo do valor do canal alfa desse pixel, ele determinaria o tipo de inimigo (por exemplo, um valor alfa de 255 pode ser um bastão, enquanto 254 seria um zumbi ou algo mais).

Outra idéia seria subdividir os objetos do jogo naqueles que são fixos em uma grade e aqueles que podem se mover "finamente" entre as peças. Mantenha os blocos fixos no bitmap e os objetos dinâmicos em uma lista.

Pode até haver uma maneira de multiplexar ainda mais informações nesses 4 canais. Se alguém tiver alguma idéia sobre isso, me avise. :)

David Gouveia
fonte
3

Você provavelmente poderia se safar de quase tudo. Infelizmente, os computadores modernos são tão rápidos que essa quantidade presumivelmente relativamente pequena de dados não faria uma diferença perceptível no tempo, mesmo que armazenados em um formato de dados inchado e mal construído, que exige uma enorme quantidade de processamento para ser lido.

Se você deseja fazer a coisa "certa", cria um formato binário como sugerido pelo Drackir, mas se você fizer outra escolha por algum motivo bobo, provavelmente não voltará e o morderá (pelo menos não em termos de desempenho).

aaaaaaaaaaaa
fonte
1
Sim, definitivamente depende do tamanho. Eu tenho um mapa com 161.000 peças. Cada um dos quais tem 6 camadas. Originalmente, eu o tinha em XML, mas tinha 82 MB e demorou uma eternidade para carregar. Agora, eu o salvo em formato binário e leva cerca de 5 MB antes da compactação e carrega em cerca de 200 ms. :)
Richard Marskell - Drackir 11/11
2

Veja esta pergunta: 'XML binário' para dados do jogo? Eu também optaria por JSON ou YAML ou mesmo XML, mas isso tem muita sobrecarga. Quanto ao SQLite, eu nunca usaria isso para armazenar níveis. Um banco de dados relacional é útil se você espera armazenar muitos dados relacionados (por exemplo, possui links relacionais / chaves estrangeiras) e se você solicita uma grande quantidade de consultas diferentes, o armazenamento de tilemaps não parece fazer esses tipos de coisas e acrescentaria complexidade desnecessária.

Roy T.
fonte
2

O problema fundamental dessa pergunta é que ela conflita dois conceitos que nada têm a ver um com o outro:

  1. Paradigma de armazenamento de arquivos
  2. Representação na memória dos dados

Você pode armazenar seus arquivos como texto sem formatação em algum formato arbitrário, JSON, script Lua, XML, um formato binário arbitrário, etc. E nada disso exigirá que sua representação na memória desses dados assuma qualquer forma específica.

O trabalho do seu código de carregamento de nível é converter o paradigma de armazenamento de arquivos em sua representação na memória. Por exemplo, você diz: "Vejo menos redundância de dados usando um banco de dados para armazenar os dados do bloco". Se você deseja menos redundância, é algo que seu código de carregamento de nível deve procurar. Isso é algo que sua representação na memória deve ser capaz de lidar.

É não algo suas necessidades de formato de arquivo para se preocupar com. E aqui está o porquê: esses arquivos precisam vir de algum lugar.

Você os escreverá manualmente ou usará algum tipo de ferramenta que cria e edita dados de nível. Se você estiver escrevendo à mão, o mais importante é um formato fácil de ler e modificar. Redundância de dados não é algo que o seu formato ainda precisa considerar, porque você estará gastando mais de seu tempo editando os arquivos. Deseja realmente usar manualmente quaisquer mecanismos que você criar para fornecer redundância de dados? Não seria melhor usar seu tempo apenas para o seu carregador de nível lidar com isso?

Se você possui uma ferramenta que os cria, o formato real só importa (na melhor das hipóteses) por uma questão de legibilidade. Você pode colocar qualquer coisa nesses arquivos. Se você deseja omitir dados redundantes no arquivo, crie um formato que possa fazê-lo e faça com que sua ferramenta use essa capacidade corretamente. Seu formato de mapa de mosaico pode incluir compactação RLE (codificação de comprimento de execução), compactação de duplicação, quaisquer técnicas de compactação que você desejar, porque é o seu formato de mapa de mosaico.

Problema resolvido.

Existem bancos de dados relacionais (RDB) para resolver o problema de executar pesquisas complexas em grandes conjuntos de dados que contêm muitos campos de dados. O único tipo de pesquisa que você faz em um mapa de blocos é "obter blocos na posição X, Y". Qualquer matriz pode lidar com isso. Usar um banco de dados relacional para armazenar e manter os dados do mapa de blocos seria um exagero extremo e não valeria nada.

Mesmo que você construa alguma compactação em sua representação na memória, ainda poderá vencer os RDBs facilmente em termos de desempenho e presença de memória. Sim, você terá que realmente implementar essa compactação. Mas se o tamanho dos dados na memória é uma preocupação que você consideraria um RDB, provavelmente você realmente deseja implementar tipos específicos de compactação. Você será mais rápido que um RDB e terá menos memória ao mesmo tempo.

Nicol Bolas
fonte
1
  • XML: realmente simples de usar, mas a sobrecarga se torna irritante quando a estrutura fica profunda.
  • SQLite: mais conveniente para estruturas maiores.

Para dados realmente simples, eu provavelmente seguiria a rota XML, se você já estiver familiarizado com o carregamento e a análise de XML.

Engenheiro
fonte