O TEMA DO FÓRUM ESTÁ EM MANUTENÇÃO. FEEDBACKS AQUI: ACESSAR

Estrutura dos Saves - O que é Importante Registrar?

Iniciado por Corvo, 09/02/2021 às 15:50

Há uns bons dias, estive conversando com o Syureri sobre os Pokémon de GB, saímos no assunto de como os jogos registravam o progresso do jogador nos arquivos de save. Ele pode explicar melhor, mas dentre os pontos mais interessantes está o cuidado em otimizar ao máximo o uso de memória, que não era muita. Por exemplo, uma única variável era responsável por registrar o progresso em todas as oito insígnias do ginásio. Como? A lógica é simples: ou jogador tem ou não tem cada uma, uma pergunta de sim ou não. Coincidentemente(?), uma variável de um byte equivale à exatamente oito bits:

0 0 0 0 0 0 0 0

Se apenas um bit for alterado, pode-se dizer que o jogador possui a insígnia de id x, e o espaço que seria necessário para guardar oito unidades de informação semelhantes cabem em um só:

0 1 0 0 0 0 0 0

Recebida esta iluminação, fiquei de abrir um tópico para ver como é que o pessoal anda se virando com isso, mas só hoje consegui alguns minutinhos. No meu caso, estive usando pontuações para aquele projeto. Por exemplo, digamos que a região/cidade x tenha n coisas à se fazer que contam como avanço no jogo, e eu tenha cem pontos disponíveis. Cada coisa tinha seu peso dados os pontos disponíveis, e assim se repetia para cada região do mapa.

Isto dá certa liberdade já que o jogador pode se mover livremente por todo o jogo, tendo apenas restrições de pontuação para certos acontecimentos. O problema é que este sistema deixa muito difícil saber se a coisa específica x foi concluída ou não, já que o progresso é amontoado com as outras. Eu poderia tentar algo como as insíginas do Pokémon e fazer o progresso de cada coisa conforme os bits de sua respectiva variável, mas ainda seria uma quantidade massiva de coisas à serem salvas e verificadas toda hora.

Como os senhores registram o progresso em seus respectivos jogos? Trocentas variáveis? Milhões de switches? E qual lógica utilizam para simbolizar isto?

09/02/2021 às 19:30 #1 Última edição: 09/02/2021 às 19:46 por Syureri
Qualquer coisa vejo se adiciono suporte pro resto das gerações naquele negócio ahuah


Interessante que a primeira vez que decidi brincar com binários foi quando migrei do Maker pro Unity. Precisava de uma forma flexível de salvar os dados do jogo e, no momento, tinha só tacado as informações num JSON da vida e deixado por isso mesmo. O problema é que dependendo da quantidade de informações guardadas no save, um JSON pode ficar gigante, imagina no caso do Terraria onde um mundo grande possui uma quantidade imensa de blocos.

Uma outra situação é quando você não deseja carregar o save inteiro na memória, apenas partes dele (como no caso de jogos de GB/GBC/GBA). O que é comum em jogos que possuem dois saves em um (caso de Pokémon da geração 3 pra cima, onde um serve de backup caso o outro se corrompa). Com JSON fica difícil, pois você não faz ideia das posições e tamanhos exatos de onde cada coisa fica. Tendo isso em mente, decidi aprender a guardar dados num arquivo binário ao invés de JSON.

O processador pode lidar com certas unidades de informação. Um byte ((u)int8) é composto de 8 bits e ocupa 1 byte, e é a menor unidade de informação que você pode acessar diretamente na memória (sim, você pode manipular os bits, mas não diretamente como um byte onde você simplesmente faz "a = b"). Um Short ((u)int16) é composto de 16 bits e ocupa 2 bytes, um Int ((u)int32) é composto de 32 bits e ocupa 4 bytes, finalmente um Long ((u)int64) é composto de 64 bits e ocupa 8 bytes. No futuro podemos ter processadores que conseguem lidar com unidades maiores que 64 bits, mas por enquanto ficamos nesses.

Tendo isso em mente, digamos que eu tenha duas unidades, uma que representa as moedas do jogador, e outra que representa a experiência que ele possui.
Se as moedas não passarem de 255, então podemos armazenar tudo em um único Byte, visto que o valor de um byte vai de 0-255 (-128-127 em caso de bytes que permitem números negativos (signed)).
A experiência do jogador pode se tornar um número um tanto quanto grande, então guardemos em um int de 32-bits, que vai de 0-4294967295 (-2147483647-   2147483647 em caso de números negativos (signed)).

Coins = 10
EXP = 30
Em binário (usarei hexadecimal por clareza) ficamos com:
Coins = 0A
EXP = 1E 00 00 00
Que juntos num arquivo fica:
0A 1E 00 00 00

Daí você só precisa ler o primeiro byte que representa Coins, e então os próximos 4 bytes que representam a experiência.

5 bytes. Muito menor que o equivalente em JSON, que seria:
Código: JSON
{
    "coins": 10,
    "exp": 30
}

Note que cada "caractere" equivale a 1 byte (em ASCII, UTF-8 pode usar mais bytes por caractere). Até mesmo o caractere de "nova linha" (\n).




Claro, ainda há problemas que não citei, como a ordem dos bytes na memória (endianness), mas no geral é muito mais fácil (e divertido) de se trabalhar com binários, assim que você abandona o medo de lidar com eles. Note que isso não se limita apenas aos arquivos salvos, mas sim qualquer coisa que você precise guardar em um arquivo, como o banco de dados, por exemplo.

10/02/2021 às 23:10 #2 Última edição: 10/02/2021 às 23:12 por Crixus
Teve uma época que eu usava só Array, eu não me batia com JSON por que me confundia muito com a estrutura e também por que queria escrever menos.

Citação de: Syureri online 09/02/2021 às 19:30Note que cada "caractere" equivale a 1 byte (em ASCII, UTF-8 pode usar mais bytes por caractere). Até mesmo o caractere de "nova linha" (\n).
Eu percebi isso quando verificava tamanho de arquivos de relatórios (ASCII) gerados por um software/servidor. Mas nesta lógica então, acentos e caracteres especiais consumiriam mais Bytes ainda? Provavelmente 2.

Citação de: Syureri online 09/02/2021 às 19:30
Claro, ainda há problemas que não citei, como a ordem dos bytes na memória (endianness), mas no geral é muito mais fácil (e divertido) de se trabalhar com binários.
Eu usava a calculadora do Windows pra tentar entender isso quando comecei à estudar "Hack-Rom" de jogos pra consoles antigos, nem preciso dizer que não fui muito longe.

11/02/2021 às 08:15 #3 Última edição: 11/02/2021 às 08:21 por Corvo
Bacana. E como os senhores decidem que informação precisa ser salva pra determinar o progresso do jogo? Por exemplo, em Super Metroid toda a evolução pode ser resumida aos itens. Eles definem por onde o jogador pode ou não passar, tornando o mundo mais ou menos livre. Outros jogos do SNES só salvam a fase atual e as vidas, como Super Mario World e os trocentos Super Bomberman (com os passwords e sem salvar as vidas, já que não tinham bateria. Acho.)

Com RPG, o buraco deve ser mais embaixo. Nunca tentei visualizar um arquivo de save deles, mas imagino que o Seventh Saga deva ter um esquema semelhante ao Metroid, mas e jogos mais complexos? Pesquisei sobre alguns modelos ou coisas do tipo, mas não encontrei nada que pudesse dar alguma ideia. Por exemplo, pelo que entendi do código do Pixel Dungeon, ele salva apenas os dados do personagem, mapa atual e conquistas desbloqueadas. Pode ser um ponto importante lembrar que os itens deste jogo são parcialmente aleatórios. Salvando somente o que o jogador tem no momento, ele evita a necessidade de um banco de dados, por exemplo.

Aquele ebook que custava uma fortuna há dois anos e hoje custa duas, pra falar a verdade, também é bem básico. No máximo explica que baús abertos uma vez deveriam, em teoria, não ser abertos de novo. Nestes casos ainda dá pra fugir das boleanas separando os baús/switches de um mesmo lugar em bits também.

Acho que o Syureri já deu o show em explicar sobre a codificação dos dados, então vou focar nessa questão:

CitarE como os senhores decidem que informação precisa ser salva pra determinar o progresso do jogo?

No geral, acho que não tem uma fórmula pra decidir isso. No fim das contas, o que o arquivo de save precisa fazer é permitir que você restaure o estado do jogo; isso pode ser atingido de várias maneiras, e qual é a melhor depende muito da natureza do jogo em questão.

A abordagem mais direta é a do RPG Maker, que simplesmente salva o estado completo num arquivo, usando um dump de objeto (do XP ao VX Ace, com o Marshal do Ruby, e no MV e MZ com JSON). Isso tem a vantagem de não ter que manter um modelo separado pro estado armazenado no save, que pode salvar algumas horas de desenvolvimento em alguma situação. No caso do RPG Maker, também tem a vantagem de que isso garante que toda mudança de estado será salva, então a maioria dos scripts/plugins não precisa se preocupar em alterar a estrutura e o procedimento de save, porque a informação que eles alteram já é salva. Também é bem fácil e direto carregar o arquivo pra memória, visto que o modelo é o mesmo.
Por outro lado, esse esquema torna o estado em memória muito dependente do estado salvo, então adiciona algumas complexidades (como, por exemplo, ter que garantir que seu modelo em memória é serializável), além de gerar arquivos com muita redundância dependendo do que é salvo.

Se o objetivo for minimizar os dados salvos, provavelmente um bom método é fazer o sistema de save do zero, e adicionar um a um os dados que forem necessários pra recuperar o estado completo do jogo (ou pelo menos o que interessa dele). Dá até pra validar usando testes automatizados que criem um estado de jogo, salvem, recarreguem e verifiquem que o estado resultante é equivalente ao estado inicial. É um processo mais demorado, e provavelmente se não for feito gradativamente durante o desenvolvimento do jogo vai dar um bom trabalho de mapear todos os dados que devem ser persistidos, mas é o mais assertivo em garantir que os dados salvos são necessários e suficientes para recuperar o estado do jogo.
~ Masked

11/02/2021 às 13:50 #5 Última edição: 11/02/2021 às 14:09 por Crixus
Citação de: Corvo online 11/02/2021 às 08:15E como os senhores decidem que informação precisa ser salva pra determinar o progresso do jogo?
Acho que realmente não entendi a proposta do assunto, deixe-me ver, isso varia do jogo. Não sei como explicar a estrutura em programação avançada, mas seguindo a lógica de Arrays e Vetores como eu fiz antes:
Spoiler
[234,240,
[1,0,1],[],[1,1,1,2,2,5],
[[0,48,48,38,38,17,17,18,23],[0,48,48,38,38,17,17,18,23]],
["newmusic","battle5",1,1,1,3,2,3,[1,2,5,7]]]
[close]

Dinheiro: [0]
Tempo de jogo: [1]
Switches: [2]
Switches 0003: Como não existe é nulo então considera Off.
Variáveis: [3]
Itens: [4] Contar por IDs, 1 seria Potion, junta os iguais ao carregar o menu.

Grupo: [5]
Personagem: [5][0]
Experiência: [5][0][0]
Max. HP: [5][0][1] (Esse tem que existir por que existem itens que aumentam sem envolver a curva do banco de dados)
HP atual: [5][0][2]

Mapa atual: [6]
Música de fundo atual: [6][0]
Habilitado Teleporte? (Troca de mapa): [6][2]
Direção Jogador: [6][5][1]
Jogador Coord. X: [6][6][2]
Jogador Coord. Y: [6][7][2]

Evento de mapa: [6][8]
ID do mesmo: [6][8][0]
Direção: [6][8][1]
Coord. X: [6][8][2]
Coord. Y: [6][8][2]

Assim alguns dados aumentariam o tamanho do SLOT save, para carregar dependeria exclusivamente do código de leitura que posicionaria os objetos na cena atual usando como base também as coordenadas do jogador.
O mesmo corrigiria qualquer erro com cálculos, tipo, alguém editou o HP pra 9999, mas o Max. HP não chega à isso, então deixaria igual ao máximo ou retornaria um erro de "Arquivo Corrompido".

Me desculpa, posso ter me confundindo em algo na estrutura.

É mais ou menos isso mesmo. Além do que o Syureri explicou ali (embutir moedas e experiência atual em só cinco bits), eu me refiro à conceitos genéricos sobre o uso da informação, não necessariamente da parte técnica. Esta, como disse o Brandt, depende de jogo pra jogo. Por exemplo, neste uso de arrays para definir o grupo:

Grupo: [5]
Personagem: [5][0]
Experiência: [5][0][0]
Max. HP: [5][0][1] (Esse tem que existir por que existem itens que aumentam sem envolver a curva do banco de dados)
HP atual: [5][0][2]


Se eu não tiver uma quantidade exorbitante de personagens, posso definir este ID como um valor de um único bit. O que sobrar na variável pode ser usado pras moedas e, talvez, coordenadas atuais do grupo. O mesmo pra cada atributos de personagens/inimigos, itens e afins. Inimigos que não tem possibilidade se cura não precisam de um identificador próprio pro HP máximo, por exemplo.

Mas é, não tem como fugir de uma montanha de variáveis de toda forma.  :viiish:

11/02/2021 às 15:57 #7 Última edição: 11/02/2021 às 19:18 por Crixus
Não sei se tem relação, mas me lembrei de uns jogos dos consoles 16-Bits (Não me recordo de ter visto nos de 8-Bits), como não tinham o sistema de save (Bateria de litium creio eu) por questão de valor ou tecnologia, usavam password.
Não eram todos, a maioria usava password estáticos, mas eu me refiro à jogos como alguns de futebol que pra recuperarmos o último momento tinham que inserir exatamente os caracteres/símbolos:

http://s.glbimg.com/po/tt/f/original/2013/09/30/pass-isss-deluxe.jpg

http://s.glbimg.com/po/tt/f/original/2013/09/30/pass-fifa.jpg

Olha, se me confundi foi mal, eu realmente não tinha muito acesso pra lembrar se recuperava a escala do time e outros dados relevantes durante um torneio.

Um jogo que também tem isso é o Doraemon:
https://tcrf.net/images/7/76/Doraemon_3_Nobita_to_Toki_no_Hougyoku_Debug_Password.png

Provavelmente seja esse mesmo método usado em alguns se não todos jogos da franquia Megaman das primeiras gerações, o Password deles tinham entre 12 e 16 dígitos, imagino que fosse pra pelo menos memorizar os mundos que já passou já que não é linear:
https://i1.sndcdn.com/artworks-000014552898-ewxkg9-t500x500.jpg

12/02/2021 às 15:24 #8 Última edição: 12/02/2021 às 16:48 por Dr.XGB
Quando eu estava desenvolvendo o Barrel Roller para a BraveJAM, pensei justamente nisso. Não queria usar os arquivos padrão de save do RM2000 (LSD e DSD, do Destiny), pois eu queria deixar o jogo mais leve possível por conta de regra de limite máximo de upload (se bem que o Rm2k pra dar 250MB tem que botar uma cacetada de arquivos e ainda falta espaço kkkkkkk !!). Então resolvi criar meu próprio arquivo de armazenamento, embora tenha ficado bem primitivo, a discussão desse tópico me fez lembrar do que eu precisei fazer na época.

Então eu catei meu OneNote e a primeira coisa que fiz foi como eu vou distribuir os valores necessários no arquivo e em qual endereço cada um será guardado:

Tá bem rabiscadão, bem thrash porque eu tinha 9 dias pra entregar o jogo auhasuhasuhsauhs !!

Certamente dá pra otimizar um bocado de espaço só nesse arquivo. Por exemplo: só o dia, mês, ano, hora, minutos e segundos dava pra colocar um único valor em um dword (que é o maior tipo suportado pelo Destiny, ou montava um esqueminha pra juntar 2 dwords para termos um qword, sei lá kkkkk !!) com a quantidade de milissegundos percorridos desde alguma data anterior. Geralmente o pessoal costuma fazer desde 1 de janeiro de 1970 até agora, mas pra driblar esse "limite" dava pra por uma data mais próxima, tipo 01/01/2000. E por que não aplicar o valor em segundos ao invés de milissegundos, já que não serão relevantes para o jogo? Só que pra implementar isso no RM em tão pouco tempo ia ser muito treta e por isso eu deixei assim mesmo tudo separadinho.
Feito isso, o arquivo ficou desse jeito:

Em 20 bytes consegui armazenar: um cabeçalho só pro jogo identificar o arquivo e proteger a extensão pro sistema aceitar somente este formato para leitura e escrita do save, dia, mês, ano, hora, minutos, segundo, tutoriais realizados, quantas vezes zerou um jogo (era um jogo muitíssimo curto, apenas 7 fases), contador de mortes e até mesmo quantas vezes os blocos foram movidos no decorrer da jogatina.
Muita coisa talvez seria desnecessária, não neste jogo já que a minha intenção era gerar uma estatística no final do jogo só para fins de curiosidades do próprio jogador. Então esse arquivinho serviu justamente pra guardar esses dados.
E então eu fiz a implementação da leitura e escrita desse arquivo no RPG Maker com ajuda do Destiny, que possui uma classe que manipula arquivos externos.
Ficou mais ou menos assim:





Bem esse é um exemplo de como a gente poderia fazer um sistema de save contendo somente o necessário.
Se a gente seguir essa lógica para um jogo mais complexo como um RPG que precisa guardar os dados de todos os heróis do grupo, que mapa ele está, em qual quest o nosso jogador está (estado da máquina), posição e tal, acredito que dá pra ter um arquivo muito mais leve que um JSON ou até o mesmo o LSD do Rm2k.

Quando eu estava planejando o meu vídeo sobre o arquivo de save do 2000, vi que lá dentro até que eles aplicaram compressão em algumas coisas mas por outro lado tinham algumas "encheções de linguiça" e acho que ele possa agregar com o assunto desse tópico.

Boa, tudo ajuda. Muito obrigado pelos comentários, pessoas. É bem mais viável do que eu imaginava.

Citação de: Crixus online 11/02/2021 às 15:57
[...]

Tem relação sim. Qualquer exemplo de "compressão" de informação vai ser bastante útil pra discussão, seja lá para o quê estiver sendo feito.

Citação de: Dr.XGB online 12/02/2021 às 15:24
[...]

Orra, essa aula eu não tinha visto. Está indo fundo mesmo o/.
Agora já não sei se, no meu caso, precisarei refazer todo o arquivo de save do zero, mas já me deram ideias de como reduzir na medida do possível.

12/02/2021 às 22:11 #10 Última edição: 12/02/2021 às 22:15 por Syureri
CitarMas nesta lógica então, acentos e caracteres especiais consumiriam mais Bytes ainda? Provavelmente 2.
Sim, caracteres com acento podem ocupar até 2 bytes em UTF-8. Caracteres asiáticos podem ocupar 4, e os famosos emojis e demais caracteres unicode podem ocupar até 6 bytes. O benefício de UTF-8 é que como a maioria dos caracteres que usamos caem dentro de ASCII, as chances de cada caractere ocupar apenas 1 byte é maior, como no caso dessa mensagem (que é guardada no banco de dados do fórum).




Excelentíssimo exemplo, Dr. (o/

Lembro que na época acabei fazendo um formato que aceitava dados com tamanhos variáveis pra guardar os recursos do jogo, funcionava tipo um zip haha'. Servia pra guardar tanto os arquivos do jogo quanto os dados de save. O benefício é que eu poderia abrir o arquivo e carregar apenas os bytes do recurso que eu queria (.png, .ogg, ...). As outras opções que eu via na época simplesmente carregavam o arquivo inteiro na memória, que certamente não era o que eu queria (especialmente pra música). Deve estar perdido numa das pastas do meu pc, qualquer dia acabo recuperando o código e ponho aqui no fórum (o/

27/02/2021 às 13:26 #11 Última edição: 27/02/2021 às 13:28 por Crixus
Eu tinha me esquecido de um detalhe, não sei se é padrão pra todos as linguagens de programação, você pode usar 1 e 0 ao invés de false e true (Bolean), que ocuparia menos espaço, apesar de não ser um Strings, o que pode ser que não faça diferença.

Eu não tenho como testar em outras linguagens, apenas me baseio no caso do Javascript com localStorage, ele transforma em String, então eu acho que usar um número ocuparia muito menos espaço.

Citação de: Crixus online 27/02/2021 às 13:26
Eu tinha me esquecido de um detalhe, não sei se é padrão pra todos as linguagens de programação, você pode usar 1 e 0 ao invés de false e true (Bolean), que ocuparia menos espaço, apesar de não ser um Strings, o que pode ser que não faça diferença.

Eu não tenho como testar em outras linguagens, apenas me baseio no caso do Javascript com localStorage, ele transforma em String, então eu acho que usar um número ocuparia muito menos espaço.
Sim, no C, por exemplo pode-se colocar 0 e 1 ao invés de false e true, respectivamente. A linguagem que vem à minha cabeça onde os booleans não podem ser substituídos por 0 e 1 é o Java e o Destiny, já que no Rm2k os booleans são switches, mas nada que um Convert.Byte(s[n]) resolva essa questão xD