Algumas semanas atrás, eu decidi trabalhar em um jogo para o Game Boy, cuja criação me deu um grande prazer. Seu nome de trabalho é Aqua and Ashes. O jogo tem código aberto e é postado no
GitHub . A parte anterior do artigo está
aqui .
Sprites fantásticos e onde eles moram
Na última parte, terminei de renderizar vários sprites na tela. Isso foi feito de maneira muito arbitrária e caótica. Na verdade, eu tive que indicar no código o que e onde eu quero exibir. Isso tornou quase impossível a criação de animação, gastou muito tempo de CPU e suporte complicado a códigos. Eu precisava de um caminho melhor.
Especificamente, eu precisava de um sistema no qual pudesse simplesmente iterar o número da animação, o número do quadro e o timer de cada animação individual. Se eu precisasse alterar a animação, alteraria a animação e redefiniria o contador de quadros. O procedimento de animação realizado em cada quadro deve simplesmente escolher os sprites apropriados para exibição e jogá-los na tela sem nenhum esforço da minha parte.
E, como se viu, essa tarefa está praticamente resolvida. O que eu estava procurando é chamado de
mapeamento de sprites . Os mapas de sprites são estruturas de dados que (grosso modo) contêm uma lista de sprites. Cada mapa de sprites contém todos os sprites para renderizar um único objeto. Também estão associados a eles os
mapas de animação (mapeamentos de animação) , que são listas de mapas de sprites com informações sobre como fazer um loop.
É bem engraçado que, em maio, eu adicionei um editor de mapas de animação ao editor de mapas de sprite pronto para jogos de 16 bits do Sonic sobre o Sonic. (Ele está
aqui , você pode estudar) Ainda não está concluído, porque é bastante difícil, dolorosamente lento e inconveniente de usar. Mas, do ponto de vista técnico, funciona. E
me parece que é bem legal ... (Uma das razões para a aspereza foi que eu literalmente trabalhei com a estrutura JavaScript.) Sonic é um jogo antigo, por isso é ideal como base para o meu novo jogo.
Sonic 2 Card Format
Eu pretendia usar o editor no Sonic 2 porque queria criar um hack para o Genesis. Sonic 1 e 3K são basicamente quase os mesmos, mas, para não complicar, vou me limitar à história da segunda parte.
Primeiro, vejamos os mapas de sprite. Aqui está um sprite típico do Tails, parte da animação piscada.
O console do Genesis cria sprites um pouco diferente. O bloco Genesis (a maioria dos programadores chama de "padrão") é 8x8, assim como no Game Boy. O sprite consiste em um retângulo de até 4x4 peças, muito parecido com o modo de sprite 8x16 no Game Boy, mas mais flexível. O truque aqui é que, na memória, esses blocos devem estar próximos um do outro. Os desenvolvedores do Sonic 2 queriam reutilizar o maior número possível de peças para um quadro Tails piscando de um quadro permanente do Tails. Portanto, o Tails é dividido em dois sprites de hardware, consistindo em peças de 3x2 - uma para a cabeça e a outra para o corpo. Eles são mostrados na figura abaixo.

A parte superior desta caixa de diálogo são os atributos de sprite de hardware. Ele contém sua posição em relação ao ponto inicial (números negativos são cortados; na verdade, são -16 e -12 para o primeiro sprite e -12 para o segundo), o bloco inicial usado no VRAM, a largura e a altura do sprite, além de vários bits de status para imagem em espelho de sprite e paleta.
Os blocos são mostrados na parte inferior, à medida que são carregados da ROM para a VRAM. Não há espaço suficiente para armazenar todos os sprites do Tails na VRAM, portanto, os blocos necessários devem ser copiados para a memória em cada quadro. Eles são chamados de
dicas de carregamento de padrão dinâmico . No entanto, embora possamos ignorá-los, porque eles são quase independentes dos mapas de sprites e, portanto, podem ser facilmente adicionados posteriormente.
Quanto à animação, tudo aqui é um pouco mais fácil. Um mapa de animação no Sonic é uma lista de mapas de sprites com dois pedaços de metadados - o valor da velocidade e a ação a ser tomada após o término da animação. As três ações mais usadas são: um loop sobre todos os quadros, um loop sobre os últimos N quadros ou uma transição para uma animação completamente diferente (por exemplo, ao alternar de uma animação de um Sonic em pé para uma animação de seu impaciente toque com o pé). Existem alguns comandos que especificam sinalizadores internos na memória dos objetos, mas poucos objetos os utilizam. (Agora me ocorreu que você pode definir o bit na RAM do objeto para um valor ao fazer um loop na animação. Isso será útil para efeitos sonoros e outras coisas.)
Se você observar o código do
Sonic 1 desmontado (o código do Sonic 2 é muito grande para vincular), você perceberá que o link para as animações não é criado por nenhum ID. Cada objeto recebe uma lista de animações e o índice de animação é armazenado na memória. Para renderizar uma animação específica, o jogo pega um índice, procura-o na lista de animações e o renderiza. Isso facilita um pouco o trabalho, porque você não precisa digitalizar animações para encontrar a que você precisa.
Limpamos a sopa das estruturas
Vejamos os tipos de cartões:
- Mapas de sprites: uma lista de sprites que consistem em um bloco inicial, o número de blocos, posição, estado de reflexão (o sprite é espelhado ou não) e uma paleta.
- DPLC: uma lista de blocos de ROM que precisam ser carregados no VRAM. Cada item em um DPLC consiste em um bloco inicial e um comprimento; cada item é colocado na VRAM após o último.
- Mapas de animação: uma lista de animações que consiste em uma lista de mapas de sprites, valores de velocidade e ações de ciclo.
- Lista de animação: uma lista de ponteiros para a ação de cada animação.
Dado que estamos trabalhando com o Game Boy, algumas simplificações podem ser feitas. Sabemos que em mapas de sprite em um sprite de 8x16 sempre haverá dois blocos. No entanto, tudo o mais deve ser preservado. Por enquanto, podemos abandonar completamente o DPLC e apenas armazenar tudo em VRAM. Essa é uma solução temporária, mas, como eu disse, esse problema será fácil de resolver. Por fim, podemos descartar o valor da velocidade se assumirmos que cada animação funciona na mesma velocidade.
Vamos começar a descobrir como implementar um sistema semelhante no meu jogo.
Verifique com commit
2e5e5b7 !
Vamos começar com mapas de sprite. Cada elemento no mapa deve espelhar o OAM (Object Attribute Memory - sprite VRAM) e, portanto, um loop simples e um memcpy serão suficientes para exibir o objeto. Deixe-me lembrá-lo de que
um elemento no OAM consiste em Y, X, um bloco inicial e um byte de atributo . Eu só preciso criar uma lista deles. Usando o pseudo-operador EQU montado, preparei o byte de atributo antecipadamente para ter um nome legível para todas as combinações possíveis de atributos. (Você pode perceber que no commit anterior, substituí o bloco Y / X nos cartões. Isso aconteceu porque li as especificações do OAM de forma desatenta. Também adicionei um contador de sprite para saber quanto tempo o loop levaria.)
Você notará que o corpo e a cauda da raposa polar são armazenados separadamente. Se eles fossem armazenados juntos, haveria
muita redundância, pois cada animação teria que ser duplicada para cada estado de cauda. E a escala de redundância aumentaria rapidamente. No Sonic 2, o mesmo problema surgiu com o Tails. Eles o resolveram lá, fazendo do Tails caudas um objeto separado com seu próprio estado de animação e timer. Não quero fazer isso porque não tento resolver o problema de manter a posição correta da cauda em relação à raposa.
Eu resolvi o problema através de mapas de animação. Se você olhar para o meu mapa (único) de animação, há três partes de metadados nele. Ele mostra o número de cartões de animação, então eu sei quando eles terminarão. (No Sonic, verifica-se que a animação a seguir é inválida, semelhante ao conceito de zero byte nas linhas C. Uma solução da Sonic libera o caso, mas adiciona uma comparação que funcionaria contra mim.) Obviamente, ainda existe uma ação de loop. (Transformei o circuito do Sonic de 2 bytes em um número de 1 byte no qual o bit 7 é o bit do modo.) Mas também tenho o número
de placas de sprite , mas não estava no Sonic. Ter vários mapas de sprite por quadro de animação me permite reutilizar animações em várias animações, o que, na minha opinião, economizará muito espaço precioso. Você também pode perceber que as animações são duplicadas para cada direção. Isso ocorre porque os mapas para cada direção são diferentes e você precisa adicioná-los.
Dançando com registros
Consulte
este arquivo em 1713848.
Vamos começar desenhando um único sprite na tela. Então, eu confesso, eu menti. Deixe-me lembrá-lo de que não podemos gravar na tela fora do VBlank. E todo esse processo é longo demais para caber no VBlank. Portanto, precisamos registrar a área de memória que alocaremos para o DMA. No final, isso não muda nada, é importante gravar no lugar certo.
Vamos começar a contar registros. O processador GBZ80 possui 6 registros, de A a E, H e L. H e L são registros especiais, portanto, são adequados para executar iterações a partir da memória. (Como eles são usados juntos, eles são chamados HL.) Em um código de operação, eu posso escrever no endereço de memória contido no HL e adicionar um a ele. Isso é difícil de lidar. Você pode usá-lo como fonte ou como destino. Usei-o como endereços e a combinação de registros BC como fonte, porque era mais conveniente. Nós temos apenas A, D e E. Eu preciso do registro A para operações matemáticas e similares. Para que o DE pode ser usado? Eu uso D como um contador de loop e E como um espaço de trabalho. E foi aí que os registros terminaram.
Digamos que temos 4 sprites. Definimos o registro D (contador de ciclo) como 4, o registro HL (destino) o endereço do buffer OAM e BC (a fonte) o local na ROM onde nossos cartões estão armazenados. Agora eu gostaria de ligar para memcpy. No entanto, um pequeno problema surge. Lembra das coordenadas X e Y? Eles são indicados em relação ao ponto de partida, o centro do objeto é usado para colisões e similares. Se os gravarmos como estão, cada objeto será exibido no canto superior esquerdo da tela. Isso não nos convém. Para corrigir isso, precisamos adicionar as coordenadas X e Y do objeto em X e Y do sprite.
Nota curta: falo de "objetos", mas não expliquei esse conceito para você. Um objeto é simplesmente um conjunto de atributos associados a um objeto em um jogo. Os atributos são uma posição, velocidade, direção. descrição do item, etc. Falo sobre isso porque preciso extrair dados X e Y desses objetos. Para fazer isso, precisamos de um terceiro conjunto de registros apontando para o local na RAM dos objetos em que as coordenadas estão localizadas. E então precisamos armazenar X e Y em algum lugar. O mesmo se aplica à direção, porque nos ajuda a determinar em qual direção os sprites estão olhando. Além disso, precisamos renderizar
todos os objetos, para que eles também precisem de um contador de loop. E ainda não chegamos às animações! Tudo rapidamente sai do controle ...
Revisão da decisão
Então, estou correndo muito à frente. Vamos voltar e pensar sobre cada dado que preciso rastrear e onde escrevê-lo.
Para começar, vamos dividir isso em "etapas". Cada etapa deve receber apenas dados para a próxima, com exceção da última que executa a cópia.
- Objeto (loop) - descobre se o objeto deve ser renderizado e o renderiza.
- Lista de animações - determina qual animação exibir. Também obtém os atributos de um objeto.
- Animação (loop) - determina qual lista de mapas usar e renderiza cada mapa a partir dela.
- Mapa (ciclo) - percorre iterativamente cada sprite na lista de sprites
- Sprite - copia atributos de sprite para o buffer do OAM
Para cada um dos estágios, listei as variáveis de que precisam, os papéis que desempenham e os locais para armazená-las. Essa tabela se parece com isso.
Descrição do produto | Tamanho | Estágio | Use | De onde | Local | Para onde |
---|
Buffer OAM | 2 | Sprite | Ponteiro | Hl | Hl | |
Origem do mapa | 2 | Sprite | Ponteiro | BC | BC | |
Byte atual | 1 | Sprite | Espaço de trabalho | Origem do mapa | E | |
X | 1 | Sprite | Variável | Hiram | Um | |
Y | 1 | Sprite | Variável | Hiram | Um | |
|
Início do mapa de animação | 2 | Mapa de Sprite | Ponteiro | Stack3 | DE | |
Origem do mapa | 2 | Mapa de Sprite | Ponteiro | [DE] | BC | |
Sprites restantes | 1 | Mapa de Sprite | Scratch | Origem do mapa | D | |
Buffer OAM | 1 | Mapa de Sprite | Ponteiro | Hl | Hl | Stack1 |
|
Início do mapa de animação | 2 | Animação | Espaço de trabalho | BC / Stack3 | BC | Stack3 |
Cartões restantes | 1 | Animação | Espaço de trabalho | Início da animação | Hiram | |
Número total de cartões | 1 | Animações | Variável | Início da animação | Hiram | |
Direção do objeto | 1 | Animação | Variável | Hiram | Hiram | |
Cartões por moldura | 1 | Animação | Variável | Início da animação | NÃO USADO !!! | |
Número do quadro | 1 | Animação | Variável | Hiram | Um | |
Ponteiro de mapa | 2 | Animação | Ponteiro | AnimStart + Dir * TMC + MpF * F # | BC | DE |
Buffer OAM | 2 | Animação | Ponteiro | Stack1 | Hl | |
|
Início da tabela de animação | 2 | Lista de Animação | Espaço de trabalho | Conjunto rígido | DE | |
Origem do Objeto | 2 | Lista de Animação | Ponteiro | Hl | Hl | Stack2 |
Número do quadro | 1 | Lista de Animação | Variável | Origem do Objeto | Hiram | |
Número da animação | 1 | Lista de Animação | Espaço de trabalho | Origem do Objeto | Um | |
Objeto X | 1 | Lista de objetos | Variável | Origem do Objeto | Hiram | |
Objeto Y | 1 | Lista de Animação | Variável | Origem do Objeto | Hiram | |
Direção do objeto | 1 | Lista de Animação | Variável | Obj src | Hiram | |
Início do mapa de animação | 2 | Lista de Animação | Ponteiro | [Tabela de Anim + Nº de Anim] | BC | |
Buffer OAM | 2 | Lista de Animação | Ponteiro | DE | Stack1 | |
|
Origem do Objeto | 2 | Ciclo de objetos | Placa de sinalização | Conjunto Duro / Pilha2 | Hl | |
Objetos restantes | 1 | Ciclo de objetos | Variável | Calculado | B | |
Campo de bit ativo de um objeto | 1 | Ciclo de objetos | Variável | Calculado | C | |
Buffer OAM | 2 | Ciclo de objetos | Ponteiro | Conjunto rígido | DE | |
Sim, muito confuso. Para ser completamente honesto, criei esta tabela apenas para publicação, para explicar mais claramente, mas ela já começou a ser útil. Vou tentar explicar: vamos começar do final e chegar ao começo. Você verá todos os dados com os quais começo: a origem do objeto, o buffer do OAM e as variáveis de loop pré-computadas. Em cada ciclo, começamos com isso e somente isso, exceto que a fonte do objeto é atualizada em cada ciclo.
Para cada objeto que renderizamos, é necessário definir a animação exibida. Enquanto fazemos isso, também podemos salvar os atributos X, Y, Quadro # e Direção antes de incrementar o ponteiro do objeto para o próximo objeto e salvá-los na pilha para recuperar quando sair. Usamos o número da animação em combinação com a tabela de animação codificada no código para determinar onde o mapa da animação começa. (Aqui simplifico, assumindo que cada objeto tenha a mesma tabela de animação. Isso me limita a 256 animações por jogo, mas é improvável que exceda esse valor.) Também podemos escrever um buffer OAM para salvar alguns registros.
Após extrair o mapa de animação, precisamos descobrir onde está localizada a lista de mapas de sprites para o quadro e a direção especificados, bem como quantos mapas precisam ser renderizados. Você pode perceber que a variável do cartão por quadro não é usada. Isso aconteceu porque eu não pensei e defini o valor constante 2. Preciso corrigi-lo. Também precisamos extrair o buffer do OAM da pilha. Você também pode notar uma completa falta de controle do ciclo. É realizado em um subprocedimento separado, muito mais simples, que permite que você se livre do malabarismo com os registros.
Depois disso, tudo se torna bastante simples. Um mapa é um monte de sprites, então nós os contornamos em um loop e desenhamos com base nas coordenadas X e Y armazenadas. No entanto, salvamos novamente o ponteiro OAM no final da lista de sprites, para que o próximo mapa comece onde terminamos.
Qual foi o resultado final de tudo isso? Exatamente o mesmo de antes: uma raposa polar balançando a cauda no escuro. Mas adicionar novas animações ou sprites agora é muito mais fácil. Na próxima parte, falarei sobre planos de fundo complexos e rolagem de paralaxe.
Parte 4. Parallax Background
Deixe-me lembrá-lo, no estágio atual, temos sprites animados em um fundo preto sólido. Se eu não pretendo fazer um jogo de arcade dos anos 70, isso claramente não será suficiente. Eu preciso de algum tipo de imagem de fundo.
Na primeira parte, quando eu estava desenhando gráficos, também criei vários blocos de fundo. É hora de usá-los. Teremos três tipos "básicos" de ladrilhos (céu, grama e terra) e dois ladrilhos de transição. Todos eles estão carregados no VRAM e prontos para uso. Agora só precisamos escrevê-los em segundo plano.
Antecedentes
Os fundos do Game Boy são armazenados na memória em uma matriz de 32 x 32 de blocos de 8 x 8. A cada 32 bytes corresponde a uma linha de blocos.
Até agora, pretendo repetir a mesma
coluna de peças em todo o espaço 32x32. Isso é ótimo, mas cria um pequeno problema: precisarei definir
cada bloco 32 vezes seguidas. Vai demorar muito tempo para escrever.
Instintivamente, decidi usar o comando REPT para adicionar 32 bytes / linha e, em seguida, usar o memcpy para copiar o plano de fundo na VRAM.
REPT 32 db BG_SKY ENDR REPT 32 db BG_GRASS ENDR ...
No entanto, isso significa que você precisa alocar 256 bytes para apenas um plano de fundo, o que é bastante. Esse problema é agravado se você se lembrar de que copiar um mapa de plano de fundo criado anteriormente com o memcpy não permitirá adicionar outros tipos de colunas (por exemplo, portões, obstáculos) sem complexidade significativa e montes de ROM desperdiçada do cartucho.
Então, em vez disso, decidi configurar uma única coluna da seguinte maneira:
db BG_SKY, BG_SKY, BG_SKY, ..., BG_GRASS
e use um loop simples para copiar cada item desta lista 32 vezes. (consulte
LoadGFX
arquivo LoadGFX
do commit 739986a .)
A conveniência dessa abordagem é que mais tarde eu posso adicionar uma fila para escrever algo como isto:
BGCOL_Field: db BG_SKY, ... BGCOL_LeftGoal: db BG_SKY, ... BGCOL_RightGoal: db BG_SKY, ... ... BGMAP_overview: db 1 dw BGCOL_LeftGoal db 30 dw BGCOL_Field db 1 dw BGCOL_RightGoal db $FF
Se eu decidir renderizar BGMAP_overview, ele desenhará 1 coluna do LeftGoal, após o que haverá 30 colunas do Field e 1 coluna do RightGoal. Se
BGMAP_overview
estiver na RAM, eu posso alterá-lo
BGMAP_overview
, dependendo da posição da câmera em X.
Câmera e posição
Ah sim, a câmera. Este é um conceito importante sobre o qual ainda não falei. Aqui estamos lidando com uma infinidade de coordenadas; portanto, antes de falar sobre a câmera, primeiro analisaremos tudo isso.
Precisamos trabalhar com dois sistemas de coordenadas. O primeiro são as
coordenadas da
tela . Essa é uma área de 256x256 que pode estar contida no VRAM do console do Game Boy. Podemos rolar a parte visível da tela dentro desses 256x256, mas quando vamos além das bordas, caímos.
Em largura, preciso de mais de 256 pixels, então adiciono
coordenadas do mundo , que neste jogo terão dimensões de 65536x256. (Não preciso de altura extra em Y, porque o jogo ocorre em um campo plano.) Esse sistema é completamente separado do sistema de coordenadas da tela. Toda física e colisões devem ser executadas em coordenadas mundiais, porque, caso contrário, os objetos colidirão com objetos em outras telas.
Comparação de coordenadas de tela e mundoComo as posições de todos os objetos são representadas em coordenadas mundiais, elas devem ser convertidas em coordenadas da tela antes da renderização. Na extremidade esquerda do mundo, as coordenadas do mundo coincidem com as da tela. Se precisarmos exibir as coisas à direita na tela, precisamos pegar tudo nas coordenadas do mundo e movê-lo para a esquerda para que elas estejam nas coordenadas da tela.
Para fazer isso, definiremos a variável “camera X”, que é definida como a borda esquerda da tela no mundo. Por exemplo, se a
camera X
é 1000, podemos ver as coordenadas mundiais 1000-1192, porque a tela visível tem uma largura de 192 pixels.
Para processar objetos, simplesmente tomamos sua posição em X (por exemplo, 1002), subtraímos a posição da câmera igual a 1000 e desenhamos o objeto na posição dada pela diferença (no nosso caso, 2). Para um fundo que
não está nas coordenadas do mundo, mas já descrito nas coordenadas da tela, definimos a posição igual ao byte inferior da variável
camera X
da
camera X
. Graças a isso, o fundo rolará para a esquerda e para a direita com a câmera.
Parallax
O sistema que criamos parece bastante plano. Cada camada de fundo se move na mesma velocidade. Não parece tridimensional e precisamos corrigi-lo.
Uma maneira simples de adicionar simulação 3D é chamada rolagem de paralaxe. Imagine que você está dirigindo por uma estrada e está muito cansado. O Game Boy ficou sem pilhas e você deve olhar pela janela do carro. Se você olhar para o chão ao seu lado, verá. que ela está se movendo a uma velocidade de 70 milhas por hora. No entanto, se você observar os campos à distância, parece que eles estão se movendo muito mais devagar. E se você olhar para as montanhas muito distantes, elas parecem mal se mover.
Podemos simular esse efeito com três folhas de papel. Se você desenhar uma cadeia de montanhas em uma folha, o campo na segunda e a estrada na terceira e colocá-las umas sobre as outras como esta. para que cada camada fique visível, será uma imitação do que vemos na janela do carro. Se queremos mover o “carro” para a esquerda, movemos a folha superior (com a estrada) para a direita, a próxima é um pouco para a direita e a última é para a direita.
No entanto, ao implementar esse sistema no Game Boy, surge um pequeno problema. O console possui apenas uma camada de plano de fundo. Isso é semelhante ao fato de termos apenas uma folha de papel. Você não pode criar um efeito de paralaxe com apenas uma folha de papel. Ou é possível?
H-blank
A tela do Game Boy é renderizada linha por linha. Como resultado da emulação do comportamento de
TVs CRT antigas, há um pequeno atraso entre cada linha. E se pudermos usá-lo de alguma forma? Acontece que o Game Boy tem uma interrupção de hardware especial especificamente para esse fim.
Semelhante à interrupção do VBlank, que costumávamos esperar até o final do quadro para gravar em VRAM, há uma interrupção do HBlank. Definindo o bit 6 do registro em
$FF41
, ligando a interrupção do
LCD STAT
e escrevendo o número da linha em
$FF45
, podemos dizer ao Game Boy que inicie a interrupção do
LCD STAT
quando estiver prestes a desenhar a linha especificada (e quando estiver em seu HBlank).
Durante esse período, podemos alterar qualquer variável VRAM. Como não é
muito tempo, não podemos alterar mais do que alguns registros, mas ainda temos algumas possibilidades. Queremos alterar o registro de rolagem horizontal em
$FF43
. Nesse caso, tudo na tela abaixo da linha especificada se moverá em uma certa quantidade de turno, criando um efeito de paralaxe.
Se você voltar ao exemplo da montanha, poderá perceber um problema em potencial. Montanhas, nuvens e flores não são linhas planas! Não podemos mover a linha selecionada para cima e para baixo durante o processo de renderização; se escolhermos, permanecerá o mesmo pelo menos até o próximo HBlank. Ou seja, só podemos cortar em linhas retas.
Para resolver esse problema, temos que ser um pouco mais inteligentes. Podemos declarar alguma linha em segundo plano como uma linha que nada pode cruzar, o que significa alterar os modos dos objetos acima e abaixo dela, e o jogador não poderá perceber nada. Por exemplo, é aqui que essas linhas estão em cena com a montanha.
Aqui eu fiz fatias logo acima e abaixo da montanha. Tudo, do topo à primeira linha, move-se lentamente, tudo à segunda linha, a uma velocidade média, e tudo abaixo dessa linha, move-se rapidamente. Este é um truque simples, mas inteligente. E aprendendo sobre isso, você pode notar isso em muitos jogos retrô, principalmente no Genesis / Mega Drive, mas também em outros consoles. Um dos exemplos mais óbvios é a
parte da caverna de Mickey Mania. Você pode notar que as estalagmites e estalactites no fundo são separadas
exatamente ao longo de uma linha horizontal com uma borda preta óbvia entre as camadas.
Eu percebi a mesma coisa no meu passado. No entanto, há um truque. Suponha que o primeiro plano se mova a uma velocidade um em um, coincidindo com o movimento da câmera, e a velocidade do plano de fundo seja um terço do movimento de pixels da câmera, ou seja, o plano de fundo se mova como um terço do primeiro plano. Mas, é claro, um terço do pixel não existe. Portanto, preciso mover o pixel de segundo plano para cada três pixels de movimento.
Se você trabalhava com computadores capazes de cálculos matemáticos, você usaria a posição da câmera, dividiria por 3 e tornaria esse valor um deslocamento de fundo. Infelizmente, o Game Boy não é capaz de fazer a divisão, sem mencionar o fato de que a divisão do programa é um processo muito lento e doloroso. Adicionar um dispositivo para dividir (ou multiplicar) a uma CPU fraca para um console de entretenimento portátil nos anos 80 não parecia ser um passo econômico, portanto, temos que inventar outra maneira.
No código, fiz o seguinte: em vez de ler a posição da câmera de uma variável, exigi que ela aumentasse ou diminuísse. Graças a isso, com cada terceiro incremento, eu posso executar um incremento da posição de segundo plano e com cada primeiro incremento - um incremento da posição de primeiro plano. Isso complica um pouco a rolagem para uma posição a partir da outra extremidade do campo (a maneira mais fácil é simplesmente redefinir as posições das camadas após uma certa transição), mas evita que tenhamos que dividir.
Resultado
Depois de
tudo isso, consegui o seguinte:
Para um jogo no Game Boy, isso é realmente muito legal. Tanto quanto eu sei, nem todos eles têm rolagem de paralaxe implementada assim.