Creando un juego para Game Boy, Parte 2

imagen

Hace unas semanas, decidí trabajar en un juego para Game Boy, cuya creación me dio un gran placer. Su nombre de trabajo es Aqua and Ashes. El juego tiene código abierto y está publicado en GitHub . La parte anterior del artículo está aquí .

Sprites fantásticos y dónde viven


En la última parte, terminé de renderizar varios sprites en la pantalla. Esto se hizo de una manera muy arbitraria y caótica. De hecho, tuve que indicar en el código qué y dónde quiero mostrar. Esto hizo que la creación de animación fuera casi imposible, pasó mucho tiempo de CPU y soporte de código complicado. Necesitaba una mejor manera.

Específicamente, necesitaba un sistema en el que simplemente pudiera iterar el número de animación, el número de fotograma y el temporizador para cada animación individual. Si tuviera que cambiar la animación, simplemente cambiaría la animación y restablecería el contador de cuadros. El procedimiento de animación realizado en cada cuadro simplemente debe elegir los sprites apropiados para mostrar y lanzarlos en la pantalla sin ningún esfuerzo de mi parte.

Y resultó que esta tarea está prácticamente resuelta. Lo que estaba buscando se llama mapeo de sprites . Los mapas de sprites son estructuras de datos que (en términos generales) contienen una lista de sprites. Cada mapa de sprites contiene todos los sprites para representar un solo objeto. También se asocian con ellos los mapas de animación (mapeos de animación) , que son listas de mapas de sprites con información sobre cómo hacer un bucle.

Es muy divertido que en mayo, agregué un editor de mapas de animación al editor de mapas de sprites listo para juegos de Sonic de 16 bits sobre Sonic. (Él está aquí , puedes estudiarlo) Todavía no está completo, porque es bastante duro, dolorosamente lento e incómodo de usar. Pero desde un punto de vista técnico, funciona. Y me parece que es bastante genial ... (Una de las razones de la aspereza fue que literalmente trabajé por primera vez con el marco de JavaScript). Sonic es un juego antiguo, por lo que es ideal como base para mi juego nuevo y antiguo.

Formato de tarjeta Sonic 2


Tenía la intención de usar el editor en Sonic 2 porque quería crear un truco para Genesis. Sonic 1 y 3K son básicamente lo mismo, pero para no complicarme, me limitaré a la historia de la segunda parte.

Primero, veamos los mapas de sprites. Aquí hay un sprite de Tails bastante típico, parte de la animación de parpadeo.


La consola Genesis crea sprites un poco diferente. El mosaico de Genesis (la mayoría de los programadores lo llaman "patrón") es 8x8, al igual que en Game Boy. El sprite consiste en un rectángulo de hasta 4x4 fichas, muy parecido al modo sprite 8x16 en Game Boy, pero más flexible. El truco aquí es que en la memoria estos mosaicos deben estar uno al lado del otro. Los desarrolladores de Sonic 2 querían reutilizar la mayor cantidad de mosaicos posible para un marco de Tails parpadeante de un marco de Tails permanente. Por lo tanto, Tails se divide en 2 sprites de hardware, que consisten en fichas de 3x2, una para la cabeza y la otra para el cuerpo. Se muestran en la figura a continuación.


La parte superior de este cuadro de diálogo son los atributos del sprite de hardware. Contiene su posición con respecto al punto de partida (los números negativos están cortados; de hecho, estos son -16 y -12 para el primer sprite y -12 para el segundo), el mosaico inicial utilizado en VRAM, el ancho y la altura del sprite, así como varios bits de estado para Imagen de espejo de sprite y paleta.

Los mosaicos se muestran en la parte inferior a medida que se cargan de la ROM a la VRAM. No hay suficiente espacio para almacenar todos los sprites de Tails en VRAM, por lo que los mosaicos necesarios deben copiarse en la memoria en cada fotograma. Se llaman señales de carga de patrón dinámico . Sin embargo, si bien podemos omitirlos, porque son casi independientes de los mapas de sprites y, por lo tanto, pueden agregarse fácilmente más adelante.


En cuanto a la animación, todo aquí es un poco más fácil. Un mapa de animación en Sonic es una lista de mapas de sprites con dos metadatos: el valor de la velocidad y la acción que se tomará una vez que finalice la animación. Las tres acciones más utilizadas son: un bucle sobre todos los fotogramas, un bucle sobre los últimos N fotogramas o una transición a una animación completamente diferente (por ejemplo, al cambiar de una animación de un Sonic de pie a una animación de sus impacientes golpes con el pie). Hay un par de comandos que especifican banderas internas en la memoria de los objetos, pero no muchos objetos los usan. (Ahora se me ocurrió que puede establecer el bit en la RAM del objeto a un valor cuando se repite la animación. Esto será útil para efectos de sonido y otras cosas).

Si observa el código desmontado de Sonic 1 (el código de Sonic 2 es demasiado grande para vincularlo), notará que el enlace a las animaciones no está hecho por ninguna ID. Cada objeto recibe una lista de animaciones y el índice de animación se almacena en la memoria. Para representar una animación específica, el juego toma un índice, lo busca en la lista de animaciones y luego lo muestra. Esto hace que el trabajo sea un poco más fácil, ya que no necesita escanear animaciones para encontrar la que necesita.

Limpiamos la sopa de las estructuras.


Veamos los tipos de tarjetas:

  1. Mapas de sprites: una lista de sprites que consta de un mosaico inicial, el número de mosaicos, la posición, el estado de reflexión (el sprite se refleja o no) y una paleta.
  2. DPLC: una lista de mosaicos de ROM que deben cargarse en VRAM. Cada artículo en un DPLC consta de un mosaico inicial y una longitud; cada artículo se coloca en VRAM después del último.
  3. Mapas de animación: una lista de animaciones que consiste en una lista de mapas de sprites, valores de velocidad y acciones de ciclo.
  4. Lista de animación: una lista de punteros a la acción de cada animación.

Dado que estamos trabajando con Game Boy, se pueden hacer algunas simplificaciones. Sabemos que en los mapas de sprites en un sprite de 8x16 siempre habrá dos mosaicos. Sin embargo, todo lo demás debe ser preservado. Por ahora, podemos abandonar completamente DPLC y simplemente almacenar todo en VRAM. Esta es una solución temporal, pero, como dije, este problema será fácil de resolver. Finalmente, podemos descartar el valor de la velocidad si suponemos que cada animación funciona a la misma velocidad.

Comencemos a descubrir cómo implementar un sistema similar en mi juego.

¡Consulte con commit 2e5e5b7 !

Comencemos con los mapas de sprites. Cada elemento en el mapa debe reflejar OAM (Object Attribute Memory - sprite VRAM) y, por lo tanto, un simple loop y memcpy serán suficientes para mostrar el objeto. Permítame recordarle que un elemento en OAM consiste en Y, X, un mosaico inicial y un byte de atributo . Solo necesito crear una lista de ellos. Utilizando el pseudo-operador ensamblado EQU, preparé el byte de atributo de antemano para tener un nombre legible para cada combinación posible de atributos. (Puede notar que en la confirmación anterior, reemplacé el mosaico Y / X en las tarjetas. Esto sucedió porque leí las especificaciones de OAM sin prestar atención. También agregué un contador de sprites para saber cuánto tiempo debería durar el ciclo).

Notarás que el cuerpo y la cola del zorro polar se almacenan por separado. Si se almacenaran juntos, entonces habría mucha redundancia, porque cada animación tendría que duplicarse para cada estado de cola. Y la escala de redundancia aumentaría rápidamente. En Sonic 2, el mismo problema surgió con Tails. Lo resolvieron allí, haciendo que Tails Tails sea un objeto separado con su propio estado de animación y temporizador. No quiero hacer esto porque no trato de resolver el problema de mantener la posición correcta de la cola en relación con el zorro.

Resolví el problema a través de mapas de animación. Si miras mi mapa de animación (único), entonces hay tres piezas de metadatos en él. Muestra la cantidad de tarjetas de animación, así que sé cuándo terminarán. (En Sonic, se verifica que la siguiente animación no sea válida, similar al concepto de cero byte en líneas C. Una solución de Sonic libera el caso, pero agrega una comparación que funcionaría en mi contra). Por supuesto, todavía hay una acción de bucle. (Convertí el circuito de Sonic de 2 bytes en un número de 1 byte en el que el bit 7 es el bit de modo). Pero también tengo el número de tarjetas de sprites , pero no estaba en Sonic. Tener varios mapas de sprites por cuadro de animación me permite reutilizar animaciones en varias animaciones, lo que, en mi opinión, ahorrará mucho espacio precioso. También puede notar que las animaciones están duplicadas para cada dirección. Esto se debe a que los mapas para cada dirección son diferentes y debe agregarlos.

imagen

Bailando con registros


Consulte este archivo en 1713848.

Comencemos dibujando un solo sprite en la pantalla. Entonces, confieso, mentí. Permítame recordarle que no podemos grabar en la pantalla fuera de VBlank. Y todo este proceso es demasiado largo para encajarlo en VBlank. Por lo tanto, necesitamos registrar el área de memoria que asignaremos para DMA. Al final, no cambia nada, es importante grabar en el lugar correcto.

Comencemos a contar registros. El procesador GBZ80 tiene 6 registros, de A a E, H y L. H y L son registros especiales, por lo que son muy adecuados para realizar iteraciones desde la memoria. (Como se usan juntos, se llaman HL). En un código de operación, puedo escribir en la dirección de memoria contenida en HL y agregarle uno. Esto es difícil de manejar. Puede usarlo como fuente o como destino. Lo utilicé como direcciones, y la combinación de registros BC como fuente, porque era más conveniente. Solo tenemos A, D y E. Necesito registrar A para operaciones matemáticas y similares. ¿Para qué se puede usar DE? Uso D como contador de bucle y E como espacio de trabajo. Y aquí es donde terminaron los registros.

Digamos que tenemos 4 sprites. Establecemos el registro D (contador de ciclos) en 4, el registro HL (destino), la dirección del búfer OAM y BC (la fuente) la ubicación en ROM donde se almacenan nuestras tarjetas. Ahora me gustaría llamar a memcpy. Sin embargo, surge un pequeño problema. ¿Recuerdas las coordenadas X e Y? Se indican en relación con el punto de partida, el centro del objeto se utiliza para colisiones y similares. Si los grabamos tal cual, entonces cada objeto se mostraría en la esquina superior izquierda de la pantalla. Esto no nos conviene. Para solucionar esto, necesitamos agregar las coordenadas X e Y del objeto a X e Y del sprite.

Nota breve: hablo de "objetos", pero no te expliqué este concepto. Un objeto es simplemente un conjunto de atributos asociados con un objeto en un juego. Los atributos son una posición, velocidad, dirección. descripción del artículo, etc. Hablo de esto porque necesito extraer datos de X e Y de estos objetos. Para hacer esto, necesitamos un tercer conjunto de registros que señalen el lugar en la RAM de los objetos donde se encuentran las coordenadas. Y luego necesitamos almacenar X e Y en algún lugar. Lo mismo se aplica a la dirección, porque nos ayuda a determinar en qué dirección miran los sprites. Además, necesitamos renderizar todos los objetos, por lo que también necesitan un contador de bucles. ¡Y todavía no hemos llegado a las animaciones! Todo se descontrola rápidamente ...

Revisión de decisiones


Entonces, estoy corriendo demasiado adelante. Volvamos y pensemos en cada dato que necesito rastrear y dónde escribirlo.

Para comenzar, dividamos esto en "pasos". Cada paso solo debe recibir datos para el siguiente, con la excepción del último que realiza la copia.

  1. Objeto (bucle): descubre si el objeto debe procesarse y lo procesa.
  2. Lista de animaciones: determina qué animación mostrar. También obtiene los atributos de un objeto.
  3. Animación (bucle): determina qué lista de mapas usar y representa cada mapa a partir de ella.
  4. Mapa (ciclo): recorre iterativamente cada sprite en la lista de sprites
  5. Sprite: copia los atributos de sprite en el búfer OAM

Para cada una de las etapas, he enumerado las variables que necesitan, los roles que desempeñan y los lugares donde almacenarlas. Esta tabla se parece a esto.

DescripciónTamañoEtapaUsoDe dondeLugarA donde
Tampón OAM2SpritePunteroHlHl
Fuente del mapa2SpritePunteroBCBC
Byte actual1SpriteEspacio de trabajoFuente del mapaE
X1SpriteVariableHiramUn
Y1SpriteVariableHiramUn
Inicio del mapa de animación.2Mapa de spritesPunteroStack3DE
Fuente del mapa2Mapa de spritesPuntero[DE]BC
Sprites restantes1Mapa de spritesRascarseFuente del mapaD
Tampón OAM1Mapa de spritesPunteroHlHlPila1
Inicio del mapa de animación.2AnimaciónEspacio de trabajoBC / Stack3BCStack3
Tarjetas restantes1AnimaciónEspacio de trabajoInicio de animaciónHiram
Numero total de tarjetas1AnimacionesVariableInicio de animaciónHiram
Dirección de objeto1AnimaciónVariableHiramHiram
Tarjetas por cuadro1AnimaciónVariableInicio de animaciónNO UTILIZADO !!!
Número de marco1AnimaciónVariableHiramUn
Puntero del mapa2AnimaciónPunteroAnimStart + Dir * TMC + MpF * F #BCDE
Tampón OAM2AnimaciónPunteroPila1Hl
Inicio de la mesa de animación.2Lista de animacionesEspacio de trabajoConjunto duroDE
Fuente de objeto2Lista de animacionesPunteroHlHlStack2
Número de marco1Lista de animacionesVariableFuente de objetoHiram
Número de animación1Lista de animacionesEspacio de trabajoFuente de objetoUn
Objeto X1Lista de objetosVariableFuente de objetoHiram
Objeto Y1Lista de animacionesVariableFuente de objetoHiram
Dirección de objeto1Lista de animacionesVariableObj srcHiram
Inicio del mapa de animación.2Lista de animacionesPuntero[Tabla de animación + Anima #]BC
Tampón OAM2Lista de animacionesPunteroDEPila1
Fuente de objeto2Ciclo de objetosPoste indicadorHard Set / Stack2Hl
Objetos restantes1Ciclo de objetosVariableCalculadoB
Campo de bit activo de un objeto1Ciclo de objetosVariableCalculadoC
Tampón OAM2Ciclo de objetosPunteroConjunto duroDE

Si, muy confuso. Para ser completamente honesto, hice esta tabla solo para publicación, para explicar más claramente, pero ya ha comenzado a ser útil. Trataré de explicarlo. Comencemos desde el final y lleguemos al principio. Verá todos los datos con los que empiezo: la fuente del objeto, el búfer OAM y las variables de bucle precalculadas. En cada ciclo, comenzamos con esto y solo esto, excepto que la fuente del objeto se actualiza en cada ciclo.

Para cada objeto que renderizamos, es necesario definir la animación mostrada. Mientras hacemos esto, también podemos guardar los atributos X, Y, Cuadro # y Dirección antes de incrementar el puntero del objeto al siguiente objeto y guardarlos en la pila para recuperarlos al salir. Usamos el número de animación en combinación con la tabla de animación codificada en el código para determinar dónde comienza el mapa de animación. (Aquí simplifico, suponiendo que cada objeto tiene la misma tabla de animación. Esto me limita a 256 animaciones por juego, pero es poco probable que supere este valor). También podemos escribir un búfer OAM para guardar algunos registros.

Después de extraer el mapa de animación, necesitamos encontrar dónde se encuentra la lista de mapas de sprites para un cuadro y dirección determinados, así como cuántos mapas necesita renderizar. Puede notar que no se utiliza la variable de tarjeta por cuadro. Sucedió porque no pensé y establecí el valor constante 2. Necesito arreglarlo. También necesitamos extraer el búfer OAM de la pila. También puede notar una falta total de control del ciclo. Se realiza en un subprocedimiento separado, mucho más simple, que le permite deshacerse del malabarismo con los registros.

Después de eso, todo se vuelve bastante simple. Un mapa es un conjunto de sprites, por lo que los rodeamos en un bucle y dibujamos en función de las coordenadas X e Y almacenadas. Sin embargo, de nuevo guardamos el puntero OAM al final de la lista de sprites para que el siguiente mapa comience donde terminamos.

¿Cuál fue el resultado final de todo esto? Exactamente igual que antes: un zorro polar agitando su cola en la oscuridad. Pero agregar nuevas animaciones o sprites ahora es mucho más fácil. En la siguiente parte, hablaré sobre fondos complejos y desplazamiento de paralaje.

imagen

Parte 4. Fondo de paralaje


Permítame recordarle que, en la etapa actual, tenemos sprites animados sobre un fondo negro sólido. Si no planeo hacer un juego de arcade de los 70, entonces esto claramente no será suficiente. Necesito algún tipo de imagen de fondo.

En la primera parte, cuando dibujaba gráficos, también creé varios mosaicos de fondo. Es hora de usarlos. Tendremos tres tipos de baldosas "básicas" (cielo, hierba y tierra) y dos baldosas de transición. Todos ellos están cargados en VRAM y listos para usar. Ahora solo tenemos que escribirlos en segundo plano.

Antecedentes


Los fondos de Game Boy se almacenan en la memoria en una matriz de 32x32 de mosaicos de 8x8. Cada 32 bytes corresponde a una línea de mosaicos.


Hasta ahora, planeo repetir la misma columna de mosaicos en todo el espacio de 32x32. Esto es genial, pero crea un pequeño problema: tendré que configurar cada mosaico 32 veces seguidas. Será mucho tiempo para escribir.

Instintivamente, decidí usar el comando REPT para agregar 32 bytes / línea, y luego usar memcpy para copiar el fondo en VRAM.

 REPT 32 db BG_SKY ENDR REPT 32 db BG_GRASS ENDR ... 

Sin embargo, esto significará que debe asignar 256 bytes para un solo fondo, lo cual es bastante. Este problema se exacerba si recuerda que copiar un mapa de fondo creado previamente con memcpy no le permitirá agregar otros tipos de columnas (por ejemplo, compuertas, obstáculos) sin una complejidad significativa y un montón de ROM de cartuchos desperdiciados.

Entonces, en cambio, decidí configurar una sola columna de la siguiente manera:

 db BG_SKY, BG_SKY, BG_SKY, ..., BG_GRASS 

y luego use un bucle simple para copiar cada elemento en esta lista 32 veces. (vea LoadGFX archivo LoadGFX de commit 739986a .)

La conveniencia de este enfoque es que luego puedo agregar una cola para escribir algo como esto:

 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 

Si decido representar BGMAP_overview, dibujará 1 columna de LeftGoal, después de lo cual habrá 30 columnas de Field y 1 columna de RightGoal. Si BGMAP_overview está en RAM, entonces puedo cambiarlo sobre la marcha dependiendo de la posición de la cámara en X.

Cámara y posición


Oh si, la cámara. Este es un concepto importante del que aún no he hablado. Aquí estamos tratando con una multitud de coordenadas, así que antes de hablar sobre la cámara, primero analizaremos todo esto.

Necesitamos trabajar con dos sistemas de coordenadas. El primero son las coordenadas de la pantalla . Esta es un área de 256x256 que puede estar contenida en la VRAM de la consola Game Boy. Podemos desplazar la parte visible de la pantalla dentro de estos 256x256, pero cuando vamos más allá de los bordes, colapsamos.

En ancho, necesito más de 256 píxeles, así que agrego coordenadas mundiales , que en este juego tendrán dimensiones de 65536x256. (No necesito altura adicional en Y, porque el juego tiene lugar en un campo plano). Este sistema está completamente separado del sistema de coordenadas de la pantalla. Toda la física y las colisiones deben realizarse en coordenadas mundiales, porque de lo contrario los objetos colisionarán con objetos en otras pantallas.


Comparación de pantalla y coordenadas mundiales

Dado que las posiciones de todos los objetos están representadas en coordenadas mundiales, deben convertirse en coordenadas de pantalla antes de renderizar. En el extremo izquierdo del mundo, las coordenadas mundiales coinciden con las coordenadas de la pantalla. Si necesitamos mostrar cosas a la derecha en la pantalla, entonces debemos tomar todo en coordenadas mundiales y moverlo a la izquierda para que estén en las coordenadas de la pantalla.

Para hacer esto, estableceremos la variable "cámara X", que se define como el borde izquierdo de la pantalla en el mundo. Por ejemplo, si la camera X es 1000, entonces podemos ver las coordenadas mundiales 1000-1192, porque la pantalla visible tiene un ancho de 192 píxeles.

Para procesar los objetos, simplemente tomamos su posición en X (por ejemplo, 1002), restamos la posición de la cámara igual a 1000 y dibujamos el objeto en la posición dada por la diferencia (en nuestro caso, 2). Para un fondo que no está en las coordenadas mundiales, pero que ya se describe en las coordenadas de la pantalla, establecemos la posición igual al byte inferior de la variable camera X de la camera X . Gracias a esto, el fondo se desplazará hacia la izquierda y hacia la derecha con la cámara.

Paralaje


El sistema que creamos parece bastante plano. Cada capa de fondo se mueve a la misma velocidad. No se siente tridimensional, y necesitamos arreglarlo.

Una forma simple de agregar simulación 3D se llama desplazamiento de paralaje. Imagine que conduce por una carretera y está muy cansado. El Game Boy se ha quedado sin baterías y debes mirar por la ventana del automóvil. Si miras el suelo a tu lado, verás. que se mueve a una velocidad de 70 millas por hora. Sin embargo, si observa los campos en la distancia, parecerá que se mueven mucho más lentamente. Y si nos fijamos en las montañas muy distantes, parece que apenas se mueven.

Podemos simular este efecto con tres hojas de papel. Si dibuja una cadena montañosa en una hoja, el campo en la segunda y el camino en la tercera, y colóquelos uno encima del otro de esta manera. para que cada capa sea visible, será una imitación de lo que vemos desde la ventana del automóvil. Si queremos mover el "auto" hacia la izquierda, entonces movemos la hoja superior (con el camino) hacia la derecha, la siguiente está un poco a la derecha y la última a la derecha.



Sin embargo, al implementar dicho sistema en Game Boy, surge un pequeño problema. La consola solo tiene una capa de fondo. Esto es similar al hecho de que solo tenemos una hoja de papel. No puede crear un efecto de paralaje con solo una hoja de papel. ¿O es posible?

H-blank


La pantalla de Game Boy se representa línea por línea. Como resultado de emular el comportamiento de los televisores CRT antiguos, hay un ligero retraso entre cada línea. ¿Qué pasa si podemos usarlo de alguna manera? Resulta que Game Boy tiene una interrupción de hardware especial específicamente para este propósito.

Similar a la interrupción VBlank, que solíamos esperar constantemente hasta el final del cuadro para grabar en VRAM, hay una interrupción HBlank. Al establecer el bit 6 del registro en $FF41 , encender la interrupción LCD STAT y escribir el número de línea en $FF45 , podemos decirle a Game Boy que inicie la interrupción LCD STAT cuando esté a punto de dibujar la línea especificada (y cuando está en su HBlank).

Durante este tiempo, podemos cambiar cualquier variable VRAM. No es mucho tiempo, por lo que no podemos cambiar más que un par de registros, pero aún tenemos algunas posibilidades. Queremos cambiar el registro de desplazamiento horizontal a $FF43 . En este caso, todo en la pantalla debajo de la línea especificada se moverá una cierta cantidad de desplazamiento, creando un efecto de paralaje.

Si vuelve al ejemplo de la montaña, puede notar un problema potencial. ¡Las montañas, las nubes y las flores no son líneas planas! No podemos mover la línea seleccionada hacia arriba y hacia abajo durante el proceso de renderizado; si lo elegimos, entonces permanece igual al menos hasta el próximo HBlank. Es decir, solo podemos cortar en líneas rectas.

Para resolver este problema, tenemos que hacerlo un poco más inteligentes. Podemos declarar alguna línea en el fondo como una línea que nada puede cruzar, lo que significa cambiar los modos de los objetos arriba y abajo, y el jugador no podrá notar nada. Por ejemplo, aquí es donde están estas líneas en la escena con la montaña.


Aquí hice rodajas justo encima y debajo de la montaña. Todo, desde la parte superior hasta la primera línea, se mueve lentamente, todo hasta la segunda línea se mueve a una velocidad promedio, y todo debajo de esta línea se mueve rápidamente. Este es un truco simple pero inteligente. Y al conocerlo, puedes notarlo en muchos juegos retro, principalmente para Genesis / Mega Drive, pero también en otras consolas. Uno de los ejemplos más obvios es la parte de la cueva de Mickey Mania. Puede notar que las estalagmitas y las estalactitas en el fondo están separadas exactamente a lo largo de una línea horizontal con un borde negro obvio entre las capas.

Me di cuenta de lo mismo en mi pasado. Sin embargo, hay un truco. Suponga que el primer plano se mueve a una velocidad que coincide con el movimiento de la cámara, y la velocidad del fondo es un tercio del movimiento de píxeles de la cámara, es decir, el fondo se mueve como un tercio del primer plano. Pero, por supuesto, un tercio del píxel no existe. Por lo tanto, necesito mover el fondo un píxel por cada tres píxeles de movimiento.

Si trabajó con computadoras capaces de realizar cálculos matemáticos, tomaría la posición de la cámara, la dividiría entre 3 y convertiría este valor en un desplazamiento de fondo. Desafortunadamente, Game Boy no es capaz de hacer la división, sin mencionar el hecho de que la división del programa es un proceso muy lento y doloroso. Agregar un dispositivo para dividir (o multiplicar) a una CPU débil para una consola de entretenimiento portátil en los años 80 no parecía ser un paso rentable, por lo que tenemos que inventar otra forma.

En el código, hice lo siguiente: en lugar de leer la posición de la cámara desde una variable, exigí que aumentara o disminuyera. Gracias a esto, con cada tercer incremento, puedo realizar un incremento de la posición de fondo, y con cada primer incremento, un incremento de la posición de primer plano. Esto complica un poco el desplazamiento a una posición desde el otro borde del campo (la forma más fácil es simplemente restablecer las posiciones de las capas después de una cierta transición), pero nos salva de la necesidad de dividirnos.

Resultado


Después de todo esto, obtuve lo siguiente:


Para un juego en Game Boy, esto es realmente genial. Hasta donde yo sé, no todos tienen el desplazamiento de paralaje implementado de esta manera.

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


All Articles