Rambo Plus

Escolha sua língua: Português English

Introdução

game box

Download: rambop.v2.zip

Quando criança, Rambo era um dos meus jogos prediletos. Era um RPG bem díficil de completar, se você não soubesse a seqüência exata de telas a seguir! Eu gostava tanto do jogo, que acabei comprando um cartucho original japonês, e até mesmo traduzi alguns trechos do manual. Mas apesar disso tudo, tinha um detalhe que sempre me incomodou: a tela de abertura.

Se o jogo é todo colorido, porque a tela de abertura tinha que ser monocromática e feiosa? Certamente o pessoal da Pack-In-Video podia ter feito uma tela melhor. E então eu resolvi encarar o desafio, pegar o jogo original e melhorar a tela de abertura, mantendo os requisitos originais: MSX-1 com 16kb de RAM, e tudo dentro de um cartucho de 32kb de ROM.

Refazer a tela de abertura foi o trabalho de um fim de semana feito com a ajuda do Cyberknight, usando o Graphos III do Renato Degiovani.

O real desafio foi colocar de volta a tela dentro da ROM. A tela original, comprimida com RLE, ocupava 2.6kb de espaço. Então essa minha nova tela tinha que ser comprimida nesse mesmo espaço, o que se revelou ser uma tarefa extremamente difícil! No final, usando algoritmos no estado-da-arte em compressão, finalmente foi possível colocar a tela de volta dentro do jogo.

O resultado está disponível aqui para download . Divirtam-se!!!

A Missão

Além da tela de fundo, outra coisa me incomodava no Rambo original: o logo com sprites feiosos ampliados. Certamente dava pra fazer coisa melhor, mas a pergunta agora era: onde? A nova tela de abertura ocupou todos os espaços livres da ROM!

A única solução foi comprimir a própria ROM! Usando as rotinas de compressão da tela de abertura, foi possível comprimir os gráficos usados dentro do jogo. E, no espaço que sobrou, foi possível não apenas colocar um logo novo, mas também duas novidades!

A primeira é que essa versão roda corretamente em leitores de ROM, como o ExecROM. A segunda é que agora o jogo tem um modo profanado, com energia infinita e armas infinitas. Para ativar o modo profanado, basta deixar pressionada a tecla P na tela de abertura, até a música começar a tocar.

Tela de abertura

sshot sshot
Gráficos originais Tela de abertura aprimorada
sshot sshot

Resultados da compressão

Método Tamanho (em bytes)
Original12288
PCX11117
PNG5852
GIF4328
Info-ZIP (mode: -9)3544
Rambo Plus3189

Making of

O primeiro desafio de Rambo Plus foi, é claro, desenhar a tela de abertura. Como o processo usado foi muito similar ao usado em N-Sub, eu não vou entrar em detalhes, e vou focar mais no segundo, e mais difícil desafio: a compressão. A nova tela de abertura, comprimida pelo GIF, tinha 4328 bytes. A tela original usava os endereços de AC10h a B768h da ROM, e também tinha um espaço sobrando no final da ROM depois do endereço BD82h (preenchido com o texto 王様の耳はろばの耳, "o rei tem orelhas de burro"). Isso significava que apenas 3542 bytes estavam disponíveis, então qualquer compressão que eu fosse criar tinha que ser melhor que o GIF.

É aí que o problema começa! O GIF usa LZW, e esse algoritmo pode ser provado que é ótimo (quando o tamanho do arquivo vai para o infinito). Então como alguém pode fazer melhor que um algoritmo ótimo? A solução é o mantra da compressão: conheça seus dados. O único jeito de bater uma compressão estatística é modelando seus dados, extraindo redundâncias. Se tiver uma maneira de jogar dados fora, melhor ainda!

Dessa maneira, vamos ver o que sabemos sobre nossos dados. Sabemos que os dados representam uma imagem, então não apenas existe redundância na horizontal, mas também na vertical. Sabemos que a imagem é SCREEN 2, então cada oito pixels consecutivos podem ter apenas duas cores (de 15). Isso significa que podemos quebrar nossa imagem complexa em três imagens mais simples, a primeira sendo uma imagem monocromática que representa os padrões (pattern), e as outras duas contendo, respectivamente, as cores dos bits "1" e dos bits "0", sendo ainda que essas duas imagens têm apenas 1/8 do tamanho horizontal da primeira. As três imagens podem ser vistas abaixo, notando que eu aumentei oito vezes a largura das imagens com as cores para que todas tivessem o mesmo aspect ratio.

A imagem com os padrões é bem diferente das imagens com as cores, então eles vão precisar de tipos diferentes de compressão. Vamos analisar os padrões primeiro. De imediato notamos que existe bastante espaço vazio, então o algoritmo que formos escolher precisa tratar bem espaços vazios. Mas, antes de escolher o algoritmo, existe algum jeito de melhorar os dados? Dá pra extrair redundâncias? Tem como jogar dados fora? A resposta, surpreendentemente, é sim, podemos usar uma compressão com perdas! O truque das compressões com perdas é que o usuário final não pode perceber que foram jogados dados fora. No mp3, por exemplo, são removidas todas as freqüências que o ouvido humano não consegue ouvir. No nosso contexto, a imagem deve parecer a mesma, depois de retirados os dados.

Então, como fazer? Já que vamos buscar um algoritmo focado em comprimir de maneira eficiente os espaços vazios, então qualquer transformação que aumente os espaços vazios é boa. Na SCREEN 2, nós temos duas cores para cada 8 pixels, se o pixel é "1", então escolhemos a primeira cor (INK), se o pixel é "0", então escolhemos a segunda cor (PAPER). Mas o que acontece se os oito pixels tem a mesma cor? Então tanto faz escolher tudo um e tornar o PAPER irrelevante, ou escolher tudo zero e tornar o INK irrelevante. Já que queremos aumentar os espaços vazios, iremos escolher então tudo zero. Se a imagem original tem tudo um, então só precisamos trocar INK com PAPER e complementar o pattern, isso é uma operação com perdas, mas o usuário não tem como notar.

Ainda tem mais operações com perdas que podemos fazer. Suponha que um octeto não tem apenas zeros ou apenas uns. Ele deve ter duas cores então. Mas e se o artista (por erro ou algum outro motivo) usou a mesma cor para PAPER e INK? Nesse caso a informação do pattern é irrelevante! Podemos, com segurança, jogar fora o pattern e trocá-lo por tudo zeros. Depois de fazer as duas operações citadas, as imagens ficam como abaixo:

Como podemos ver, o Rambo propriamente dito já estava bem formatado, mas conseguimos aumentar o espaço vazio no helicóptero. Terminado o modelamento dos dados, agora podemos ver a compressão. Só de olhar para a imagem com os padrões já dá pra ver que o RLE não vai funcionar. Nós temos um monte de espaço vazio que o RLE iria codificar, mas o RLE só opera em um eixo, e precisamos explorar a conectividade bidimensional da imagem. Isso usualmente é feito com análise espaço-frequencial, como wavelets, mas no Z80 wavelets seriam lentas demais. Podemos, entretanto, usar quad-trees!

A idéia por trás das quad-trees é simples. Esse algoritmo recursivo começa olhando pro retângulo atual (que no início é a tela toda). O retângulo é vazio? Se for, imprima um "0". Se não for, imprima um "1", quebre o retângulo em quatro, e repita o procedimento pra cada retângulo menor, até que ele seja vazio, ou que consista de um único pixel, nesse caso, imprima seu valor. O algoritmo pode comprimir regiões inteiras para um único bit, então é bem apropriado no nosso caso. Porém, ainda temos que fazer uns ajustes pequenos. Como nossa tela inicial de 256x192 não é quadrada, podemos quebrá-la em três regiões de 256x96, e parar a recursão quando o retângulo for 4x1, dessa maneira cada passo da recursão pode ser facilmente calculado como x/2 e y/2. Na imagem abaixo, eu pintei cada retângulo vazio com um tom cada vez mais claro de vermelho, então é possível ver a partição do espaço feita pelo algoritmo.

O resultado parece bom, tem grandes áreas com vermelho escuro, então essas áreas foram comprimidas para poucos bits. A bitstream completa para essa quad-tree no final ficou com apenas 2334 bytes, muito bom. Agora precisamos comprimir as imagens com as cores. A abordagem de espaços vazios não vai funcionar, mas podemos notar as cores em uma mesma coluna vertical são iguais na maioria dos casos. Podemos melhorar isso, jogando dados fora ou modificando de uma maneira que o usuário não perceba? Claro! Suponha que o artista pintou um octeto com azul sobre vermelho, e o seguinte com vermelho sobre azul. Se nós invertermos o pattern, trocando zeros com uns, podemos também trocar INK com PAPER, e os dois octetos agora terão a mesma cor. Um jeito simples de fazer isso é ordenando lexicograficamente todos os octetos, por exemplo, impondo que INK precisa sempre ser maior que PAPER. Depois de ordenas as cores em todos os octetos, o comprimento das faixas verticais vai aumentar.

Ainda tem outra transformação que podemos fazer. Nós vimos anteriormente que padrões com oito pixels iguais foram trocados para tudo zero, e suas cores foram setadas no PAPER. Mas e o INK delas? Nós podemos escolher qualquer cor para o INK, já que ele não vai ser usado. De modo a aumentar ainda mais o tamanho das nossas barras verticais, podemos escolher o INK como sendo o mesmo do octeto anterior. Após fazer as duas operações, as imagens ficam assim:

Muito melhor! E precisamos notar que, apesar de termos mudado bastante as imagens, o usuário não vai perceber nada. Só precisamos agora escolher o método de compressão. As faixas verticais seriam bem codificadas pelo RLE, mas tem alguns padrões que ficariam ruins, como as cores alternadas no cabelo do Rambo. Essas cores alternadas seriam bem comprimidas por alguma variante do LZ, mas variantes de LZ não seriam boas nas faixas verticais.

A solução é usar o RLE e o LZ ao mesmo tempo! Podemos, por exemplo, imprimir um "0" se a string que seguir for codificada com RLE, e imprimir um "1" se a string que seguir for codificada com LZ. O truque pra fazer isso funcionar é escolher certinho o número de bits que usam o RLE e o LZ. Lembre que o RLE precisa de um número que identifica quantas vezes devemos repetir a cor. Se esse número for representado com poucos bits, então faixas muito grandes podem precisar de várias strings para serem codificadas, o que não é bom. Por outro lado, se escolhermos bits demais, faixas pequenas terão um monte de bits 0 sobrando na parte mais significativa do número. No LZ o mesmo se aplica, mas temos dois números pra se preocupar: o tanto que precisamos voltar na string, e o tanto que devemos copiar a partir desse ponto.

Como escolher a distribuição ótima de bits então? Se estivéssemos fazendo um compressor genérico, isso poderia ser um problema. Mas nós vamos comprimir a abertura do Rambo uma única vez. Nesse caso, nada será melhor que uma busca por força bruta no espaço dos números! Mesmo se a força bruta levar algumas horas, não tem problema, já que esse trabalho será feito uma única vez. Depois de implementado, o algoritmo nem precisou de tanto tempo assim, alguns minutos foram suficientes. Os números mágicos são 7 bits para o RLE, 8 bits para o ponteiro de retorno do LZ, e 5 bits para a repetição do LZ. A bitstream final, somando os padrões com as cores, ficou com apenas 3189, o que não apenas pequeno o suficiente para caber na ROM, mas também menor que o Info-ZIP na compressão máxima, o que é muito legal!

O toque final é escrever um decoder para essas bitstreams, que seja rápido o suficiente para que o usuário não perceba a descompressão, e pequeno o suficiente para caber no pouco espaço que resta. Isso foi feito usando o Super Otimizador. Ufa! Foi um monte de trabalho só pra ter uma tela de abertura colorida, mas o resultado final valeu a pena.

Créditos

Jogo Original
Pack-in-Video 1985
Reprogramação
Ricardo Bittencourt
Nova Tela de Abertura
Ricardo Bittencourt
Cyberknight
Beta Testing
Daniel Caetano
Modo Profanado
Paulo Maluf
Scan da Caixa Original
Ricardo Bittencourt
< Voltar para o blog do Ricbit

< Voltar para o Mundo Bizarro
Autor: Ricardo Bittencourt
Data: 2004.09.15
Copyright © 2004 Ricardo Bittencourt