Este artículo es la tercera parte de la serie diario Breakout. En esta parte, voy a crear la clase Brick que será la clase encargada de gestionar los bloques del juego. También añadiré unos bloques a la pantalla e implementaré las colisiones entre bola y bloques. Además, mejoraré el sistema de colisiones entre la bola y la pala para hacer más interesante el juego.
La clase Brick
La clase Brick es la encargada de gestionar los bloques del juego. Esta clase es la más sencilla de las que he creado hasta ahora, este es el código javascript:
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 |
// Constructor del bloque. function Brick(x, y, width, height, color) { // Valores por defecto. var DEFAULTS = { x: 0, y: 0, width: 38.9, height: 20, color: '#00ff00' }; // Asignación de valores si existen. this.x = (x == undefined) ? DEFAULTS.x : x; this.y = (y == undefined) ? DEFAULTS.y : y; this.width = (width == undefined) ? DEFAULTS.width : width; this.height = (height == undefined) ? DEFAULTS.height : height; this.color = (color == undefined) ? DEFAULTS.color : color; console.log('Brick created'); }; Brick.prototype = { constructor: Brick, // Método draw que se encarga de dibujar el bloque. draw: function(context) { context.fillStyle = this.color; context.fillRect(this.x, this.y, this.width, this.height); } }; |
Esta clase sólo tiene un método: «draw». No necesita más métodos, ya que no modifica su posición y la gestión de colisiones se hará en la clase Ball. Ahora debo crear bloques en el canvas para mostrarlos.
Crear los bloques en game.js y dibujarlos
Así quedará el archivo game.js con los cambios.
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 |
(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(); // Creo una pala. var paddle = new Paddle(); // Creo los bloques. // En este array almaceno los bloques. var bricks = []; var brick_new, x_act = 1, y_act = 40; // 3 filas de bloques. for (var i = 1; i <= 3; i++) { // 10 columnas de bloques. for (var j = 1; j <= 10; j++) { // Creo un bloque con coordenadas (x_act, y_act). bricks.push(new Brick(x_act, y_act)); // Sumo el ancho de un bloque y 1 de separación entre bloques // para conseguir la siguiente coordenada x. x_act += 38.9 + 1; } // Reseteo la coordenada x al inicio. x_act = 1; // Sumo el alto de un bloque y 1 de separación entre bloques // para conseguir la siguiente coordenada y. y_act += 20 + 1; } // Inicio el bucle. gameloop(); // Bucle del juego. function gameloop() { window.requestAnimationFrame(gameloop); // Actualizar posiciones. updateAll(); // Comprobar colisiones. checkCollisions(); // Dibujar todo. drawAll(); } function updateAll() { // Actualizar posiciones... // Actualizar la posición de la bola. ball.update(); // Actualizar la posición de la pala. paddle.update(); } function checkCollisions() { // Comprobar colisiones... // Comprobar colisiones bola-paredes y bola-pala. ball.checkCollisions(canvas, paddle, bricks); // Comprobar colisiones pala-paredes. paddle.checkCollisions(canvas); } function drawAll() { // Dibujar todo... // Borro el canvas. context.clearRect(0, 0, canvas.width, canvas.height); // Dibujar bola. ball.draw(context); // Dibujar pala. paddle.draw(context); // Dibujar los bloques. var total_bricks = bricks.length; while (total_bricks--) { bricks[total_bricks].draw(context); } } }()); |
Las líneas 14 hasta 35 y 66 hasta 70 contienen el código que he añadido. Con el primer código (líneas 14 a 35), creo 3 filas de 10 columnas de bloques. Cada bloque medirá 38.9 de ancho y 20 de alto. Entre cada bloque hay una separación de 1. Cada bloque creado se almacena en un array. Con el segundo cacho de código (líneas 66 a 70) recorro el array de bloques y los dibujo uno a uno. En la línea 62, añado el array bricks a la llamada al método «checkCollisions» de mi clase Ball. De esta forma, en ese método, puedo comprobar las colisiones entre bola y bloques.
Colisión entre bola y bloques
Este es el código responsable de comprobar las colisiones entre bola y bloques:
1 2 3 4 5 6 7 8 9 10 |
if (bricks !== undefined) { var total_bricks = bricks.length, brick; while (total_bricks--) { brick = bricks[total_bricks]; if (this.intercept(brick, false)) { bricks.splice(total_bricks, 1); } } } |
Recorro el array de bloques y para cada bloque hago una llamada a un nuevo método «intercept». Si este método devuelve verdadero, elimino el bloque del array.
El nuevo método «intercept»
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 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 |
// Método que se encarga de comprobar si hay colisión entre la bola y un rectángulo. intercept: function (rectangle, isPaddle) { var distance_sq, ballDistance = { x: null, y: null }; ballDistance.x = Math.abs(this.x - (rectangle.x + (rectangle.width/2))); ballDistance.y = Math.abs(this.y - (rectangle.y + (rectangle.height/2))); if (ballDistance.x > (rectangle.width/2 + this.radius)) { // No colisionan. return false; } if (ballDistance.y > (rectangle.height/2 + this.radius)) { // No colisionan. return false; } if (ballDistance.x <= (rectangle.width/2)) { // Hay colisión con uno de los bordes superior o inferior. if (this.y >= (rectangle.y + (rectangle.height/2))) { // Hay colisión con el borde inferior. this.y = rectangle.y + rectangle.height + this.radius; this.angle = 360 - this.angle; } else { // Hay colisión con el borde superior. this.y = rectangle.y - this.radius; // Si se trata de una colisión bola-pala, la respuesta a la colisión es la siguente. if (isPaddle) { // Si colisiona con el cuadrante izquierdo, invertimos el ángulo hacia ese lado. if (this.angle < 90 && (this.x < (rectangle.x + (rectangle.width/5)))) { this.angle += 180; // Si colisiona con el cuadrante derecho, invertimos hacia ese lado. } else if (this.angle > 90 && (this.x > (rectangle.x + (4*rectangle.width/5)))) { this.angle -= 180; // Si colisiona con el medio de la pala, enderezamos un poco el ángulo. } else if ((this.x > (rectangle.x + (2*rectangle.width/5))) && (this.x < (rectangle.x + (3*rectangle.width/5)))) { this.angle = 180 - (this.angle*2); // Sino no hacemos nada especial. } else { this.angle = 360 - this.angle; } } else { // Si se trata de una colisión bola-bloque, la respuesta es básica. this.angle = 360 - this.angle; } } this.update(); return true; } if (ballDistance.y <= (rectangle.height/2)) { // Hay colisión con uno de los bordes laterales. if (this.x >= (rectangle.x + (rectangle.width/2))) { // Hay colisión con el borde derecho. this.x = rectangle.x + rectangle.width + this.radius; } else { // Hay colisión con el borde izquierdo. this.x = rectangle.x - this.radius; } this.angle = 180 - this.angle; this.update(); return true; } distance_sq = (ballDistance.x - (rectangle.width/2)) * (ballDistance.x - (rectangle.width/2)) + (ballDistance.y - (rectangle.height/2)) * (ballDistance.y - (rectangle.height/2)); if (distance_sq <= (this.radius*this.radius)) { if (this.x >= (rectangle.x + (rectangle.width/2))) { if (this.y >= (rectangle.y + (rectangle.height/2))) { // Hay colisión con esquina inferior derecha. this.x = rectangle.x + rectangle.width + this.radius; this.y = rectangle.y + rectangle.height + this.radius; if (this.angle > 0 && this.angle < 90) { // Imposible. } else if (this.angle > 90 && this.angle < 180) { this.angle += -90; } else if (this.angle > 180 && this.angle < 270) { this.angle += 180; } else if (this.angle > 270 && this.angle < 360) { this.angle += 90; } } else { // Hay colisión con esquina superior derecha. this.x = rectangle.x + rectangle.width + this.radius; this.y = rectangle.y - this.radius; if (this.angle > 0 && this.angle < 90) { this.angle += -90; } else if (this.angle > 90 && this.angle < 180) { this.angle += 180; } else if (this.angle > 180 && this.angle < 270) { this.angle += 90; } else if (this.angle > 270 && this.angle < 360) { // Imposible. } } } else { if (this.y >= (rectangle.y + (rectangle.height/2))) { // Hay colisión con esquina inferior izquierda. this.x = rectangle.x - this.radius; this.y = rectangle.y + rectangle.height + this.radius; if (this.angle > 0 && this.angle < 90) { this.angle += 90; } else if (this.angle > 90 && this.angle < 180) { // Imposible. } else if (this.angle > 180 && this.angle < 270) { this.angle += -90; } else if (this.angle > 270 && this.angle < 360) { this.angle += 180; } } else { // Hay colisión con esquina superior izquierda. this.x = rectangle.x - this.radius; this.y = rectangle.y - this.radius; if (this.angle > 0 && this.angle < 90) { this.angle += 180; } else if (this.angle > 90 && this.angle < 180) { this.angle += 90; } else if (this.angle > 180 && this.angle < 270) { // Imposible. } else if (this.angle > 270 && this.angle < 360) { this.angle += -90; } } } this.update(); return true; } } |
Este método es el más complicado que he tenido que implementar hasta ahora y es que he tenido que leer muchas posibles soluciones para entender cómo detectar la colisión entre la bola y un rectángulo y además implementar la respuesta a esta colisión. Este método recibe como parámetro un rectángulo «rectangle» que es un objeto que tiene las siguientes propiedades:
- Coordenada x.
- Coordenada y.
- Ancho width.
- Alto height.
El otro parámetro es «isPaddle». Un simple booleano que será verdadero si la colisión a comprobar es con la pala y falso en caso de que comprobemos la colisión con un bloque. Explicaré más adelante el por qué de este parámetro. Este método ha sido implementado gracias a esta respuesta dada por e. James. Traduzco a continuación la explicación del método:
- Las líneas 8 y 9 calculan el valor absoluto de la coordenada x y la coordenada y que es la diferencia entre el centro del círculo y el centro del rectángulo. Esto hace que sólo deba hacer los cálculos sobre un cuadrante y no sobre los cuatro cuadrantes posibles. En la imagen, el color gris representa el rectángulo, el color rojo representa donde debe de estar el centro del círculo si hay colisión.
- Las líneas 11 a 18 eliminan los casos sencillos en los que el círculo está lo suficientemente lejos como para no colisionar con el rectángulo. Esto es representado por las zonas de color verde en la imagen.
- Las líneas 20 a 53 resuelven el caso en los que el círculo está en el color gris o naranja. Esto es decir que está suficientemente cerca del rectángulo como para estar colisionando con él. Además en mi código, el caso de la colisión con el borde inferior es resuelta de forma sencilla invirtiendo el sentido en el que se dirige la bola, pero en el caso de la colisión con el borde superior de la pala, lo he complicado un poco. Lo que he hecho es dividir la pala en 4 secciones:
- La sección izquierda (Sección 1) que tiene un ancho de 1/5 del ancho de la pala y que va desde el borde izquierdo hasta 1/5 del ancho de la pala.
- La sección central (Sección 3) que tiene un ancho de 1/5 del ancho de la pala y que va desde 2/5 del ancho de la pala a 3/5 del ancho de la pala.
- La sección derecha (Sección 5) que tiene un ancho de 1/5 del ancho de la pala y que va desde 4/5 del ancho de la pala hasta el borde derecho de la misma.
- Las secciones centrales (Secciones 2 y 3) que van de 1/5 a 2/5 y 3/5 a 4/5.
El siguiente dibujo aclara este tema:
El significado de mi código es que según el cuadrante en el que la bola impacte con la pala, cambiaré el ángulo de respuesta a la colisión. Si la bola impacta en la sección 1 o 5, invertiré el ángulo de respuesta hacia ese mismo lado del que viene. Si la bola impacta en la sección 3, la bola verá como el ángulo se endereza ligeramente. Si la bola impacta en la sección 2 o 4, el ángulo se invertirá normalmente. Esto es para hacer el juego más interesante sino la bola iría rebotando siempre hacia el mismo lado siendo muy previsible.
- Las líneas 55 a 68 resuelven el mismo caso que el anterior pero cuando la colisión se produce en vez de en uno de los lados superior o inferior, en un lado lateral.
- Desde la línea 70 a 145, se resuelve el caso más complicado que es el caso en el que la bola colisiona con una de las esquinas del rectángulo. Se examina caso por caso para calcular la respuesta a cada posibilidad. En cada esquina, existe un ángulo que es imposible de tener por ejemplo es imposible que una bola viaja con un ángulo entre 0º y 90º colisione con la esquina inferior derecha de un rectángulo.
Cambios en la detección de colisiones entre bola y pala
Ahora puedo utilizar el nuevo método intercept para gestionar las colisiones entre bola y pala.
1 2 3 |
if (paddle !== undefined) { this.intercept(paddle, true); } |
Y ya está, sólo necesito añadir en mi index.html como siempre la nueva clase que he creado brick.js.
Cada día estoy más cerca de tener una versión que se parece a un juego de verdad. En esta parte, he implementado un sistema de colisiones más avanzado para la pala y la clase Brick. En la siguiente parte, necesitaré trabajar en hacer de esto un juego de verdad (por ejemplo, ahora mismo nunca hay Game Over…).
Aquí os dejo la versión demo de esta tercera parte: VER DEMO
Algunas de las fuentes que he utilizado:
- Respuesta a duda sobre colisión entre círculo y rectángulo en Stack Overflow
- Khan Academy: Recordando clases de mates y físicas…
- Mathematics and Physics for Programmers Second Edition de John Patrick Flynt y Danny Kodicek: Mucho más ameno que el que os había recomendado pero aún así, cuesta leer este tipo de libros.
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.