Cómo mover un personaje en canvas

May 14, 2026 14:13 — desarrollo de videojuegos, javascript

Después de implementar un mapa de losetas básico en canvas, voy a añadir un personaje y moverlo con las flechas del teclado. Puedes ver el resultado final en Codepen.

Lo primero que hago es añadir un nuevo tipo de loseta para representar al personaje. Para que se diferencie bien de las demás, elijo el color rojo.

const TILES = {
  ...
  4: "red"
}

Ahora necesito una forma de saber dónde está ubicado el personaje. Para no complicarme mucho decido ir con una variable state en la que persistir este tipo de información.

const state = {
  player: {
    col: 0,
    row: 0,
  }
}

Con esto estamos preparados para mostrar el personaje en pantalla. Mientras escribo esto me doy cuenta de que no tiene mucho sentido pasar map como argumento y luego leer del state global. No me preocupo por ahora. Estoy observando a ver que patrones emergen.

function renderCharacter(map) {
  drawTile(state.player.col, state.player.row, 4)
}

Para mostrar el personaje en pantalla, llamamos a la función renderCharacter(MAP) justo después de renderMap(MAP). Con este código deberíamos ver una loseta roja arriba a la izquierda. Prueba a cambiar los valores de col y row para ver como la loseta aparece en otras partes del mapa.

De momento esto tiene poco de videojuego. Para solucionarlo tenemos que permitir al jugador controlar al personaje. Como de momento solo queremos movernos, vamos a escribir algo de código para que nuestro personaje reaccione a las flechas del teclado.

document.addEventListener("keydown", event => {
  if (event.repeat) return

  const player = state.player

  switch(event.key) {
    case "ArrowUp":
      console.log("ArrowUp")
      player.row--
      break
    case "ArrowDown":
      console.log("ArrowDown")
      player.row++
      break
    case "ArrowLeft":
      console.log("ArrowLeft")
      player.col--
      break
    case "ArrowRight":
      console.log("ArrowRight")
      player.col++
      break
  }
})

Con event.repeat evitamos que nuestro personaje reciba la acción repetida si el jugador deja la tecla apretada. De momento nos vamos a mover una loseta a la vez. Más adelante mejoraremos el sistema de movimiento.

Si pruebas ahora, verás que no está pasando absolutamente nada en la pantalla. Aprietas las teclas, pero la loseta roja sigue en su sitio. Tendrás que abrir la consola para comprobar que, efectivamente, nuestro código está reaccionando al pulsar las teclas. Lo que pasa es que una vez mostrado el contenido en pantalla, no tenemos forma de decir que se actualice con la nueva información. Para eso necesitamos un game loop. Hay mucha información en la MDN.

function loop() {
  renderMap(MAP)
  renderCharacter(MAP)

  requestAnimationFrame(loop)
}

loop()

La función loop va a ser ahora la encargada de llamar a renderMap y renderCharacter y lo va a hacer a velocidad diferente creo que dependiendo de los FPS a los que esté configurado el navegador. Más adelante arreglaremos esto para que corra igual en todos los dispositivos.

Si aprietas las flechas del teclado, la loseta roja se debería mover por el mapa. Hay que levantar y volver a apretar la tecla para volver a moverse. Parece que todo funciona, pero se puede mejorar.

En el estado actual del juego, el personaje puede moverse más allá de los límites del mapa. Para eso necesitamos evaluar si las coordenadas a las que se quiere mover la loseta están dentro de los límites o no. En caso de no estarlo, sobreescribimos la posición con la actual. Es decir, impedimos el movimiento.

Para solucionar esto voy a implementar otro paso en el game loop: updateCharacter(). Ahora tendremos una zona dedicada a actualizar los diferentes elementos del juego, y después procederemos a dibujarlos en el canvas.

Y, en vez de mover el personaje directamente, las flechas solicitarán el movimiento. Una función clampCharacterToMap determinará si el movimiento es válido o no, y decidirá la posición final a la que solicita ir el personaje.

Añade targetCol y targetRow a state.player.

const state = {
  player: {
    col: 0,
    row: 0,

    targetCol: 0,
    targetRow: 0
  }
}

Actualiza el eventListener para que en vez de modificar la posición del personaje, modifique la posición solicitada. Al final, llama a clampPlayerToMap, que es la función que va a validar que el personaje no se salga del mapa.

document.addEventListener("keydown", event => {
  if (event.repeat) return

  const player = state.player

  switch(event.key) {
    case "ArrowUp":
      player.targetRow = player.row - 1
      break
    case "ArrowDown":
      player.targetRow = player.row + 1
      break
    case "ArrowLeft":
      player.targetCol = player.col - 1
      break
    case "ArrowRight":
      player.targetCol = player.col + 1
      break
  }

  clampPlayerToMap()
})

Para evitar que el personaje se salga del mapa, lo que hacemos es validar la posición solicitada. Para columnas y filas, validamos que la posición solicitada esté entre la primera y la última, sino corregimos y solicitamos la primera o la última.

function clampPlayerToMap() {
  const player = state.player

  player.targetCol = Math.max(0, Math.min(player.targetCol, MAP.cols -1))
  player.targetRow = Math.max(0, Math.min(player.targetRow, MAP.rows - 1))
}

Ahora que sabemos que los valores son los adecuados, tenemos que hacerlos efectivos. Simplemente copiamos el valor solicitado al valor actual.

function updateCharacter() {
  const player = state.player

  player.col = player.targetCol
  player.row = player.targetRow
}

El último paso es llamar a updateCharacter() al principio del todo del game loop. Con esto, la loseta del personaje se puede mover libremente por todo el mapa, pero no salir de él.

Si pasas por encima de la loseta número 5, verás que al entrar es transparente, pero al salir se queda roja. Esto es porque al inicializar el mapa, no pintamos nada ahí. Cuando el personaje entra, pintamos la loseta del personaje ahí, pero cuando sale, al no haber loseta, no pintamos nada encima. La solución es modificar drawTile para que en vez de no pintar, borre lo que haya ahí.

He aprovechado para crear una constante DEBUG que sea la que decida si se pintan los números en las losetas o no.

function drawTile(col, row, tileNumber) {
  if (tileNumber in TILES) {
    ctx.fillStyle = TILES[tileNumber]

    ctx.fillRect(
      col * MAP.tileSize,
      row * MAP.tileSize,
      MAP.tileSize,
      MAP.tileSize
    )
  } else {
    ctx.clearRect(
      col * MAP.tileSize,
      row * MAP.tileSize,
      MAP.tileSize,
      MAP.tileSize
    )
  }

  if (DEBUG) {
    ctx.fillStyle = "black"
    ctx.font = "16px monospace"
    ctx.textAlign = "center"
    ctx.textBaseline = "middle"

    ctx.fillText(
      tileNumber,
      col * MAP.tileSize + MAP.tileSize / 2,
      row * MAP.tileSize + MAP.tileSize / 2
    )
  }
}

const DEBUG = true

Y con esto ya tendríamos un personaje moviéndose por el mapa. Hay mejoras que aplicar, como movimiento suave (que el personaje se deslice entre losetas, no que se teletransporte) y movimiento continuo (que puedas dejar apretadas las teclas y se siga moviendo). También podemos añadir velocidad, por si atravesamos un terreno pantanoso o nos calzamos unos patines.

Para el siguiente artículo tengo tres opciones:

  • Mejorar el movimiento del personaje como he comentado anteriormente.
  • Implementar una cámara, para que el personaje siempre esté en el centro del mapa y podamos tener un mapa enorme, pero solo la zona en la que esté el personaje sea visible.
  • Añadir un teclado en la pantalla para que se pueda jugar desde pantallas táctiles.

Déjame un comentario si tienes alguna preferencia o sugerencia.

Cómo dibujar un mapa de losetas con canvas

May 13, 2026 08:11 — desarrollo de videojuegos, javascript

Llevo tiempo tonteando con la idea de hacer un videojuego. No porque tenga una gran idea, sino porque quiero aprender cómo se hacen. Como estoy interesado en aprender desde "cero", no quiero usar un game engine como Unity o Godot. He probado Dragon Ruby y es genial poder programar videojuegos en ruby, mi lenguaje preferido. Al final me he decidido por javascript por una sencilla razón: siempre que tenga acceso a un navegador puedo probar conceptos desde la consola o páginas como Codepen.

En la MDN hay toda una guía de desarrollo de videojuegos en javascript. Esa guía y ChatGPT gratis han sido mis recursos por ahora.

Tengo la intención de documentar lo que voy aprendiendo con ejemplos lo más sencillos posibles, para que me sea fácil reaprender en el futuro. Retomo el aprendizaje cada ciertos meses y aunque los conceptos se quedan, reimplementarlos me cuesta un montón y acabo usando ayuda de tutoriales e IA.

Voy a empezar sin organizar el código. He leído sobre algunos patrones de diseño comunes en el desarrollo de videojuegos, pero no me quiero adelantar. Quiero ir desarrollando en base a mis necesidades.

Puedes explorar el código y ver el resultado en Codepen. A continuación está la explicación paso a paso.

Lo primero, necesitamos un canvas.

<canvas id="canvas"></canvas>

Accedemos al canvas, y obtenemos su contexto. No voy a pararme a explicar cómo funciona un canvas en este post. Puedes revisar la guía de canvas en MDN.

const canvas = document.getElementById("canvas")
const ctx = canvas.getContext("2d")
ctx.imageSmoothingEnabled = false

De momento no me quiero liar con imágenes y sprites, así que para los primeros posts usaré cuadrados de colores.

const TILES = {
  1: "brown",
  2: "green",
  3: "blue"
}

Siguiendo el tutorial de MDN, pongo toda la información del mapa en una constante. tileSize es el temaño de cada loseta. Como en este caso son cuadrados de color, no importa mucho y se puede cambiar este valor por cualquiera. Cuando se usen imágenes, tendremos que ajustar este valor.

const MAP = {
  tileSize: 32,
  cols: 6,
  rows: 6,
  tiles: [
    2, 2, 2, 3, 2, 2,
    1, 1, 1, 3, 1, 2,
    1, 2, 1, 3, 1, 2,
    1, 1, 1, 1, 1, 2,
    5, 1, 1, 3, 1, 2,
    3, 3, 3, 3, 3, 3,
  ]
}

Una vez tenemos el mapa, podemos determinar el temaño del canvas:

canvas.width = MAP.cols * MAP.tileSize
canvas.height = MAP.rows * MAP.tileSize

Para dibujar una loseta en el mapa, he creado una función:

function drawTile(col, row, tileNumber) {
  if (tileNumber in TILES) {
    ctx.fillStyle = TILES[tileNumber]

    ctx.fillRect(
      col * MAP.tileSize,
      row * MAP.tileSize,
      MAP.tileSize,
      MAP.tileSize
    )
  }
}

Para poder debuggear más fácilmente, puedes mostrar el número de loseta añadiendo después de ctx.fillRect. Los ajustes en el contexto son globales, por lo que hay que sobreescribirlos cada vez.

ctx.fillStyle = "black"
ctx.font = "16px monospace"
ctx.textAlign = "center"
ctx.textBaseline = "middle"

ctx.fillText(
  tileNumber,
  col * MAP.tileSize + MAP.tileSize / 2,
  row * MAP.tileSize + MAP.tileSize / 2
)

Ya con el mapa definido y la función para dibujar las losetas, lo único que falta es iterar sobre cada loseta del mapa y dibujarla. Esta es una versión bastante explicita en la que creo que es bastante fácil seguir qué es lo que está pasando.

function renderMap(map) {
  let currentCol = 0
  let currentRow = 0

  map.tiles.forEach(tile => {
    drawTile(currentCol, currentRow, tile)

    if (currentCol == map.cols - 1) {
      currentCol = 0
      currentRow++
    } else {
      currentCol++
    }
  })
}

Otra versión con menos código sería:

function renderMap(map) {
  map.tiles.forEach((tile, index) => {
    const col = index % map.cols
    const row = Math.floor(index / map.cols)

    drawTile(col, row, tile)
  })
}

Hay muchas maneras diferentes de iterar sobre las losetas del mapa para dibujarlas. En el tutorial de MDN lo hacen con dos bucles for anidados, uno para las columnas y otro para las filas.

Imagino que el método elegido afectará en el futuro cuando tenga que dibujar varias capas o interactuar con ellas. De momento me parece más intuitivo un único bucle.

Por último solo queda llamar a la función para pintar el mapa.

renderMap(MAP)

Espero que con esto de copiar y pegar no se me haya pasado nada. Si tienes alguna duda o ves que algo no funciona como esperas, déjame un comentario y lo miramos.

En las siguientes iteraciones exploraré cómo añadir un cuadrado que represente al personaje jugador y se pueda mover por el mapa, así como una cámara, para que el personaje esté siempre en el centro de la pantalla y solo se vea la zona de mapa a su alrededor.

Migrar ActiveStorage de Disk a otro servicio

January 19, 2026 18:28 — active storage, ruby on rails

Cuando empiezo una aplicación nueva de Rails intento configurar lo menos posible, especialmente cuando se trata de servicios externos. Cuando ya se hace insostenible es cuando me decido a dar el paso. En este caso tengo una aplicación corriendo en una Raspberry, a la que accede mi familia desde diferentes partes del mundo. Es una pequeña red social privada. Ha llegado el momento en el que he sentido que lo mejor es mover las imágenes y vídeos a un proveedor externo, para mejorar la latencia y el cach

Lo primero de todo es configurar un servicio y usarlo como mirror para ver que todo funciona correctamente.

Tras configurarlo, la mejor manera de comprobar que funciona es subir algún fichero desde la aplicación e ir al servicio configurado y ver que efectivamente se han creado ficheros en él.

El servicio de mirroring solo funciona desde el momento en que se configura, no tiene carácter retroactivo. Tendremos que, por lo tanto, migrar todos los archivos antiguos a mano.

La opción más efectiva que he encontrado consiste en lo siguiente:

# Fetch services
local_service      = ActiveStorage::Blob.services.fetch(:local)
cloudflare_service = ActiveStorage::Blob.services.fetch(:cloudflare)

# Loop through all blobs, skipping already migrated ones
ActiveStorage::Blob.find_each(batch_size: 50) do |blob|
  next if blob.service_name == "cloudflare"

  begin
    # Download from local disk and upload to Cloudflare
    local_service.download(blob.key) do |file|
      cloudflare_service.upload(blob.key, file, checksum: blob.checksum)
    end

    # Update blob to point to Cloudflare
    blob.update!(service_name: "cloudflare")

    puts "Migrated blob #{blob.id}"
  rescue => e
    puts "Failed to migrate blob #{blob.id}: #{e.message}"
  end
end

Aunque intento llegar a soluciones por mi mismo, en este caso me estaba complicando la vida y he acabado tirando de una IA gratuita para que me revise el código. Resulta que ActiveStorage::Blob tiene métodos download y upload que vienen finísimos para esta tarea.

Lo ideal es poner esto en una tarea de rake que encole jobs y bla bla bla, pero aquí hemos venido a jugar. Ha tardado un buen rato y al final todos los ficheros han acabado duplicados.

El siguiente paso es cambiar el mirror de tal forma que el servicio primario sea cloudflare (o el que tú quieras) y el secundario sea local. Y en cuanto hayas comprobado que todo funciona como esperas, puedes cargarte el mirror y funcionar solo con el nuevo servicio.

Lo que me falta por hacer ahora es borrar los ficheros del disco, pero no tengo prisa y no quiero cargarme nada, así que creo que se quedarán ahí hasta que sea inevitable mover ficha.

Si lo que deseas o necesitas es migrar entre servicios que no sean locales, el proceso es más fácil todavía, ya que puedes usar el método .mirror_later en cada blob. Puedes leer más sobre este método en el blog de Oliver Eidel.

Instalar Ruby con una versión de openssl específica

December 10, 2025 23:34 — openssl, ruby

Hace poco descubrí Sendamatic, un servicio para enviar emails transaccionales con un precio decente, y decidí usarlo en varios de mis proyectos.

Configuré Rails para usar Action Mailer. Todo estaba bien configurado, porque en producción funcionaba, pero en local me daba un error de SSL cada vez que intentaba enviar un email.

Encontré la solución en Github. El problema es una incompatibilidad de mis versiones locales de openssl y ruby. La solución es decirle al instalador de ruby qué openssl quieres usar.

Como uso rbenv para gestionar mis versiones de ruby y brew para instalar programas, he de instalar nuevas versiones de ruby con el siguiente comando:

RUBY_CONFIGURE_OPTS="--with-openssl-dir=$(brew --prefix [email protected])" rbenv install 3.4.7

En el trabajo uso mise y el comando necesario es el siguiente:

MISE_RUBY_BUILD_OPTS="--with-openssl-dir=$(brew --prefix [email protected])" mise install

En mi caso tengo openssl 3.6 en local y parece que no es compatible con ruby 3.4 y posteriores, así que uso la 3.5 que no me ha dado problemas.

Para comprobar qué versión de openssl usa tu versión actual de ruby hay unos cuantos comandos que son útiles.

ruby -ropenssl -e 'puts OpenSSL::OPENSSL_VERSION'
ruby -ropenssl -e 'puts OpenSSL::OPENSSL_LIBRARY_VERSION rescue puts "N/A"'
ruby -rrbconfig -e 'puts RbConfig::CONFIG["configure_args"]'

Este último comando escupe un montón de cosas. Busca por algo como --with-openssl-dir=/opt/homebrew/opt/[email protected].

Me ha pasado dos veces, y a la tercera he decidido dejarlo por escrito. Me volverá a pasar.

Un método simple para prevenir el spam

October 30, 2025 17:45 — 2 comentarios — blog

Hace unos días leí una teoría del por qué es más fácil que te entre spam teniendo formularios en la web que publicando tu email.

De momento lo que he experimentado es que desde que activé los comentarios, un montón de bots han estado validando mi código. Efectivamente, se pueden dejar comentarios. Me hubiese gustado que los comentarios viniesen de humanos, pero qué se le va a hacer.

Se me ocurrió una manera muy sencilla de filtrar comentarios de spam. He asumido que los comentarios spam vienen de scripts que detectan formularios y los rellenan automáticamente. La solución ha consistido en añadir un campo oculto con CSS que, de ser rellenado, ignoro el comentario y no lo persisto en la base de datos.

La solución es muy chorra y solo sirve para spam muy genérico, pero ¿quién va a dirigir una campaña personalizada hacia un mindundi como yo?