Cómo había escrito hace unos días, este es el primer artículo de una serie en la que iré contando cómo creo con HTML5 y Javascript un clon del Breakout. En esta primera parte, centraré mi trabajo en crear la estructura del juego (carpetas y archivos), el archivo html (sólo necesitaré uno), el bucle del juego (Game Loop) y terminaré creando una bola que haré rebotar contra las paredes.
El HTML
1 |
charset="UTF-8">Breakout - Parte 1 |
Breakout – Parte 1
1 2 3 4 5 6 7 8 9 |
<canvas id="canvas" width="400" height="500"> Tu navegador no soporta canvas. Actualízalo. </canvas> <!-- http://paulirish.com/2011/requestanimationframe-for-smart-animating/ --> <script src="js/rAF.js"></script> <!-- Clase ball --> <script src="js/ball.js"></script> <!-- Archivo principal del juego --> <script src="js/game.js"></script> |
El código HTML que utilizo sólo dispone de un elemento canvas que es en el que dibujaré todo. El canvas será el contenedor de todos los elementos del juego. Además utilizaré dos archivos javascript. El primero «rAF.js» es el ya famoso script de Paul Irish para utilizar requestAnimationFrame en cualquier navegador. También tendré un archivo para almacenar la clase Ball. Esta clase me permitirá crear tantas bolas como quiera y definir métodos específicos para la bola. Por último, en «game.js» guardará todo el javascript necesario para hacer funcionar el juego.
El bucle del juego: Game Loop
En mi caso, el game loop se compondrá de las siguientes acciones y se ejecutará en el siguiente orden:
- Comprobar acciones del usuario. ¿El usuario ha pulsado una tecla?
- Actualizar las posiciones de los elementos del juego. Mover la pala a la posición requerida según la tecla que ha pulsado el usuario. Mover la bola a la posición siguiente.
- Comprobar colisiones. Hay que comprobar todas las colisiones: bola-pared, bola-pala o bola-bloque.
- Dibujar todo. Una vez que tengo todas las posiciones definitivas de los elementos del juego, toca dibujarlos en pantalla.
Existen más pasos en un bucle de juego, cómo realizar operaciones de la inteligencia artificial o mover enemigos, pero este no es mi caso. Por ahora, en esta primera parte, voy a omitir el primer paso. Este será nuestro bucle:
1 2 3 4 5 6 7 8 9 10 11 |
// Bucle del juego. function gameloop() { window.requestAnimationFrame(gameloop); // Comprobar acciones del usuario --> PRÓXIMAMENTE. // Actualizar posiciones. updateAll(); // Comprobar colisiones. checkCollisions(); // Dibujar todo. drawAll(); } |
La primera línea hace que la función «gameloop» se ejecute en bucle indefinidamente utilizando el requestAnimationFrame. Antes de poder utilizar esta función, necesito inicializar mi juego y crear los elementos que lo van a componer. En esta primera parte, sólo voy a crear la bola.
Crear la bola: ball.js
Para crear la bola, no tengo más que crear un círculo. En canvas, esto se hace con el método arc. Cómo es bastante sencillo, no voy a extenderme sobre cómo hacerlo y voy a crear un objeto Ball. Este es el contenido del archivo ball.js
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 |
// Constructor de la bola. function Ball(x, y, radius, angle, speed, color) { // Valores por defecto. var DEFAULTS = { x: 200, y: 250, radius: 5, angle: 60, speed: 5, color: '#ffffff' }; // Asignación de valores si existen. this.x = (x == undefined) ? DEFAULTS.x : x; this.y = (y == undefined) ? DEFAULTS.y : y; this.radius = (radius == undefined) ? DEFAULTS.radius : radius; this.angle = (angle == undefined) ? DEFAULTS.angle : angle; this.speed = (speed == undefined) ? DEFAULTS.speed : speed; this.color = (color == undefined) ? DEFAULTS.color : color; console.log('¡Bola creada!'); }; // Sobreescribimos el prototype para añadir métodos. Ball.prototype = { constructor: Ball, // Método draw que se encargará de dibujar la bola. draw: function (context) { context.fillStyle = this.color; context.beginPath(); context.arc(this.x, this.y, this.radius, 0, Math.PI*2, true); context.closePath(); context.fill(); } }; |
Por el momento, tengo un constructor en la que asigno todas las variables necesarias sea por los valores por defecto o por los valores que pasaré por parámetro al mismo. Estos valores que veo necesarios son:
- x: Coordenada en el eje X.
- y: Coordenada en el eje Y.
- radius: Radio de la bola.
- angle: Ángulo de la dirección hacia la que se mueve la bola.
- speed: Velocidad a la que se mueve la bola.
- color: Color de la bola.
Ahora puedo crear una bola utilizando esta clase. Vuelvo al archivo game.js y escribo las sentencias que inician mi juego y creo los elementos necesarios (la bola en este caso). Este es el contenido actual del archivo:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
(function() { // Capturo el canvas donde voy a dibujar. var canvas = document.getElementById('canvas'); // Capturo el contexto 2d. var context = canvas.getContext('2d'); // Creo una bola. var ball = new Ball(); // Inicio el bucle. gameloop(); // Bucle del juego. function gameloop() { window.requestAnimationFrame(gameloop); // Comprobar acciones del usuario --> PRÓXIMAMENTE. // Actualizar posiciones. updateAll(); // Comprobar colisiones. checkCollisions(); // Dibujar todo. drawAll(); } function updateAll() { // Actualizar posiciones... } function checkCollisions() { // Comprobar colisiones... } function drawAll() { // Dibujar todo... // Borro el canvas. context.clearRect(0, 0, canvas.width, canvas.height); // Dibujar bola. ball.draw(context); } }()); |
Sólo quiero remarcar la línea en la que borro la canvas. Es importante borrar el canvas porque sino estaría guardando el rastro que va dejando la bola. Si no pusiera esa línea de código, tendría un trazo en vez de una bola que se mueve en el canvas.
Ahora tengo una bola justo en mitad de la pantalla, pero quiero que se mueva, por lo tanto vamos a programar el segundo método update().
Mover la bola: método update
Añado el método update al archivo ball.js, concretamente al Prototype de Ball. Este es el método:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
// Método update que se encarga de actualizar la posición de la bola. update: function() { // Calculo el ángulo en radianes. this.radians = this.angle * Math.PI/180; // Actualizo el movimiento sobre el eje X. this.vX = Math.cos(this.radians) * this.speed; // Actualizo el movimiento sobre el eje Y. this.vY = Math.sin(this.radians) * this.speed; // Actualizo la posición (x,y). this.x += this.vX; this.y += this.vY; } |
Convertimos el ángulo a radianes porque las funciones coseno y seno de javascript sólo funcionan con radianes. Al final de esta entrada, os pongo varias fuentes de información que he utilizado para encontrar las fórmulas necesarias para calcular el movimiento de la bola.
Además, como es de esperar modificamos el archivo game.js y la función updateAll:
1 2 3 4 5 |
function updateAll() { // Actualizar posiciones... // Actualizar la posición de la bola. ball.update(); } |
El problema que tengo ahora es que la bola sale de la pantalla y desaparece. Es el momento de programar la comprobación de colisiones con las paredes.
Comprobar colisiones de la bola con la pared
Nuestro método debe de comprobar si la bola colisiona con cualquiera de las cuatro paredes. En cuyo caso, debe cambiar el ángulo de la dirección hacia la que se mueve la bola. Para eso es importante tener en cuenta lo siguiente:
- Para las colisiones con la pared izquierda y derecha, debo de comprobar si existe algún punto de la bola que ha sobrepasado o está encima de la pared. Para comprobar si la bola ha superado la pared derecha, sólo debo comprobar si el punto más a la derecha de la bola tiene una coordenada x mayor que el ancho del canvas. El punto más a la derecha de la bola es el centro sumando el radio. En el caso de la pared izquierda, debo comprobar que el punto más a la izquierda de la bola es menor que 0 en su coordenada x. El punto más a la izquierda es el centro restando el radio.
- En el caso de las colisiones de pared superior e inferior, ocurre algo parecido pero con el punto situado más arriba y el punto situado más abajo.
Este es el código necesario:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 |
// Método checkCollisions que se encarga de comprobar las colisiones. checkCollisions: function(canvas) { if (canvas === undefined) { return; } // Colisión con la pared derecha o izquierda. if ( ((this.x + this.radius) >= canvas.width) || ((this.x - this.radius) <= 0) ) { if ((this.x - this.radius) <= 0) { this.x = this.radius; } else { this.x = canvas.width - this.radius; } this.angle = 180 - this.angle; this.update(); } // Colisión con la pared inferior o superior. else if ( ((this.y + this.radius) >= canvas.height) || ((this.y - this.radius) <= 0) ) { if ((this.y - this.radius) <= 0) { this.y = this.radius; } else { this.y = canvas.height - this.radius; } this.angle = 360 - this.angle; this.update(); } } |
Un apunte que deseo hacer sobre las líneas 10,12,20 y 22. Son necesarias porque sin ellas podría ocurrir que la bola ya haya superado la pared o se encuentre encastrada en la misma y eso daría lugar a un comportamiento inesperado. Estas líneas de código recolocan la bola a la mínima distancia posible de la pared, esto evitará que se quede atascada en la pared.
Y por último, añado el código a game.js para llamar a la función de comprobar colisiones.
1 2 3 4 5 |
function checkCollisions() { // Comprobar colisiones... // Comprobar colisiones bola-paredes. ball.checkCollisions(canvas); } |
¡Ya está! Nuestra bola se mueve y colisiona correctamente con las cuatro paredes. Os dejo la demostración con el código completo: VER DEMO
Algunas de las fuentes que he utilizado:
- HTML5 Canvas de Steve Fulton y Jeff Fulton
- Javascript Breakout en Code inComplete
- Canvas Tutorial de Bill Mill
Este artículo forma parte de una serie:
- Introducción: Another Breakout game…
- Diario Breakout – Parte 1
- Diario Breakout – Parte 2
- Diario Breakout – Parte 3
- Diario Breakout – Parte 4
- Diario Breakout – Parte 5 y ¿última?
- Diario Breakout – Integrar Clay.io
He subido Breakout a GitHub.