DEV Community

Misael Braga de Bitencourt
Misael Braga de Bitencourt

Posted on

Criando um jogo em Javascript em apenas 13Kb

Como um aficcionado por games e programador, vez por outra, eu busco estudar um pouco sobre desenvolvimento de games. Há algum tempo atras, estava eu estudando como criar uma cena de jogo 2D utilizando apenas C e OpenGL, o que resultou em um projeto POC (prova de conceito) interessante.

Esse projeto fez-me interessar um pouco mais por processamento de imagens 2D e entender melhor como formatos como o PNG funcionam. Um tempo depois, eu me deparo com o anúncio de uma competição de desenvolvimento de games em Javascript, a js13kgames.

Nessa competição, os participantes têm de desenvolver o melhor jogo em javascript utilizando apenas 13kb em seu código fonte, o que inclui script, bibliotecas, sons e imagens! Tudo isso deve ser empacotado em apenas 13 kilobytes! O game deveria ser desenvolvido em um mês. Em Agosto, o tema do game em questão seria revelado. Em Setembro seria a entrega e em Outubro sairiam os resultados.

O jogo poderia ser comprimido com zip e o javascript minimizado. Criar scripts em apenas 13kb não é realmente um problema dado que temos javascript minifiers muito
eficientes a nossa disposição. Os sons em formato MIDI não são nada pesados. Tão pouco textos. A barreira está mesmo na criação dos gráficos.

Empolgado com o desafio, eu decidi fazer algo novo e inusitado em termos técnicos. Se eu decidisse por utilizar um sprite em png, para que este ocupasse pouco espaço, teria de ser de resolução MUITO baixa (foi o que muitos competidores utilizaram).

Criar gráficos vetoriais como SVG é outra solução óbvia. Todavia, a estética de "jogo de flash" seria quase que inevitável, além de as imagens todas ficarem parecendo recortes de papel (por incrível que pareça, o vencedor
utilizou essa abordagem, com todos os seus defeitos).

Imagens matriciais em poucos kilobytes

A abordagem que eu resolvi utilizar foi criar gráficos com imagens matriciais animadas. Estas seriam inseridas em um vetor javascript e não em um asset em binário.

Para desenhar as imagens, eu utilizei uma ferramenta open source de criação de mapas, o Tiled. Esse mapa, poderia ter apenas três variações: transparente, cinza e preto como exemplificado na imagem abaixo:

Tiled

A imagem abaixo pode ser exportada em json pelo Tiled. Uma das propriedades desse json é o vetor contendo a imagem, algo como:

/* Imagem do ícone de âncora no inicio do game */
[1, 1, 1, 1, 1, 2, 3, 3, 3, 2, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 2, 1, 1, 1, 1, 3, 3, 1, 1, 1, 2, 1, 1, 1, 1, 1, 2, 3, 1, 1, 1, 3, 3, 1, 1, 3, 3, 1, 1, 1, 1, 2, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 1, 1, 1, 1, 3, 3, 1, 1, 1, 2, 3, 1, 1, 3, 3, 3, 3, 1, 1, 1, 3, 3, 1, 1, 3, 3, 3, 3, 2, 3, 3, 3, 3, 1, 1, 1, 3, 3, 1, 1, 3, 3, 3, 3, 2, 1, 2, 3, 1, 1, 1, 1, 3, 3, 1, 1, 1, 2, 3, 1, 1, 1, 2, 3, 1, 1, 1, 1, 3, 3, 1, 1, 1, 2, 3, 1, 1, 1, 2, 3, 3, 1, 1, 1, 3, 3, 1, 1, 1, 3, 3, 1, 1, 1, 1, 3, 3, 3, 2, 2, 3, 3, 2, 2, 3, 3, 1, 1, 1, 1, 1, 1, 3, 3, 3, 3, 3, 3, 3, 3, 3, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 3, 3, 2, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 1, 2, 2, 1, 1, 1, 1, 1, 1, 1]
Enter fullscreen mode Exit fullscreen mode

Esse vetor representaria uma imagem. O número 1 seria o pixel transparente. O número 2, o pixel cinza e o número 3, o pixel preto. As imagens, por padrão, seriam em preto e branco. Na hora de renderizá-las, pode-se trocar a paleta de cores e uma imagem pode ser "azul forte" e "azul claro" no lugar de cinza e preto.

A imagem acima é uma imagem com a dimensão 16x16. Nesse caso, a cada 16 pixels, a função que realiza a renderização deve avançar uma linha a cada
16 posições do vetor.

Isso por si só já ajuda muito na compressão gzip, mas pode ficar muito menor! Por isso, a cada imagem dessa, eu passei por um script que diminuía 1 a cada número do vetor, utilizando apenas 0, 1 e 2. Depois disso, eu utilizo um número de 8 bits para representar 4 posições desse vetor. Por exemplo:

Para representar o vetor:

[2, 2, 1, 2]

Pode-se apenas usar o número 116.

116 em binário = 10100110

10 = 2 em binário
10 = 2 em binário
01 = 1 em binário
10 = 2 em binário

Se estivéssemos trabalhando com uma linguagem de baixo nível, isso não faria sentido, Mas como em um arquivo Javascript tudo é string, o texto 116 ocupa menos espaço em disco do que o texto [2,2,1,2].

Além disso, o script que faz essa compressão também cuida das repetições de zero. Como toda a parte transparente da imagem é 0, várias sequências de zero são adicionadas em um vetor. Isso pode ser substituído por apenas um número negativo representando a quantidade que se segue de zeros. Exemplo:

[1,0,0,0,0,0,0,0,0,0,0,0,0,2]
Enter fullscreen mode Exit fullscreen mode

trocando por:

[1,-12,2]
Enter fullscreen mode Exit fullscreen mode

Assim, temos uma string MUITO menor, quando a função de "descompactar" a imagem encontrar um número negativo, ela apenas adiciona x*-1 zeros no vetor.

O script que realiza essa compressão é esse abaixo:

(funciona tanto no console do browser quanto em um runtime como o NodeJS ou Bun)

const image = [1, 1, 3, ....];

const leftPad = (str, length) => {
    while (str.length < length) {
        str = '0' + str;
    }
    return str;
}

const IMAGE_ARRAY_NUMBER_LENGTH = 8;


function compressImage(image) {
    let byteBuffer = '';
    return image.reduce((acc, pixel) => {
        let pixelVal = pixel - 1;
        pixelVal = pixelVal > 2 ? 0 : pixelVal; /* Sometimes tiled exports wrong map tiles */
        pixelVal = pixelVal < 0 ? 1 : pixelVal; /* Sometimes tiled exports wrong map tiles */
        byteBuffer += leftPad(pixelVal.toString(2)+'', 2);
        if (byteBuffer.length === IMAGE_ARRAY_NUMBER_LENGTH) {
            const val = parseInt(byteBuffer, 2);
            acc.push(val);
            byteBuffer = '';
        }
        return acc;
    }, []);
}

function uncompressImage(compressed) {
    return compressed.reduce((acc, byte) => {
        let binaryNumber = leftPad((+byte).toString(2), IMAGE_ARRAY_NUMBER_LENGTH);
        while (binaryNumber.length) {
            const twoBits = binaryNumber.substring(0, 2);
            const twoBitsInInt = parseInt(twoBits, 2);
            acc.push(twoBitsInInt);
            binaryNumber = binaryNumber.substring(2, binaryNumber.length);
        }
        return acc;
    }, []);
}

function compressMore(compressed) {
    let buffer = 0;
    const compressedMore = compressed.reduce((acc, current) => {
        if (current === 0) {
            buffer += 1;
            return acc;
        }
        if (buffer) {
            acc.push(buffer * -1);
            buffer = 0;
        }
        acc.push(current);
        return acc;
    }, []);
    if (buffer) {
        compressedMore.push(buffer * -1);
    }
    return compressedMore;
}


const compressed = compressImage(image);
console.log(JSON.stringify(compressMore(compressed)));
const uncompressed = uncompressImage(compressed);
Enter fullscreen mode Exit fullscreen mode

O Resultado

Death Sea XIII

O game Death Sea XIII foi criado na abordagem mencionada. Para jogá-lo, basta acessar o link:

JOGAR

Death Sea XIII
Gameplay

Eu optei por criar um shooter 2d pelo fato de que é rápido programar esse tipo de game, tanto em suas mecânicas quanto no balanceamento de dificuldade e jogabilidade. O nome death sea (mar mortífero) foi escolhido por razões óbvias e o 13 (XIII em algarismo romano) se refere ao século onde a sua estória acontece e uma referência a competição.

A Competição

A js13kgames acontece todo o ano e é dividida em modalidades. Esse projeto foi inserido na modalidade de games desktop. Durante o desenvolvimento do Death Sea, eu acompanhei no Slack da competição os projetos que estavam sendo criados. Muitos deles são realmente impressionantes.

Eu recomendo o leitor interessado no assunto a dar uma olhada nesses projetos:

Eu recomendo checkar essa lista em vez de verificar os vencedores. Infelizmente, nenhum desses chegou ao TOP 10 na categoria Desktop. Os vencedores foram os games com a melhor interface gráfica. É um pouco desapontador que essa seja mais uma competição de webdesign do que de games em si. Por algum milagre, o meu game ficou entre os 100 primeiros (90º de 146) pois, dentre outros defeitos, a interface gráfica dele
deixou realmente a desejar.

Top comments (2)

Collapse
 
lucasm4sco profile image
Lucas

que dahora, parece que ficou legal parabéns

Collapse
 
justaguyfrombr profile image
Misael Braga de Bitencourt

Valeu!