01Primeros pasos
Esta guía explica cómo funciona HARAPEKO SHIROHEBI, desde las entradas del mando hasta los píxeles de la pantalla. El juego es obra de DAISUKE OBA. Hay que comer granadas para que crezca la serpiente blanca, evitando las minas y el propio cuerpo. El programa incluido, los recursos originales y esta guía se distribuyen con la licencia MIT.
La pantalla tiene 160 × 144 píxeles. Una misma ROM admite la Game Boy original (DMG) y la Game Boy Color (CGB); el modo de color se elige durante la ejecución. El cartucho utiliza MBC5, 64 KiB de ROM y 8 KiB de RAM respaldada por batería. La versión jugable está disponible en la página del juego en itch.io.
Orden de lectura recomendado: empieza por el diagrama de flujo y después lee juntas las secciones del movimiento de la cabeza y del seguimiento del cuerpo. La tabla de bibliotecas relaciona cada técnica con el código que puedes consultar. En los identificadores, Apple se refiere a la granada de la pantalla y Block a la mina.
02Compilación y organización de archivos
Con Rust 1.85 o posterior puede compilar el compilador y todas las herramientas para Windows, Linux, macOS ARM y macOS Intel. Los ejecutables nativos no necesitan .NET; las herramientas de recursos tampoco requieren Python ni Pillow.
Los scripts PowerShell usan el ejecutable Windows de la raíz. En Linux/macOS pase las mismas entradas C y opciones al compilador nativo, o use PowerShell 7. Los gráficos CHR y el código C son entradas distintas; font.chr conserva la fuente de los ejemplos.
powershell -NoProfile -ExecutionPolicy Bypass -File .\apps\harapeko_shirohebi\build.ps1
El script une 17 fragmentos de código C en un orden explícito. La música, los recursos exportados y los archivos envoltorio que seleccionan los bancos son entradas independientes del compilador. La compilación enlaza lib/ del repositorio; no necesita un proyecto gráfico privado ni una herramienta de exportación. No añadas los fragmentos por separado a un proyecto C genérico: comparten declaraciones dentro de la unidad de traducción ensamblada.
| Opción / archivo generado | Función |
|---|---|
-KitaqgbRoot | Selecciona el directorio que contiene kitaqgb.exe y lib/. Se puede omitir con la estructura prevista por el repositorio. |
-OutputDirectory | Elige el directorio de salida; por defecto es out/ dentro del directorio del juego. |
-KeepBuildFiles | Conserva el código combinado, las adaptaciones temporales de audio y los diagnósticos del compilador tras una compilación correcta. |
shirohebi.gb | La ROM recién compilada. Si la compilación falla, no utilices una ROM anterior como si fuera el nuevo resultado. |
shirohebi.map | Direcciones de los símbolos para depurar exactamente esta compilación. |
build_manifest.json | Hashes del compilador y de la ROM, tamaño de salida y dirección del vector de interrupción. |
Por qué hay un paso posterior a la compilación
La interrupción de música VBlank debe poder acceder a su código con cualquier banco ROM conmutable seleccionado. El script comprueba que cuatro símbolos relacionados con la interrupción estén en el banco fijo 0, instala un JP en $0040 y vuelve a calcular las dos sumas de comprobación del cartucho. Si el vector no es el esperado, detiene la compilación. Una copia temporal del código de audio desactiva el servicio musical ordinario para que el bucle principal y la interrupción no hagan avanzar la música por duplicado. Otra copia temporal sitúa las funciones auxiliares de audio y utiliza un salto absoluto cuyo destino es alcanzable. No se modifican las bibliotecas del repositorio.
Consulta build.ps1 para conocer el orden completo de las entradas y las opciones del compilador. Las principales son --profile=release --stack-bank=fixed --rst-disable --cgb=cgb --cart=mbc5_ram_battery --romsize=64k --ramsize=8k.
03Flujo y estados del programa
| Transición de estado | Condición y efecto |
|---|---|
| TITLE → READY → PLAY | START inicia una partida. La animación de START termina antes de que comience el movimiento. |
| PLAY ⇄ PAUSE | START pausa o reanuda la partida. Durante la pausa se detienen la física y el seguimiento del cuerpo. |
| PAUSE → RETURN_CONFIRM | SELECT abre la confirmación de vuelta al título, con NO seleccionado por defecto. Cancelar devuelve a la misma partida en pausa; YES vuelve a TITLE. |
| PLAY → MISS | Chocar con una mina o con el propio cuerpo inicia la animación correspondiente. |
| MISS → NAME_ENTRY / GAMEOVER | Si la puntuación entra en la clasificación, se abre la entrada del nombre. En caso contrario, se pregunta directamente si se quiere reintentar. Al confirmar el nombre, se guarda y se abre esa misma pregunta. |
| GAMEOVER → READY / TITLE | YES inicia una partida nueva; NO vuelve al título. |
| TITLE ⇄ ERASE | SELECT+START abre la confirmación de borrado de puntuaciones. A confirma; B o START cancela. La opción inicial es NO. |
La espera de liberación descarta las entradas hasta que se hayan soltado todos los botones, incluida la vuelta en la que se detecta que ya están sueltos. Así, el botón que abrió una pantalla no confirma de inmediato la siguiente acción. El modo de demostración sustituye las entradas de dirección y utiliza el mismo código de movimiento y colisión. La inactividad en el título se cuenta como 900 vueltas del bucle principal; la demostración dura 1.800 VBlank. Ninguno de los dos contadores garantiza una duración exacta en tiempo real.
04Movimiento de la cabeza: punto fijo y giros
Solo la cabeza es un KQBody2D. Su KQWorld2D contiene un cuerpo activo, gravedad nula y cero iteraciones del solucionador de contactos. El juego realiza sus propias comprobaciones de colisión después del movimiento. Las coordenadas Q4 usan 16 unidades enteras por píxel: 160 representa 10 píxeles. Esto conserva el movimiento inferior a un píxel sin operaciones de coma flotante.
base = 8 + 2 * min(score / 5, 10) // Q4 component limit
first UP update: limit = base + (base >> 2)
held UP: limit += max(base >> 2, 1), capped at 2 * base
released UP: limit -= max(base >> 2, 1), floored at base
pixel_center = (q4_position + 8) >> 4
El límite base de cada componente de velocidad va de 0,5 a 1,75 píxeles por actualización. Las 16 orientaciones siguen el sentido horario: derecha (0), abajo (4), izquierda (8), arriba (12). LEFT y RIGHT cambian la orientación deseada en un sector. Si se mantiene el botón, se usa primero una cuenta atrás de 8 actualizaciones y después se repite con una de 4; pulsar LEFT y RIGHT a la vez anula el giro.
Las componentes de la velocidad objetivo toman el valor completo del límite, tres cuartos, un cuarto o cero. Cada componente real se acerca a su objetivo como máximo 3 unidades Q4 por actualización. Son aproximaciones enteras de bajo coste, no vectores trigonométricos normalizados. Por ello, la orientación visible puede cambiar antes de que la velocidad termine de alinearse.
Después de kq2d_step, la cabeza salta al borde opuesto si su centro rebasa los límites: X menor que 4 pasa a 156 y X mayor que 156 pasa a 4; Y utiliza del mismo modo 4 y 140. A continuación se redondea una sola vez a coordenadas de píxel, compartidas por dibujo y colisiones. Esta regla de la cabeza difiere del retorno módulo 160/144 utilizado por el cuerpo.
Consulta shirohebi_snake.c / shirohebi_flow.c.
05Cómo sigue el cuerpo a la cabeza
ChainBody apunta directamente a SND_TraceX, SND_TraceY y SND_TraceDir. Estos arrays contienen la posición y orientación actuales de cada segmento, no un historial del recorrido de la cabeza. Las colisiones y el dibujo leen los mismos arrays. La serpiente empieza con una cabeza y tres segmentos de cuerpo, y puede crecer hasta 31 segmentos en total.
En pantallas estrechas, desplaza el diagrama horizontalmente.
- Guardar la posición en píxeles y la orientación de la cabeza en el índice 0; asignar esa orientación a
pull. - Para el segmento
i, conservar su orientación anterior. Restar un pequeño desplazamiento dependiente de la dirección a la posición ya actualizada del predecesor para obtener el objetivo. - Calcular el desplazamiento X/Y más corto hasta el objetivo teniendo en cuenta el retorno por los bordes. Mover cada eje 0, 1 o 2 píxeles según la tabla siguiente.
- Asignar
pulla la orientación del segmento, ajustar su nueva posición para que retorne dentro del área y pasar la orientación anterior guardada al segmentoi + 1.
| Desplazamiento con signo d | Corrección en cada eje |
|---|---|
| d > 4 | +2 |
| 0 < d ≤ 4 | +1 |
| d = 0 | 0 |
| −4 ≤ d < 0 | −1 |
| d < −4 | −2 |
Los desplazamientos X para las orientaciones de 0 a 15 son [4,4,3,1,0,-1,-3,-4,-4,-4,-3,-1,0,1,3,4]; Y utiliza los mismos valores con el índice desplazado 12 sectores. Las coordenadas retornan por el borde opuesto dentro de un área de 160 × 144. Para un eje de longitud W, se resta W a un desplazamiento mayor que W/2 y se suma W si es menor que −W/2. Un desplazamiento de exactamente medio campo conserva su signo.
Ejemplo de cálculo en el borde de la pantalla
Si el segmento anterior está en (2, 50) y mira a la derecha, el objetivo del siguiente queda cuatro píxeles detrás: X objetivo = (2 − 4) mod 160 = 158. Si el siguiente está en X=156, el desplazamiento es +2, por lo que avanza +1 hasta X=157. Desde X=0, el desplazamiento más corto es −2 y se mueve a X=159. Así sigue al anterior a través del borde, sin cruzar toda la pantalla.
Al crecer, se duplican la posición y la orientación de la cola y se aumenta el número de segmentos activos. Las actualizaciones posteriores separan gradualmente el nuevo segmento. La antigua cola se resalta durante 20 actualizaciones. El archivo envoltorio de la biblioteca mantiene la función de seguimiento y sus auxiliares juntas en el banco 2, evitando un cambio de banco por segmento.
/* Excerpt: the game owns these arrays for the entire run. */
chain_body_init(&SND_ChainBody, SND_TraceX, SND_TraceY, SND_TraceDir,
SND_MAX_SEGMENTS, SND_SCREEN_W, SND_SCREEN_H);
chain_body_reset(&SND_ChainBody, SND_Length,
SND_HeadX, SND_HeadY, SND_Facing);
/* Once per PLAY update, after integrating and rounding the head. */
SND_ChainBody.count = SND_Length;
chain_body_step(&SND_ChainBody, SND_HeadX, SND_HeadY, SND_Facing);
/* On fruit collection, within capacity. */
chain_body_grow(&SND_ChainBody);
SND_Length = SND_ChainBody.count;
Este fragmento muestra la duración de los arrays y el orden de las llamadas; el juego completo también comprueba si el crecimiento ha tenido éxito y activa el resaltado. Fuentes: shirohebi_snake.c / shirohebi_chain.c / chain.c.
06Frutas, colisiones y animaciones de derrota
Una actualización PLAY sigue este orden: aceleración → giro → física de la cabeza → retorno por el borde y redondeo de la cabeza → seguimiento del cuerpo → minas → colisión con el propio cuerpo → fruta → fin del resaltado. Las colisiones mortales tienen prioridad sobre recoger una fruta en la misma actualización.
| Prueba | Regla aplicada |
|---|---|
| Minas | Se comprueba el centro de cada segmento dentro de rectángulos con límites incluidos: X=46…58 o 102…114, Y=62…74. |
| Propio cuerpo | Se compara la cabeza con los segmentos desde el índice 2: las distancias mínimas con retorno por los bordes deben cumplir |dx| < 3 e |dy| < 3. Se excluye el segmento inmediatamente posterior a la cabeza. |
| Fruta | Entre la cabeza y el centro de la fruta, las distancias ordinarias sin retorno por los bordes deben cumplir |dx| < 7 e |dy| < 7. Se suma un punto, se intenta crecer y se elige la posición siguiente. |
Estas comprobaciones comparan umbrales por eje; no calculan una distancia circular. Se examinan 16 posiciones fijas de fruta en un orden determinista. La distancia mínima a una mina, medida en tiles, disminuye con la puntuación: 4, 3, 2 y 1 para 0, 10, 20 y 30 puntos. Siempre se descarta una posición en el mismo tile que una mina. La búsqueda prueba como máximo 16 candidatos. La puntuación es un valor de 8 bits, por lo que no es un contador ilimitado.
Una colisión con el cuerpo congela su postura y la desvanece. Una mina dispersa los segmentos con velocidades de punto fijo, gravedad retardada y explosiones escalonadas, y finalmente muestra un cráter. Al terminar el temporizador de derrota, las puntuaciones que entran en la clasificación pasan a la entrada del nombre; las demás, a la pregunta de reintento. CGB utiliza colores para los efectos y DMG sus paletas de tonos.
07Dibujo: tiles y orden de sprites
La inicialización espera una fase segura de la pantalla, apaga el LCD, limpia la VRAM, carga tiles, caracteres y paletas, y activa la pantalla. La ruta CGB configura atributos del fondo y paletas de color; la ruta DMG usa registros de tonos. El fondo exportado tiene 20 tiles por fila, pero el mapa del hardware usa un paso de 32, de modo que sus índices no son intercambiables.
En cada vuelta del bucle, el juego prepara una copia de OAM en RAM. Cada sprite se escribe con los desplazamientos del hardware X+8 e Y+16. Después de system_wait_vblank(), se transfieren los datos OAM. La serpiente ocupa las posiciones de 0 a 30; el texto utiliza las restantes. Las entradas sin uso se borran.
Los seis primeros segmentos conservan una prioridad OAM fija. El orden de los posteriores rota con una fase que aumenta en cuatro por dibujo. Así se reparte entre los segmentos traseros el efecto del límite de diez sprites por línea de barrido; no se elimina el límite ni se borran segmentos del cuerpo. Al cambiar la longitud máxima, hay que revisar tanto la capacidad de los arrays como la asignación de las 40 posiciones OAM.
Los textos superpuestos START y PAUSE registran por separado el estado deseado, el mostrado y la necesidad de redibujar. Al retirar el texto, cada celda se restaura primero según el estado actual de la fruta y, si hace falta, a partir del mapa de fondo. Rellenar toda la zona con un único tile borraría una fruta situada debajo. Las máscaras de caracteres pasan de ocho filas de un bit por píxel a dieciséis bytes de datos de tiles de dos bits por píxel; los píxeles del carácter tienen prioridad sobre el dibujo del fondo.
shirohebi_render.c / shirohebi_display.c / shirohebi_pause.c
08La música y el papel del VBlank
audio_vblank.c lee el flujo musical del banco fijo. Cada registro normal sigue el orden espera, CH1, CH2, CH3, CH4: pulso con barrido de frecuencia, pulso, onda y ruido. La identidad de los canales sigue, por tanto, el orden físico del hardware. La música se controla con AudioVBlank_PlayMusic, AudioVBlank_Stop y AudioVBlank_SetEnabled.
El controlador ordinario audio.c gestiona los efectos del juego. Su servicio musical se excluye mediante una definición temporal durante la compilación. Los efectos utilizan __bankof para seleccionar el banco de sus datos y respetan prioridades; el efecto de mina también usa directamente el ruido de CH4. La pausa coordina música y efectos. Las llamadas a SND_AudioServiceWork del bucle principal están vacías a propósito: procesar más segmentos no debe hacer avanzar más deprisa el reloj musical.
AudioVBlank_FrameHook llama a SND_DemoVBlankHook, situado en el banco fijo del juego. Este incrementa un contador de demostración de 8 bits y lee el grupo de botones del sistema conservando la selección de P1. Cuando la función correspondiente está activa, retiene SELECT+START en el título y SELECT durante la pausa para no perder pulsaciones breves mientras el bucle principal está ocupado. No dibuja ni llama a funciones de bancos conmutables.
/* Excerpt from initialization, after shared state has been set up. */
SND_AudioInit();
AudioVBlank_FrameHook = SND_DemoVBlankHook;
AudioVBlank_EnableIrq();
09Guardar puntuaciones e introducir el nombre
La ranura 0 guarda 42 bytes de datos de la aplicación: la marca 0x53, la versión 2, diez puntuaciones de un byte y treinta caracteres de nombres. La biblioteca de guardado comprueba la longitud y la suma de comprobación de la envoltura. El lector también reconoce datos de puntuación de 5 y 15 bytes, pero la escritura utiliza el formato de 42 bytes.
Una puntuación no nula que cumple los requisitos se inserta en orden descendente. La comparación es estrictamente mayor, de modo que los empates se sitúan después de las entradas existentes. Los nombres se desplazan junto con sus puntuaciones. Una entrada nueva empieza como AAA en RAM; el juego escribe en SRAM cuando el jugador confirma el nombre de tres caracteres. Abandonar un resultado pendiente vuelve a cargar la clasificación guardada en lugar de escribirlo.
UP y DOWN eligen A–Z o un punto. RIGHT y SELECT pasan al carácter siguiente de forma circular; LEFT retrocede también de forma circular. B solo retrocede si el cursor no está en el primer carácter. A avanza o confirma desde el último carácter; START confirma desde cualquier posición. La espera de liberación evita que esa confirmación seleccione también el reintento.
10Uso de las bibliotecas de KITAQGB
Utiliza juntas la biblioteca y la cabecera del repositorio. Los archivos envoltorio y los ajustes específicos del juego forman parte del ejemplo, especialmente la distribución de bancos y la coordinación del audio.
| Biblioteca | Puntos de entrada y función | Fuente de ejemplo |
|---|---|---|
system.c | system_init, system_wait_vblankInicializa el estado del sistema y sincroniza la presentación. La función de la IRQ de audio se instala por separado. | shirohebi_main.c |
chain.c | chain_body_init, chain_body_reset, chain_body_step, chain_body_growAsocia arrays persistentes, crea la postura inicial, sigue a la cabeza y añade un segmento a la cola. No se utiliza la API Chain de historial circular. | shirohebi_snake.c |
physics2d.c | kq2d_world_init, kq2d_stepIntegra el movimiento de un único cuerpo Q4 para la cabeza. La gravedad y las iteraciones del solucionador de contactos se fijan en cero. | shirohebi_snake.c |
sprite.c | sprite_init; kq_sprite_oam y estado asociadoInicializa y utiliza la copia de OAM en RAM. El juego escribe las entradas directamente y luego las transfiere; hay que respetar las reglas de posiciones y coordenadas. | shirohebi_render.c |
audio_vblank.c | AudioVBlank_Init, PlayMusic, Stop, SetEnabled, EnableIrq, FrameHookReproduce el flujo musical residente y llama al callback del juego durante la interrupción. El código y los datos IRQ deben estar en ROM fija. Los nombres abreviados después del primero también llevan el prefijo AudioVBlank_. | shirohebi_audio.c |
audio.c | Audio_Init, Audio_Update, Audio_PlaySFXBanked, Audio_StopSfx, Audio_SetPausedReproducción, prioridad y pausa de efectos, coordinadas con el controlador musical VBlank. | shirohebi_audio.c |
save.c | save_exists, save_read, save_writeLee y escribe la clasificación con información de versión en la ranura 0. save_read indica éxito o fallo después de comprobar la longitud exacta solicitada y la suma de comprobación; el juego también valida su propia marca. | shirohebi_save.c |
cgb_palette.c | cgb_bg_palette, cgb_obj_paletteConfigura los colores CGB; los registros de paleta DMG se establecen explícitamente por separado. | shirohebi_display.c |
map.c | map_load y descriptor map_t exportadoDa soporte a la función auxiliar de mapa del archivo de recursos; el juego restaura por sí mismo las celdas individuales. | shirohebi_assets.c |
fixed.c | soporte de punto fijoCódigo de apoyo enlazado; el movimiento de la cabeza se expresa mediante physics2d y aritmética entera Q4. | shirohebi_common.c |
audio_hwregs_gb.c | definiciones compartidas de registros de audioProporciona las definiciones de registros de Game Boy que necesitan ambos tratamientos de audio. | shirohebi_audio.c |
Funciones intrínsecas del compilador utilizadas con las bibliotecas
__readpadex(previous) devuelve los botones mantenidos en el byte bajo y los recién pulsados en el alto. __cgb_is_cgb() permite elegir la ruta de color. Las funciones intrínsecas de tiles y atributos, como __settile y __settileattr, actualizan celdas del fondo; __bankof proporciona el banco ROM de los datos de efectos. Son funciones del compilador, no implementaciones C adicionales que haya que copiar al juego.
11Guía del código y precauciones al modificarlo
| Archivo | Responsabilidad |
|---|---|
shirohebi_common.c | Tipos, constantes, estado compartido y declaraciones anticipadas. Empieza por aquí al seguir un símbolo. |
shirohebi_main.c | Secuencia de arranque y bucle entrada → actualización → dibujo → VBlank. |
shirohebi_flow.c | Selección del procesamiento por estado: opciones del título, READY, PLAY, pausa, derrota, entrada del nombre y reintento. |
shirohebi_input.c | Espera de liberación, atajo del título y confirmación de vuelta al título. |
shirohebi_snake.c | Física de la cabeza, aceleración, giro relativo, paso por los bordes e inicialización, actualización y crecimiento del cuerpo. |
shirohebi_chain.c | Archivo envoltorio del banco 2 que incluye toda la implementación compartida de chain. |
shirohebi_collision.c | Colisiones con minas y el propio cuerpo, clasificación del impacto y animación de segmentos dispersos. |
shirohebi_apples.c | Selección de la posición de la granada, recogida, puntuación y crecimiento. Apple es el identificador interno. |
shirohebi_demo.c | Dirección de la demostración, postura de la serpiente larga y cómputo de los ticks transcurridos con el contador VBlank. |
shirohebi_render.c | Preparación de OAM en RAM, tiles de cabeza, cuerpo y cola, rotación de prioridades y efectos de derrota. |
shirohebi_display.c | Inicialización de LCD/VRAM, carga de paletas y caracteres, disposición del título y la clasificación. |
shirohebi_background.c | Celdas del fondo de juego, minas y restauración de celdas sobrescritas. |
shirohebi_pause.c | Textos superpuestos START/PAUSE con estado solicitado, mostrado y pendiente de redibujado. |
shirohebi_text.c | Conversión de caracteres a tiles, cadenas, puntuaciones numéricas y cursores de menús. |
shirohebi_save.c | Validación e inserción en la clasificación, nombres de tres caracteres y almacenamiento en SRAM. |
shirohebi_audio.c | Configuración de música y efectos, inicio y parada, opciones, pausa y coordinación de canales. |
music.c | Registros musicales VBlank residentes de longitud fija para la melodía de inicio, el tema principal y el de fin de partida. |
sfx.c | Flujos de bytes de efectos seleccionados por las funciones de audio del juego. |
text_tiles.c | Máscaras de caracteres y conversión a filas de tiles de Game Boy de dos bits por píxel. |
shirohebi_assets.c | Tiles, mapa, atributos y tablas de colisión/eventos exportados; descriptor del mapa y funciones auxiliares. |
shirohebi_input_shared.h | Declaraciones de los indicadores de entrada en WRAM y del callback en banco fijo compartidos con la interrupción. |
shirohebi_lib_bank0.c | Callback VBlank de entrada y reloj en banco fijo, indicadores WRAM compartidos y selección del banco 0 para las siguientes entradas de compilación de sistema y audio. |
shirohebi_lib_bank1.c | Selección del banco 1 para las siguientes entradas de compilación de bibliotecas de apoyo. |
Qué revisar al modificar el juego
- Velocidad y giros: tras cambiar las constantes Q4, comprueba los giros lentos y la aceleración con UP. El sector hacia el que mira la cabeza no es la dirección exacta de su velocidad.
- Longitud: actualiza conjuntamente la capacidad de los arrays, la de la cadena y la reserva de posiciones OAM. Comprueba las colisiones de la cola con minas o con la cabeza, y el límite de sprites por línea.
- Gráficos: conserva los números de tile, el paso entre filas del mapa y los atributos CGB. Al retirar textos superpuestos, restaura el contenido actual de la celda inferior.
- Audio: mantén el código de interrupción en el banco fijo y una única fuente de avance del reloj musical. Prueba la pausa y la reanudación, además de la reproducción normal.
- Guardado: cambia la versión del formato cuando modifiques su estructura; valida la longitud antes de acceder mediante índices y prueba SRAM nueva e inválida.
Después de recompilar, utiliza el archivo de direcciones generado en esa compilación. Comprueba el título, la introducción START, el movimiento, los giros, las frutas, las colisiones con el cuerpo y las minas, la pausa y la reanudación, la confirmación de vuelta y la clasificación en ambos modos de hardware. Una compilación correcta no demuestra por sí sola que funcionen esos recorridos ni que el juego se comporte correctamente en una consola real.