Portando Flapper a Boriel BASIC -- una breve retrospectiva.

2026-05-02 por juntelart

Portar un juego de Sinclair BASIC a Boriel BASIC es una lección en traducir restricciones: las ideas de juego son pequeñas y claras, pero el timing, el mapeado de memoria y unos pocos primitivos auxiliares cambian cómo se implementan. Me centré en tres objetivos:

  • preservar la sensación original (timing, ritmo de las tuberías)
  • mantener el código modular y legible para quien quiera aprender
  • aplicar cambios mínimos en tiempo de ejecución cuando Boriel se comportaba diferente

A continuación explico las decisiones principales y muestro pequeños fragmentos de código para que puedas seguir o reutilizar las técnicas.

Flapper gameplay

¿Por qué esta estructura?

Separar entrada, física, render y colisión hizo que portar fuese sobre todo afinar constantes y pequeños cambios específicos de la plataforma (no reescribir la jugabilidad). Esa separación también facilita explicar el código: cada concepto está en una función pequeña.

Latido por frame (en palabras)

El juego ejecuta un bucle por frame simple y predecible: leer entrada, actualizar física, desplazar el mundo, dibujar el pájaro, y luego comprobar colisiones y puntuación. Ese orden mantiene el comportamiento determinista —muy útil para ajustar el ritmo al portar.

En la práctica, el bucle es corto y claro y está compuesto por helpers pequeños: screenSync(), readKeyboard(), gravity(), scroll(), redrawBird(), checkScore(), checkBirdCollision().

Módulos clave y cambios

  • definitions.bas — perillas de ajuste: gravedad, geometría de tuberías y atributos de pantalla.
  • physics.bas — movimiento en punto fijo; matemáticas sencillas y fáciles de tunear.
  • draw.bas — separé los bytes de atributos (color) de los datos de píxeles; eso simplificó la colisión y borrar/dibujar el pájaro.
  • collision.bas — colisión basada en atributos: rápida y conceptualmente simple.

Rara vez fue necesario reescribir lógica algorítmica; la mayoría del trabajo fue ajustar timings y asegurar que las escrituras al buffer de atributos coincidieran con el mapeo de Boriel.

Puntuación y ritmo de las tuberías

Las tuberías se generan con dos ranuras entrelazadas usando un único contador worldCol. Esta única fuente de timing mantiene sincronizado dibujo, puntuación y el patrón del suelo. El resultado es un ritmo predecible que encajó bien tras un par de pasadas de ajuste.

Flapper gameplay

Render y colisión — la elección práctica

Elegí la colisión basada en atributos para evitar máscaras por píxel en el pájaro. El pájaro escribe solo píxeles, dejando los atributos intactos; la colisión es simplemente "¿la caja de atributos 2×2 en la posición del pájaro contiene algo distinto de ATTR_SKY?".

Este diseño es barato, robusto y fácil de explicar a quien lea el código por primera vez.

Flapper gameplay

Nota de rendimiento

El timing en Boriel requirió un ajuste pragmático: la build Boriel en este repo se ralentiza por un factor 4 para que la velocidad de juego coincida con la original. Ese cambio es únicamente de tiempo de ejecución para preservar la experiencia.

Conclusiones — lecciones para quien porte juegos

  • Mantén la lógica de juego separada del rendering y el 'glue' de plataforma.
  • Usa colisión por atributos en sistemas con capas de carácter/atributo; suele ser más simple y rápido.
  • Ajusta constantes al final — la mayoría de bugs al portar son de timing, no algorítmicos.

Ejemplos de código (Español)

Abajo hay ejemplos pequeños extraídos del código para ilustrar tareas comunes: mostrar la pantalla de juego, detección de colisión y desplazamiento del mundo.

Mostrar la pantalla (bucle de juego)

Sub showPlayGameScreen(clearScreen As Ubyte)
  initGame(clearScreen)
  Do
    screenSync()
    readKeyboard()
    preserveYPosition()
    gravity()
    scroll()
    redrawBird()
    checkScore()

    If checkBirdCollision(birdX, Int(birdYPos)) Then
      showGameOverScreen()
    End If
  Loop
End Sub

Esta secuencia es la actualización por frame: entrada → física → desplazamiento del mundo → render → colisión/puntuación.

Detección de colisión (por atributos)

Function checkBirdCollision(bx As Ubyte, by As Ubyte) As Ubyte
  Dim attrBuf(3) As Ubyte
  getPaintData(bx, by, 2, 2, @attrBuf(0))
  If attrBuf(0) <> ATTR_SKY Then Return 1
  If attrBuf(1) <> ATTR_SKY Then Return 1
  If attrBuf(2) <> ATTR_SKY Then Return 1
  If attrBuf(3) <> ATTR_SKY Then Return 1
  Return 0
End Function

La colisión se calcula leyendo los 2×2 bytes de atributos en la posición del pájaro y comprobando si hay algo distinto de ATTR_SKY.

Desplazamiento del mundo (atributos + última columna)

Detalle de implementación: al desplazar el playfield movemos los bytes de atributos fila a fila (usando MemMove por fila) en lugar de mover toda la memoria de atributos de golpe. Hacerlo por fila evita que aparezca "basura" temporal en la columna más a la derecha por solapamiento de regiones en la memoria.

Sub scrollPlayfieldAttrs()
  Dim row As Ubyte
  Dim src As UInteger = $5821
  Dim dst As UInteger = $5820
  For row = 0 To 23
    MemMove(src, dst, 31)
    src = src + 32
    dst = dst + 32
  Next row
End Sub

Sub paintLastColumn()
  Dim wc As Ubyte = worldCol Mod PIPE_PERIOD
  Dim attribute As Ubyte = ATTR_PIPE
  Dim pipeLastCol As Ubyte = PIPE_WIDTH - 1

  If wc < PIPE_WIDTH Then
    If wc = pipeLastCol Then attribute = ATTR_PIPE_SHADOW
    writePipeColumn(31, pipeGap(0), attribute)
    Return
  End If

  If wc >= PIPE_SPAWN_INTERVAL Then
    If wc < PIPE_SPAWN_INTERVAL + PIPE_WIDTH Then
      If wc - PIPE_SPAWN_INTERVAL = pipeLastCol Then attribute = ATTR_PIPE_SHADOW
      writePipeColumn(31, pipeGap(1), attribute)
      Return
    End If
  End If

  writeSkyColumn(31)
End Sub

Sub scroll()
  scrollPlayfieldAttrs()
  paintLastColumn()
  worldCol = worldCol + 1
End Sub

El código desplaza el buffer de atributos a la izquierda, pinta la nueva columna derecha (tubería o cielo) según worldCol y aumenta el contador global.

Dibujar / borrar el pájaro (solo píxeles)

Sub eraseBird(bx As Ubyte, by As Ubyte)
  putChars(bx, by, 2, 2, @blankSprite(0))
End Sub

Sub drawBird()
  putChars(birdX, Int(birdYPos), 2, 2, @sprite0(0))
End Sub

Sub redrawBird()
  waitretrace
  eraseBird(birdX, birdOldY)
  drawBird()
End Sub

Nota: dibujar solo escribe bytes de píxeles; los atributos se preservan para una colisión correcta. Se usa waitretrace antes de redrawBird() para evitar parpadeo al borrar y dibujar el sprite.

Más información

Para más información: github.com/rtorralba/flapper-boriel

Download the game


Volver a publicaciones