Aprende a crear juegos en HTML5 Canvas

lunes, 13 de febrero de 2012

Parte 5. Interactuando con varios elementos iguales.

Ya vimos como hacer para que un objeto interactúe con otro. El problema sería si quisiéramos, por ejemplo, querer interactuar con 50 elementos que hagan exactamente lo mismo (Como serían por ejemplo los enemigos). Tener que evaluar uno por uno sería demasiado tedioso y complicado. Afortunadamente, hay una forma más sencilla de interactuar con varios elementos de propiedades iguales a través de los arreglos.

Para este ejemplo, crearemos una variable de tipo arreglo llamada “wall”:
var wall = new Array();
Este arreglo contendrá todos nuestros elementos de tipo pared. Ahora, agregaremos cuatro elementos a este arreglo en la función “init” de la siguiente forma:
    // Create walls
    wall.push(new Rectangle(100, 50, 10, 10));
    wall.push(new Rectangle(100, 100, 10, 10));
    wall.push(new Rectangle(200, 50, 10, 10));
    wall.push(new Rectangle(200, 100, 10, 10));
Para dibujar los elementos de la pared, recorreremos los elementos del arreglo a través de un “for”, de la siguiente forma:
    // Draw walls
    ctx.fillStyle = '#999';
    for (i = 0, l = wall.length; i < l; i += 1) {
        wall[i].fill(ctx);
    }
De igual forma, comprobaremos cada elemento de la pared con un “for”, y comprobaremos si hace una intersección con la comida o el jugador:
        // Wall Intersects
        for (i = 0, l = wall.length; i < l; i += 1) {
            if (food.intersects(wall[i])) {
                food.x = random(canvas.width / 10 - 1) * 10;
                food.y = random(canvas.height / 10 - 1) * 10;
            }

            if (player.intersects(wall[i])) {
                pause = true;
            }
        }
Primero, comprobamos si la comida choca con la pared. En dado caso, cambiamos de lugar la comida, esto evitará que esta quede “atorada” en la pared. Segundo, comprobamos si el jugador choca con la pared, y en tal caso, el juego se detendrá. Eso está bien, pero lo ideal sería que cuando el jugador choque, al reanudar el juego, este comience desde el principio.

Arreglemos esto. Comencemos por saber cuando el jugador ha perdido, a través de una variable llamada “gameover”:
var gameover = true;
Luego, agreguemos estas líneas justo donde el juego comienza, después del “if (!pause)”:
        // GameOver Reset
        if (gameover) {
            reset();
        }
De esta forma, llamaremos a una función llamada “reset”, donde indicaremos como queremos que inicie el juego. En este caso, pondremos el score en cero, la dirección hacia su punto original, regresaremos al jugador a su punto inicial y cambiaremos de lugar la comida. Por último, por supuesto, nos aseguraremos que el juego deje de estar en Game Over:
function reset() {
    score = 0;
    dir = 1;
    player.x = 40;
    player.y = 40;
    food.x = random(canvas.width / 10 - 1) * 10;
    food.y = random(canvas.height / 10 - 1) * 10;
    gameover = false;
}
Por último, cambiaremos el “if (pause)” en nuestra función “paint” para ver si el juego está en Game Over o en una pausa común:
    // Draw pause
    if (pause) {
        ctx.textAlign = 'center';
        if (gameover) {
            ctx.fillText('GAME OVER', 150, 75);
        } else {
            ctx.fillText('PAUSE', 150, 75);
        }
        ctx.textAlign = 'left';
    }
De esta forma, concluimos con todos los conocimientos básicos para crear un juego. En el último capítulo, nos enfocaremos en los detalles para darle forma a este juego.

Código final:

[Canvas not supported by your browser]
var KEY_ENTER = 13,
    KEY_LEFT = 37,
    KEY_UP = 38,
    KEY_RIGHT = 39,
    KEY_DOWN = 40,
    
    canvas = null,
    ctx = null,
    lastPress = null,
    pause = true,
    gameover = true,
    dir = 0,
    score = 0,
    wall = new Array(),
    player = null,
    food = null;

window.requestAnimationFrame = (function () {
    return window.requestAnimationFrame ||
        window.mozRequestAnimationFrame ||
        window.webkitRequestAnimationFrame ||
        function (callback) {
            window.setTimeout(callback, 17);
        };
}());

document.addEventListener('keydown', function (evt) {
    lastPress = evt.which;
}, false);

function Rectangle(x, y, width, height) {
    this.x = (x == null) ? 0 : x;
    this.y = (y == null) ? 0 : y;
    this.width = (width == null) ? 0 : width;
    this.height = (height == null) ? this.width : height;

    this.intersects = function (rect) {
        if (rect == null) {
            window.console.warn('Missing parameters on function intersects');
        } else {
            return (this.x < rect.x + rect.width &&
                this.x + this.width > rect.x &&
                this.y < rect.y + rect.height &&
                this.y + this.height > rect.y);
        }
    };

    this.fill = function (ctx) {
        if (ctx == null) {
            window.console.warn('Missing parameters on function fill');
        } else {
            ctx.fillRect(this.x, this.y, this.width, this.height);
        }
    };
}

function random(max) {
    return Math.floor(Math.random() * max);
}

function reset() {
    score = 0;
    dir = 1;
    player.x = 40;
    player.y = 40;
    food.x = random(canvas.width / 10 - 1) * 10;
    food.y = random(canvas.height / 10 - 1) * 10;
    gameover = false;
}

function paint(ctx) {
    var i = 0,
        l = 0;
    
    // Clean canvas
    ctx.fillStyle = '#000';
    ctx.fillRect(0, 0, canvas.width, canvas.height);
    
    // Draw player
    ctx.fillStyle = '#0f0';
    player.fill(ctx);
    
    // Draw walls
    ctx.fillStyle = '#999';
    for (i = 0, l = wall.length; i < l; i += 1) {
        wall[i].fill(ctx);
    }
    
    // Draw food
    ctx.fillStyle = '#f00';
    food.fill(ctx);

    // Debug last key pressed
    ctx.fillStyle = '#fff';
    //ctx.fillText('Last Press: '+lastPress,0,20);
    
    // Draw score
    ctx.fillText('Score: ' + score, 0, 10);
    
    // Draw pause
    if (pause) {
        ctx.textAlign = 'center';
        if (gameover) {
            ctx.fillText('GAME OVER', 150, 75);
        } else {
            ctx.fillText('PAUSE', 150, 75);
        }
        ctx.textAlign = 'left';
    }
}

function act() {
    var i,
        l;
    
    if (!pause) {
        // GameOver Reset
        if (gameover) {
            reset();
        }

        // Change Direction
        if (lastPress == KEY_UP) {
            dir = 0;
        }
        if (lastPress == KEY_RIGHT) {
            dir = 1;
        }
        if (lastPress == KEY_DOWN) {
            dir = 2;
        }
        if (lastPress == KEY_LEFT) {
            dir = 3;
        }

        // Move Rect
        if (dir == 0) {
            player.y -= 10;
        }
        if (dir == 1) {
            player.x += 10;
        }
        if (dir == 2) {
            player.y += 10;
        }
        if (dir == 3) {
            player.x -= 10;
        }

        // Out Screen
        if (player.x > canvas.width) {
            player.x = 0;
        }
        if (player.y > canvas.height) {
            player.y = 0;
        }
        if (player.x < 0) {
            player.x = canvas.width;
        }
        if (player.y < 0) {
            player.y = canvas.height;
        }

        // Food Intersects
        if (player.intersects(food)) {
            score += 1;
            food.x = random(canvas.width / 10 - 1) * 10;
            food.y = random(canvas.height / 10 - 1) * 10;
        }

        // Wall Intersects
        for (i = 0, l = wall.length; i < l; i += 1) {
            if (food.intersects(wall[i])) {
                food.x = random(canvas.width / 10 - 1) * 10;
                food.y = random(canvas.height / 10 - 1) * 10;
            }

            if (player.intersects(wall[i])) {
                gameover = true;
                pause = true;
            }
        }
    }
    
    // Pause/Unpause
    if (lastPress == KEY_ENTER) {
        pause = !pause;
        lastPress = null;
    }
}

function repaint() {
    window.requestAnimationFrame(repaint);
    paint(ctx);
}

function run() {
    setTimeout(run, 50);
    act();
}

function init() {
    // Get canvas and context
    canvas = document.getElementById('canvas');
    ctx = canvas.getContext('2d');
    
    // Create player and food
    player = new Rectangle(40, 40, 10, 10);
    food = new Rectangle(80, 80, 10, 10);
    
    // Create walls
    wall.push(new Rectangle(100, 50, 10, 10));
    wall.push(new Rectangle(100, 100, 10, 10));
    wall.push(new Rectangle(200, 50, 10, 10));
    wall.push(new Rectangle(200, 100, 10, 10));
    
    // Start game
    run();
    repaint();
}

window.addEventListener('load', init, false);

21 comentarios:

  1. He venido siguiendo cada uno de los capitulos del tutorial, es excelente encontrar un tutorial de esta calidad, el lenguaje es claro y facil de entender. Espero siga avanzando en las entregas, estare muy pendiente para devorar todo este material. Leonardo E.

    ResponderEliminar
  2. Muy buenos tutoriales, no sólo esán bien explicados sino que además motivan a seguir avanzando para acabarlos.

    ResponderEliminar
  3. En serio muchas gracias, este curso lo estoy cogiendo con ganas... cada vez me veo más cerca de poder hacer mi juego de cartas cargando imágenes con links y sin que el jugador deba bajarse nada!
    Sólo tengo una duda, a lo mejor ves esto y me puedes responder: se me hace raro que las funciones sean las clases, pero me acostumbraré... al principio del código haces un llamado a init cuando la página de haya cargado pero... eso no significa que, al llamar al init antes de declarar las variables, se saltaría todas las declaraciones de variables? No habría que declararlas antes y luego llamar al init? Podrías explicarme un poco el tema este del orden del init y las declaraciones, y también el orden de las funciones: importa si primero declaro una función (random por ejemplo) y luego la uso en la siguiente, o si primero la uso y luego la declaro? Podría dar error en este caso porque la primera no encuentre la segunda (pues ha sido creada después) o da igual el orden en que declaremos las funciones?

    ResponderEliminar
    Respuestas
    1. ¡Ah! Pasar de C++ a JavaScript, que gran salto tan enorme y aterrador, aun recuerdo cuando entré en estado de shock cuando tuve que botar por el inodoro todo el conocimiento que me habían hecho aprender a fuerza de errores de compilación en la escuela.

      Te lo resumiré así: Los verdaderos programadores, llaman a JavaScript un chiste, una broma de mal gusto, ya que es demasiado permisible y flexible. No le importa el órden de las funciones, no importa si no declaras una variable antes de usarla. Incluso, ¡No importa si olvidas poner un punto y coma al final de un renglón!

      Pero siempre es mejor hacer uso de las buenas prácticas de programador, para evitar posibles errores, y para no olvidar lo aprendido cuando uno use otros lenguajes.

      Por último, notando que si altera un poco esa primer línea, te explicaré como funciona la llamada al init. La función Init es agregada a un "escucha", que accionará dicha función hasta que termine de cargar la página web. Por eso las variables son declaradas antes que el propio init, y esa función se realiza hasta varios milisegundos después, cuando la página ha cargado por completo, por lo que no hay problemas con ello.

      Sin embargo, es importante asignarla al momento que se cargue la página y no antes, ya que si se le llamara de forma directa [ init(); ], no solo causaría un error por iniciar antes de declarar las variables, si no que además, sería llamada antes de crearse el elemento donde se dibujaría el juego, lo que ocasionaría un grave error al intentarlo asignarlo a la variable del mismo nombre, imposibilitando el funcionamiento del juego mismo.

      Buenas observaciones, que bueno que has preguntado, y espero esto tranquilice tus códigos posteriores con JavaScript. Suerte :)

      Eliminar
    2. ya veo, primero la página se carga entera, lo que implica cargar todas las variables declaradas, y una vez hecho todo esto llamamos al init e inicia todo, pues hasta no llamar al init nada inicia pues todo está dentro de funciones, y eso no se inicia solo!
      Para organizarme mejor estoy haciendo mil funciones para todo, para poder esconder cosas. Por ejemplo, todas las declaraciones de variables con sus valores iniciales las tengo dentro de un "if(true) {..."; para poder esconderlas y trabajar más a gusto con todo lo demás (notepad++ permite esconder los ifs con un boton - que sale al lado).

      Pues sí, esto de que las variables no tengan tipo (int, char, string), que las clases sean funciones... está todo muy suelto y raro. En parte no me molesta tanto, pues antes de aprender C++ aprendí GML, el lenguaje de Game Maker (estupendo programar para hacer juegos, más sencillo que javascript pues te lo da todo mascado, pero menos compatible (nada compatible) con linux, este es mas versatil) y allí tampoco dabamos tipo a las variables, ni poniamos ; si no querias, ni () rodeando los ifs... pero no era TAN flexible como javascript, mola xD
      En todo caso como dices, hay que programar con normas propias. AUNQUE esto de que javascript no reporte errores por casi nada puede ser malo, pues los errores a la mínima suele ser lo que permite al programador darse cuenta de sus errores, no? Seguramente me tope con esto pronto.

      Eliminar
    3. Es cierto, puede ser un problema que JavaScript no reporte errores para los programadores, pero permite que códigos parcialmente correctos se ejecuten "sin problemas" para los usuarios, aunque a veces no hacen exactamente lo que el programador esperaba (Pero al menos mantiene satisfecho a los usuarios).

      Por supuesto, para los programadores, tenemos buenas herramientas que es bueno siempre usar para prevenir estos problemas. En la misma introducción lo menciono, en el tercer párrafo, que cito a continuación:

      "... a diferencia de muchos otros lenguajes, es imposible saber de forma nativa si existe algún problema con nuestro código de Javascript, pero hoy día los navegadores cuentan con muchas herramientas para ayudarnos con este problema. En Firefox podemos agregar el Add-On de Firebug (addons.mozilla.org/es-es/firefox/addon/firebug), el cual nos notificará de forma automática de errores en nuestro código. En Google Chrome, podemos acceder a la consola de javascript presionando F12, en la cual podremos ver los errores en nuestro código."

      Espero esto te sirva para prevenir errores que puedas pasar por alto :)

      Eliminar
  4. Yo ahora mismo estoy cursando el 2º año de ciclo superior de Desarrollo de aplicaciones webs y ya te digo que me he pegado muchos mareos aprendiendo JavaScript exactamente por que no te dice los Errores jajaja.

    Referente a tu tutorial, es muy bueno, yo aún no he dado canvas y gracias a ti me estoy iniciando en ello. Muchas gracias =).

    Podrias plantearte hacer un minitutorial sobre tratamiento de los tiles, como trabajar con ellos, o incluso como crear mapas a partir de una matriz de caracteres y a continuación asignandoles tiles.

    Es una idea, ya que es un tema muy interesante y al menos a mi me resultaria muy util.

    Lo dicho, sigue asi y gracias por tu esfuerzo =)

    ResponderEliminar
    Respuestas
    1. Como ya he mencionado antes, usar Firebug para Firefox o la consola de JavaScript para Chrome, y no tendrás que preocuparte mas por saber si hay errores, y que los provocan.

      Me alegra saber que este curso es de ayuda para ti. Sobre los tiles, ya esta planeado en un futuro, pero adelantaré el tema para que pueda ayudarte pronto. ¡Gracias!

      Eliminar
  5. Muchas gracias por el curso que están realizando... he aprendido aquí bastante... :)
    espero sigan haciendo muchos mas de HTML5.

    ResponderEliminar
  6. Hola como estas, primero que nada déjame felicitarse por tan buen material, en 12 horas ya tengo un juego funcional basándome en tus artículos... pero este juego tiene varios errores o bugs que se presentan 1 de 20 veces...

    Link del juego: http://snake.angelkurten.com/

    Error 1: En ocaciones la culebra pasa por encima de la comida como si nada... y al volver a pasar si se la come

    Error 2: Muchaces veces cuando pierdo la todas vidas y vuelvo a empezar(sin recargar la pagina), los obstaculos no se pintan.

    y por ultimo una pregunta... yo quiero que mi culebra dispare pero quiero que esta lo haga en dirección a donde tiene la cabeza... y el disparo conserve su trayectoria en caso de que la culebra cambie de ruta

    Espero tu colaboración y muchas gracias.

    ResponderEliminar
    Respuestas
    1. Reviso con detalle los errores que me indicas, que si veo un poco difícil de seguir tu código a primera vista.

      Con respecto a los disparos, en el siguiente tema muestro como crear disparo. Solo debes agregar un "dir" a cada rectángulo, para que así cada disparo tenga su propia dirección (Que será la misma que aquella de la cabeza de la serpiente al ser creado), y mueves el disparo de acuerdo a su respectiva dirección, de igual forma que ahora se hace con la cabeza. Espero me haya explicado claro.

      ¡Éxito en tu código! ;)

      Eliminar
  7. Muy bueno el tutorial, realmente muy interesante! jajajaja

    ResponderEliminar
    Respuestas
    1. estoy trabajando con Node.js, y me esta costando mas que nada la parte gráfica, esto me ayuda mucho,

      Eliminar
  8. Este comentario ha sido eliminado por el autor.

    ResponderEliminar
  9. En las lineas que son así

    for (i = 0, l = wall.length; i < l; i += 1) {

    tengo una corrección, que seguramente es de tipeo.

    La forma correcta de escribirlo es

    for (i = 0; l = wall.length, i < l; i += 1) {

    Cambié ';' por ',' y viceversa. Sino no funciona.

    Saludos y muchas gracias por compartir tu conocimiemto.

    ResponderEliminar
    Respuestas
    1. Tambien tengo para sugerir que se puede escribir directamente

      for (i = 0; i<wall.length; i += 1) {

      aunque supongo que lo hicieron de la otra forma para que esté mas claro.

      Eliminar
    2. Muchas gracias por tu comentario. Me extraña que no te funcione en la forma que lo he hecho yo, ya que lo he hecho así durante mucho tiempo y no he tenido conflicto con ello.

      La razón por la que se hace de esta forma, es para optimizar el ciclo, ya que de esta forma sólo se lee la longitud del arreglo una vez al comienzo, mientras que de lo contrario, esta es leída en cada proceso del ciclo, lo cual puede impactar el rendimiento en arreglos de gran longitud.

      La forma en que tú lo has hecho, aunque funcione, hace exactamente el mismo proceso de leer de nuevo la longitud del arreglo en cada ciclo, por lo que no resulta dicha optimización, aunque no se cual pueda ser el problema que hace que la forma en que he enseñado no funcione contigo.

      Eliminar