Embalagem de 93K níveis

Uma história de serialização binária personalizada para um jogo feito com Unity3D

Este ano, ajudei um cliente a desenvolver e lançar um jogo de quebra-cabeças de palavras para iOS e Android. Um dos desafios deste projeto era que o jogo deveria ser entregue localizado em 31 idiomas. Isso significa que cada idioma tem seu próprio conjunto de níveis. O cliente queria começar com níveis de 3K, que eles puderam gerar offline de maneira processual. O jogo é direcionado a pessoas que conhecem vários idiomas, portanto, alternar entre os idiomas deve ser fácil. Além disso, o jogo deve ser jogável offline e não houve esforços para construir uma equipe de BackEnd, então a coisa mais simples a fazer foi empacotar todos os 93K níveis com o jogo.

Existem muitas maneiras de representar um nível a ser carregado no Unity3D, mas quando se trata de 93 mil níveis, devemos manter o tempo de carregamento e o tamanho do pacote em mente. Os níveis gerados por procedimento vieram como um arquivo CSV por idioma, cada um com mais de 2 MB. 2 MB multiplicado por 31 idiomas resulta em 62 MB de dados apenas para os níveis. Essa era uma penalidade inaceitável, então tive que ser criativo.

Um nível em um jogo de quebra-cabeça de palavras consiste principalmente em … palavras. Em nosso jogo específico, cada palavra em um nível pode ser formada por até 9 caracteres diferentes. Essa restrição foi um detalhe muito bom, que nos ajudou a reduzir o nível de tamanho.

Digamos que temos 20 palavras em um nível e uma palavra pode ter de 3 a 9 caracteres. Para o inglês, resulta em um consumo de armazenamento entre 60 e 180 bytes, quando as palavras são armazenadas em ASCII / UTF-8 ou 120 a 360 bytes quando armazenamos as palavras em UTF-16. Quando se trata de outras línguas, japonês, russo, grego, etc … pagamos um preço ainda mais alto. Um japonês (letra katakana) ocupa 3 bytes em UTF-8 movendo a agulha para 180 a 440 bytes.

Mas e se armazenarmos as palavras não como uma sequência de letras, mas como uma sequência de índices de pesquisa? Cada palavra em um nível é uma permutação de caracteres X, onde X está entre 3 e 9.

OK, acho que é hora de dar um exemplo. Aqui estão as palavras para um dos níveis simples de inglês:

Todas as 5 palavras são formatáveis ​​por quatro caracteres G L O D .

Portanto, se criarmos uma tabela de pesquisa onde G = 0 , L = 1 , O = 2 , D = 3 .

Podemos escrever as palavras no quebra-cabeça da seguinte maneira:

Para tornar o ponto ainda melhor, aqui está um exemplo de um quebra-cabeça russo simples:

Se os caracteres forem representados como П = 0 , А = 1 , К = 2 , Р = 3 .

As palavras são traduzidas para:

Quando fazemos esta tradução de letras específicas de um idioma para números entre 0 e 8, introduzimos indireção, o que nos permite armazenar dados de uma forma mais compacta. Números entre 0 e 8 podem ser representados com apenas 4 bits (meio byte). Portanto, no caso de palavras em inglês:

Passamos de 16 bytes para 8 bytes, onde cada caractere é representado em 1 byte como uma letra em inglês, mas pode ser representado na metade de um byte como um número.

E no caso de palavras russas:

Reduzimos o tamanho de 42 bytes (cada letra é representada em 2 bytes) para 11 bytes, consumindo quase 4x menos memória no disco.

Palavras não são os únicos dados que precisam ser armazenados no nível, mas poder representar palavras de uma forma muito compacta foi um dos maiores ganhos para nós a fim de reduzir o tamanho do arquivo para 93K níveis. No final, conseguimos armazenar todos os níveis para um idioma em cerca de 400 KB, o que resulta em cerca de 12 MB para todos os 31 idiomas. Além disso, os arquivos são projetados de forma que um determinado nível possa ser decodificado quase instantaneamente, resultando em uma instância da classe Level , que pode ser usada pela lógica do jogo. Aqui está a API simplificada das classes Level e Word :

Como um pequeno bônus, gostaria de mencionar. Eu usei FlexBuffers para armazenar dados de localização de texto de interface do usuário e metagame. Eu escrevi uma postagem de blog muito mais extensa sobre os benefícios de usar FlexBuffers no Unity3D:

Para este jogo em particular, converti arquivos CSV com dados de localização de texto em arquivos FlexBuffer, o que nos permitiu alternar entre 31 idiomas instantaneamente com custo de carregamento quase zero também do ponto de vista da IU.

🙌 Obrigado por ler minha história. Entre em contato se tiver alguma dúvida ou se estiver interessado em saber mais sobre a serialização de dados binários personalizados.

👋

PS: Você pode me contratar para este tipo de trabalho 😉.