Criando um jogo para o Game Boy, parte 2

imagem

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:

  1. 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.
  2. 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.
  3. 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.
  4. 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.

imagem

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.

  1. Objeto (loop) - descobre se o objeto deve ser renderizado e o renderiza.
  2. Lista de animações - determina qual animação exibir. Também obtém os atributos de um objeto.
  3. Animação (loop) - determina qual lista de mapas usar e renderiza cada mapa a partir dela.
  4. Mapa (ciclo) - percorre iterativamente cada sprite na lista de sprites
  5. 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 produtoTamanhoEstágioUseDe ondeLocalPara onde
Buffer OAM2SpritePonteiroHlHl
Origem do mapa2SpritePonteiroBCBC
Byte atual1SpriteEspaço de trabalhoOrigem do mapaE
X1SpriteVariávelHiramUm
Y1SpriteVariávelHiramUm
Início do mapa de animação2Mapa de SpritePonteiroStack3DE
Origem do mapa2Mapa de SpritePonteiro[DE]BC
Sprites restantes1Mapa de SpriteScratchOrigem do mapaD
Buffer OAM1Mapa de SpritePonteiroHlHlStack1
Início do mapa de animação2AnimaçãoEspaço de trabalhoBC / Stack3BCStack3
Cartões restantes1AnimaçãoEspaço de trabalhoInício da animaçãoHiram
Número total de cartões1AnimaçõesVariávelInício da animaçãoHiram
Direção do objeto1AnimaçãoVariávelHiramHiram
Cartões por moldura1AnimaçãoVariávelInício da animaçãoNÃO USADO !!!
Número do quadro1AnimaçãoVariávelHiramUm
Ponteiro de mapa2AnimaçãoPonteiroAnimStart + Dir * TMC + MpF * F #BCDE
Buffer OAM2AnimaçãoPonteiroStack1Hl
Início da tabela de animação2Lista de AnimaçãoEspaço de trabalhoConjunto rígidoDE
Origem do Objeto2Lista de AnimaçãoPonteiroHlHlStack2
Número do quadro1Lista de AnimaçãoVariávelOrigem do ObjetoHiram
Número da animação1Lista de AnimaçãoEspaço de trabalhoOrigem do ObjetoUm
Objeto X1Lista de objetosVariávelOrigem do ObjetoHiram
Objeto Y1Lista de AnimaçãoVariávelOrigem do ObjetoHiram
Direção do objeto1Lista de AnimaçãoVariávelObj srcHiram
Início do mapa de animação2Lista de AnimaçãoPonteiro[Tabela de Anim + Nº de Anim]BC
Buffer OAM2Lista de AnimaçãoPonteiroDEStack1
Origem do Objeto2Ciclo de objetosPlaca de sinalizaçãoConjunto Duro / Pilha2Hl
Objetos restantes1Ciclo de objetosVariávelCalculadoB
Campo de bit ativo de um objeto1Ciclo de objetosVariávelCalculadoC
Buffer OAM2Ciclo de objetosPonteiroConjunto rígidoDE

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.

imagem

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 mundo

Como 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.

Source: https://habr.com/ru/post/pt436918/


All Articles