Desarrollando un videojuego sencillo (Parte II – Bucle principal y mapa del nivel)

Pequeño cambio de planes

Aunque en la primera parte había comentado que iba a usar Python+PyGame para escribir el prototipo, he cambiado de idea y voy a emplear HTML5 con JavaScript. Hay varias razones que me han animado a llevar a cabo este cambio:

  • Aparte de anotame.es, no he trabajado mucho con JavaScript, y me apetece reforzar mi experiencia con este lenguaje.
  • JavaScript es también un lenguaje de alto nivel y, aunque no me entusiasma depender del navegador, debo reconocer los ciclos de coding<->debugging<->coding son bastante ágiles.
  • Me permite publicar los ejemplos funcionando en este mismo Blog.
  • Facilita la integración de funciones “sociales” que tan de moda están hoy en día, y con las que puede ser interesante experimentar más adelante.

El bucle principal

Habitualmente, el corazón del código de un videojuego está compuesto por un bucle principal que, al menos conceptualmente, suele ser similar a éste:

  1. Procesar los eventos de entrada.
  2. Actualizar la representación lógica del mundo.
  3. Pintar el mundo en la pantalla (representación visual del mundo).

Para este proyecto, voy a partir del código de del proyecto mortar-game-stub de Mozilla, que nos proporciona un canvas de HTML5 y el esqueleto de la aplicación JavaScript. La función principal se encuentra en el fichero www/js/app.js, donde podemos encontrar además una implementación básica del mencionado bucle (he añadido más comentarios para explicar su funcionamiento):

    // Bucle principal
    function main() {
        // Si nuestra pestaña en el navegador no es visible,
        // omitimos las actualizaciones.
        if(!running) {
            return;
        }

        // Tomamos la referencia del tiempo transcurrido desde
        // la última iteración.
        var now = Date.now();
        var dt = (now - then) / 1000.0;

        // Procesamos los eventos y actualizamos el mundo.
        update(dt);

        // Representamos el mundo en el canvas de HTML5.
        render();

        // Actualizamos la marca de tiempo.
        then = now;

        // Solicitamos al navegador que nos vuelva a llamar
        // cuando considere oportuno.
        requestAnimFrame(main);
    };

 

Pintando el mapa del nivel

Ahora que ya está localizado el bucle principal, hay que empezar a darle forma al juego. A la hora de  diseñar los componentes, se debe tener en cuenta que se tiene que trabajar tanto en su representación visual (cómo se va a ver el componente en la pantalla) como en su representación lógica (las estructuras de datos que permiten definir y manipular el componente en el código).

Voy a empezar por implementar el mapa del nivel, es decir, el mundo por el que se va a mover el personaje del jugador en cada pantalla del juego. Utilizando el Donkey Kong original como inspiración, quiero preparar algo sencillo, con plataformas a varias alturas y escaleras conectándolas. Mirando en el spritesheet del Platformer Art Deluxe, veo cuatro tiles que me pueden venir bien para empezar:

grassMid

 

 

grassRight

 

 

grassLeft

 

 

ladder_mid

 

 

El primero representa la parte central de la plataforma, mientras que el segundo y el tercero se corresponden a los bordes derecho e izquierdo, respectivamente. El cuatro tile será la pieza base para construir las escaleras que conectan las plataformas.

Para poder hacer cambios de forma rápida, voy a hacer que cada nivel se represente en una rejilla de 15×12 celdas, y a definir el contenido de las mismas con números enteros. Como cada tile tiene un tamaño de 70×70 píxeles, la aplicación tendrá un resolución de 1050×840 (más adelante ya la ajustaremos convenientemente).

Dicha rejilla se puede implementar con un doble array (me niego a utilizar la traducción “arreglo”). Un nivel definido de esta forma, quedaría así:

var map = [
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,1,1,1,1,2,0,0,3,1,1,1,1,1,1],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[1,1,1,1,1,1,1,1,1,1,1,1,1,1,1]]

En este caso, los “0” representan espacios vacíos, los “1” se corresponden con el tile del bloque central, el “2” con el derecho y el “3” con el izquierdo.

Como se puede observar, este mapa no tiene representada ninguna escalera. La razón es que la parte superior de la escalera debe superponerse al bloque de la plataforma, lo que obligaría a incrementar el número de dígitos empleados para representar las combinaciones posibles. De momento, y con el objetivo de simplificar, he creado un mapa independiente para las escaleras:

var map_ladders = [
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,4,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,4,0,0,0,0,0,0,0,0,0,0],
[0,0,0,0,4,0,0,0,0,0,0,0,0,0,0],
[0,0,4,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,4,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,4,0,0,0,0,0,0,0,0,0,0,0,0],
[0,0,0,4,0,0,0,0,0,0,0,4,0,0,0],
[0,0,0,4,0,0,0,0,0,0,0,4,0,0,0],
[0,0,0,4,0,0,0,0,0,0,0,4,0,0,0],
[0,0,0,0,0,0,0,0,0,0,0,0,0,0,0]]

Si se mezclara este último mapa con el anterior, se vería que, efectivamente, las escaleras actúan como conectores entre las plataformas, y que la parte superior de las primeras se superpone con el bloque conectado. De todas formas, no hace falta imaginarlo, ya que va a ser posible verlo en cuanto se implemente el pintado del mapa en el canvas.

Como se ha visto en el código del bucle principal, éste llama a una función render que es la encargada de representar el mundo sobre el canvas. Ahora mismo, dicha función tan sólo rellena todo el espacio de color negro, y pinta un cuadrado verde para el jugador.

Voy a eliminar el contenido de la función para que pinte el mapa, pero antes necesito cargar las imágenes para tenerlas disponibles para su uso. Esta tarea se puede realizar añadiendo al principio de la función principal un fragmento de código como éste (he copiado previamente las imágenes que voy a emplear al directorio www/img):

    var imgGrassMid = new Image();
    imgGrassMid.src = 'img/grassMid.png';
    var imgGrassRight = new Image();
    imgGrassRight.src = 'img/grassRight.png';
    var imgGrassLeft = new Image();
    imgGrassLeft.src = 'img/grassLeft.png';
    var imgLadderMid = new Image();
    imgLadderMid.src = 'img/ladder_mid.png';

    var ASSETS = new Array();
    ASSETS.push('');
    ASSETS.push(imgGrassMid);
    ASSETS.push(imgGrassRight);
    ASSETS.push(imgGrassLeft);
    ASSETS.push(imgLadderMid);

El array ASSETS, con ese push vacío al principio, me proporciona una forma sencilla (aunque algo sucia) de posicionar los recursos gráficos en el mismo orden que estoy usando para representarlos en el mapa.

Ahora que los tengo cargados, ya puedo pintarlos en el canvas:

    function render() {
        // Pintamos todo el canvas de azul claro
        ctx.fillStyle = 'lightblue';
        ctx.fillRect(0, 0, canvas.width, canvas.height);
        
        // Recorremos la rejilla pintando los tiles representados
        // en la misma.
        for (var y=0; y < 12; y++) {
            for (var x=0; x < 15; x++) {
                var imgNum = map[y][x];
                if (imgNum != 0) {
                    ctx.drawImage(ASSETS[imgNum], x * BLKSIZE, y * BLKSIZE);
                }
                
                var ladderNum = map_ladders[y][x];
                if (ladderNum != 0) {
                    ctx.drawImage(ASSETS[4], x * BLKSIZE, y * BLKSIZE);
                }
            }
        }
    };

Con esto, ya está implementada la representación lógica y visual del mapa, y el primer nivel del juego queda representado en el navegador:

blog_juego_p2

Por supuesto, esta implementación de la función render es muy ineficiente, ya que se está pintando toda la pantalla indistintamente de lo que cambie. Una de las tareas que será necesario hacer en el futuro, será detectar qué partes de la rejilla han cambiado, y hacer que se pinten sólo estas últimas.

En la siguiente parte, añadiré el personaje del jugador y una lógica sencilla de movimiento, para que el juego empiece a ser algo interactivo.

Desarrollando un videojuego sencillo (Parte I – Introducción)

Durante mi carrera profesional, he tenido la fortuna de ir probando varias de las distintas vertientes que tiene el campo de las TI. Empecé como técnico de hardware, de ahí pasé a administrador de sistemas y, finalmente, llevo casi cuatro años desarrollando software, con la suerte de haber podido trabajar en proyectos de diversa naturaleza. Durante todo este tiempo, además, he ido complementando mi formación con proyectos personales, especialmente en el área de los Sistemas Operativos. Pero, a pesar de ello, sigo teniendo una espinita clavada: desarrollar un videojuego desde cero. Así pues, he decidido ponerme manos a la obra y, de paso, contar mi experiencia en este blog.

(more…)