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.

Comentarios

Nadie ha comentado nada por ahora.

Deja un comentario