En esta cuarta parte de Diario Breakout, explicaré como convertir este juego en un juego de verdad. A este juego le falta un menú, botones, un botón de pausa, puntuaciones y sobre todo un «Game Over». ¿Qué es un videojuego sin «Game Over»?
Para poder un poco de orden en todo esto, voy a cambiar el html y crear un archivo de estilos css. En el html, voy a añadir un div que contendrá las puntuaciones y las vidas restantes del jugador y otro div oculto en el que tendré los elementos del menú.
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 |
<!DOCTYPE html> <html> <head> <meta charset="UTF-8"> <title>Breakout - Parte 4</title> <!-- Estilos --> <link href="css/styles.css" rel="stylesheet" /> </head> <body> <h1>Breakout - Parte 4</h1> <div class="container"> <!-- Div que contiene la puntuación y las vidas restantes. --> <div id="scoreboard"> <div class="fl"> <!-- Puntuación --> Score: <span id="score">00000</span> </div> <div class="fr"> <!-- Vidas restantes --> Lives: <span id="lives">3</span> </div> </div> <!-- Canvas del juego --> <canvas id="canvas" height="500" width="400"> Tu navegador no soporta canvas. Actualízalo. </canvas> <!-- Elementos del menú --> <div id="menu"> <!-- Nombre del juego --> <h2 id="title">Breakout</h2> <!-- Game Over - Mensaje que se muestra al perder todas las vidas. --> <h3 id="gameover">Game Over</h3> <!-- You win! - Mensaje que se muestra al ganar. --> <h3 id="gamewin">You win!</h3> <!-- Try again - Mensaje que se muestra al perder una vida. --> <h4 id="message">Try again</h4> <!-- Play - Botón de empezar partida --> <a href="#play" id="button_play">Play</a> <!-- Resume - Botón de reanudar partida después de pausa o después de perder una vida. --> <a href="#resume" id="button_resume">Resume</a> <!-- Quit - Botón de abandonar partida. --> <a href="#quit" id="button_quit">Quit</a> </div> </div> <!-- http://paulirish.com/2011/requestanimationframe-for-smart-animating/ --> <script src="js/rAF.js"></script> <!-- Funciones útiles para javascript - Javascript Patterns --> <script src="js/utils.js"></script> <!-- Clase ball --> <script src="js/ball.js"></script> <!-- Clase paddle --> <script src="js/paddle.js"></script> <!-- Clase brick --> <script src="js/brick.js"></script> <!-- Archivo principal del juego --> <script src="js/game.js"></script> </body> </html> |
El div de id «menu» contiene los elementos necesarios en nuestro menú:
- El nombre del juego.
- El mensaje que se muestra en el «Game Over».
- El mensaje que se muestra cuando el usuario gana la partida.
- El mensaje que se muestra cuando el usuario pierde una vida.
- El botón «Play» que se muestra sólo cuando el usuario pierde la partida («Game Over») o la primera vez que inicia la partida.
- El botón «Resume» que se muestra sólo cuando el usuario pierde una vida o pausa el juego.
- El botón «Quit» que se muestra sólo cuando el usuario pausa el juego.
El div de id «scoreboard» contiene los elementos «score» y «lives». En estos dos elementos, almacenaré la puntuación del usuario y las vidas que le quedan. No pondré el contenido del archivo css, como siempre lo tenéis disponible en la demo. Sólo os diré que utilizo la fuente gratuita Freedom para los textos.
Cambios en game.js, creación de la clase Game
He decidido reorganizar por completo el archivo principal del juego «game.js», crear una clase de la misma manera que he venido haciendo con el resto de entidades del juego. En este caso, he decidido utilizar un nuevo patrón: Singleton. El objetivo de este patrón es evitar que se puedan crear más de una instancia de la clase Game. No tiene sentido que exista más de una instancia de esta clase, por lo tanto me pareció el patrón adecuado.
He añadido a esta clase los estados del juego. Un juego pasa por distintos estados y puede programarse en función de ellos. En este breakout, he definido los siguientes estados:
- GAME_INIT: Estado inicial del juego. Cuando se inicia el juego, este es el estado en el que está.
- GAME_PLAYING: Estado jugando del juego. Cuando se pulsa en el botón «Play» o en el botón «Resume», el juego pasa a este estado.
- GAME_OVER: Estado del juego cuando se ha perdido todas las vidas.
- GAME_WIN: Estado del juego cuando se ha destruído todos los bloques.
- GAME_LIFELOST: Estado del juego cuando se pierde una vida.
- GAME_PAUSE: Estado del juego cuando se pulsa la barra espaciadora, se pausa el juego.
He añadido varios métodos y hecho cambios en los que ya tenía:
- addEvents: Este método añade los eventos click a los botones del menú y el evento al pulsar la barra espaciadora que pausará el juego o reaundará el juego después de pausarlo.
- gameloop: En este método, he añadido la comprobación del estado del juego para poder saber qué hacer en cada momento. Si está en el estado «GAME_PLAYING», el código es muy parecido a lo que tenía antes. En cualquier otro de los estados, se muestra el menú.
- showMenu: Este método como su nombre indica se encarga de mostrar el menú. Según el estado en el que está el juego, mostrará unas opciones u otras.
- hideMenu: Este método oculta el menú por completo.
- resumeGame: Este método se encarga de reanudar el juego después de una pausa.
- pauseGame: Este método se encarga de pausar el juego.
- startGame: Este método se encarga de iniciar una nueva partida, reinicia la puntuación, las vidas restantes y todos los elementos del juego.
- updateScore: Este método se encarga de actualizar la puntuación del jugador.
- updateLives: Este método se encarga de actualizar las vidas restantes del jugador.
- checkCollisions: Este método ha sufrido algún cambio. Al método «checkCollisions» de la clase Ball, le paso ahora la instancia de la clase Game. Esto es necesario ya que ahora existe la posibilidad de perder y necesito llamar a un método (al método «lose») desde la clase Ball. Además si no quedan bloques en el array «bricks», eso significa que el usuario ha ganado.
- drawAll: Este método ha sufrido cambios. Ahora sólo se dibujan los elementos si existe, de esta forma, podemos llamar a este método sin miedo de que surja algún error por haber reiniciado el juego anterior.
- lose: Este método es llamado por la clase Ball sólo cuando la bola impacta contra el borde inferior del canvas, esto significa que se nos ha escapado y por lo tanto el jugador pierde una vida. El juego cambia al estado «GAME_LIFELOST». Si el jugador no tiene vidas, se llama al método «gameover» sino se reinicia la posición de la bola, la posición de la pala y se cambia el estado del juego a «GAME_PAUSE». Esto se hace para evitar que el juego vuelva a empezar y sorprenda al jugador…
- gameover: Este método se encarga de reiniciar todos los elementos del juego y cambiar el estado del juego a «GAME_OVER». Además se mostrará después el típico mensaje de «Game Over».
- gamewin: Este método hace lo mismo que el anterior pero cambiando el estado del juego a «GAME_WIN».
- resetAll: Este método se encarga de reiniciar los elementos del juego y llama al método drawAll() para dibujar el nuevo estado del canvas.
Este es el código completo del javascript con comentarios. Si tenéis alguna duda, dejadme un comentario.
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 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282 283 284 285 286 287 288 289 290 291 292 293 294 295 296 297 298 299 300 301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320 321 322 323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 |
var Game; (function() { // Variable que contendrá la instancia var instance; /** * Clase Game - Singleton * Sólo puede haber una instancia de esta clase. **/ Game = { // Valores por defecto. Defaults: { // Configuración por defecto config: { canvasId: 'canvas' }, // Estados del juego. states: { initial: 1, // Estado inicial del juego. GAME_INIT: 1, // ESTADO INICIAL DEL JUEGO - Pantalla de inicio. GAME_PLAYING: 2, // ESTADO JUGANDO AL JUEGO - Pantalla de juego. GAME_OVER: 3, // ESTADO GAME OVER DEL JUEGO - Pantalla Game Over. GAME_WIN: 4, // ESTADO GAME WIN DEL JUEGO - Pantalla Game Win. GAME_LIFELOST: 5, // ESTADO VIDA PERDIDA. GAME_PAUSE: 6 // ESTADO PAUSA DEL JUEGO. }, // Puntuación. score: { initial: 0 }, // Vidas. lives: { initial: 3, max: 10 } }, // Teclas. KEYS: { KEY_SPACEBAR: 32, KEY_LEFT: 37, KEY_RIGHT: 39 }, // Constructor. initialize: function(config) { if (instance) { return instance; } instance = this; // Asignamos los valores por defecto. this.game = this; this.config = (config==undefined) ? this.Defaults.config : config; this.canvas = document.getElementById(this.config.canvasId); this.context = this.canvas.getContext('2d'); this.states = this.Defaults.states; this.actualstate = this.Defaults.states.initial; this.score = this.Defaults.score.initial; this.lives = this.Defaults.lives.initial; // Esto es necesario para el gameloop. window.game = this; // Añadimos eventos click al menú. this.addEvents(); // Arrancamos el bucle del juego. this.gameloop(); }, // Este método se encarga de añadir los eventos del menú. // El click sobre le play, el click sobre el resume y pulsar la barra espaciadora. addEvents: function() { var self = this; // Al hacer click en Play, iniciamos el juego. utils.addListener(document.getElementById('button_play'), 'click', function(e) { if (self.actualstate == self.states.GAME_INIT || self.actualstate == self.states.GAME_OVER || self.actualstate == self.states.GAME_WIN) { self.startGame(); } e.preventDefault(); }); // Al hacer click en Resume, seguimos con el juego. utils.addListener(document.getElementById('button_resume'), 'click', function(e) { if (self.actualstate == self.states.GAME_PAUSE) { self.resumeGame(); } e.preventDefault(); }); // Al hacer click en Quit, reiniciamos el juego. utils.addListener(document.getElementById('button_quit'), 'click', function(e) { if (self.actualstate == self.states.GAME_PAUSE) { self.actualstate = self.states.GAME_INIT; self.resetAll(); } e.preventDefault(); }); // Al pulsar la barra espaciadora, pausamos el juego. utils.addListener(window, 'keydown', function(e) { if (e.keyCode == self.KEYS.KEY_SPACEBAR) { if (self.actualstate == self.states.GAME_PLAYING) { self.pauseGame(); } else if (self.actualstate == self.states.GAME_PAUSE) { self.resumeGame(); } } }); }, // Este método es el bucle del juego. gameloop: function() { // Es necesario guardar la instancia en una variable para poder utilizar en la // función requestAnimationFrame. En esta función, no puedo utilizar this, ya que // el contexto sería window y this se refiere a window. var self = this.game; window.requestAnimationFrame(self.gameloop); switch(self.actualstate) { // El jugador está jugando. case self.states.GAME_PLAYING: // Actualizar posiciones. self.updateAll(); // Comprobar colisiones. self.checkCollisions(); if (self.actualstate == self.states.GAME_PLAYING) { // Dibujar todo. self.drawAll(); } break; // Al principio del juego o cuando se pausa, se muestra el menú. case self.states.GAME_INIT: case self.states.GAME_PAUSE: case self.states.GAME_OVER: case self.states.GAME_WIN: default: self.showMenu(); break; } }, // Este método se encarga de mostrar el menú. showMenu: function() { document.querySelector('#menu').style.display = 'block'; switch(this.actualstate) { case this.states.GAME_INIT: document.getElementById('button_play').style.display = 'block'; document.getElementById('button_resume').style.display = 'none'; document.getElementById('button_quit').style.display = 'none'; document.getElementById('gameover').style.display = 'none'; document.getElementById('gamewin').style.display = 'none'; document.getElementById('message').style.display = 'none'; break; case this.states.GAME_PAUSE: document.getElementById('button_play').style.display = 'none'; document.getElementById('button_resume').style.display = 'block'; document.getElementById('button_quit').style.display = 'block'; document.getElementById('gameover').style.display = 'none'; document.getElementById('gamewin').style.display = 'none'; document.getElementById('message').style.display = 'none'; break; case this.states.GAME_OVER: document.getElementById('button_play').style.display = 'block'; document.getElementById('button_resume').style.display = 'none'; document.getElementById('button_quit').style.display = 'none'; document.getElementById('gameover').style.display = 'block'; document.getElementById('gamewin').style.display = 'none'; document.getElementById('message').style.display = 'none'; break; case this.states.GAME_WIN: document.getElementById('button_play').style.display = 'block'; document.getElementById('button_resume').style.display = 'none'; document.getElementById('button_quit').style.display = 'none'; document.getElementById('gameover').style.display = 'none'; document.getElementById('gamewin').style.display = 'block'; document.getElementById('message').style.display = 'none'; break; } }, // Este método se encarga de ocultar el menú. hideMenu: function() { document.querySelector('#menu').style.display = 'none'; }, // Este método se encarga de reanudar el juego. resumeGame: function() { // Nuevo estado => Jugando. this.actualstate = this.states.GAME_PLAYING; // Oculto el menú. this.hideMenu(); }, // Este método se encarga de pausar el juego. pauseGame: function() { // Nuevo estado => Jugando. this.actualstate = this.states.GAME_PAUSE; // Mostramos el menú. this.showMenu(); }, // Este método se encarga de iniciar el juego. startGame: function() { // Nuevo estado => Jugando. this.actualstate = this.states.GAME_PLAYING; // Oculto el menú. this.hideMenu(); // Creo una bola. this.ball = new Ball(); // Creo una pala. this.paddle = new Paddle(); // Creo los bloques. // En este array almaceno los bloques. this.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). this.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; } this.score = this.Defaults.score.initial; this.lives = this.Defaults.lives.initial; this.updateScore(); this.updateLives(); }, updateScore: function() { document.getElementById('score').innerHTML = this.score; }, updateLives: function() { document.getElementById('lives').innerHTML = this.lives; }, // Este método se encarga de actualizar las posiciones de los elementos del juego. updateAll: function() { // Actualizar posiciones... // Actualizar la posición de la bola. this.ball.update(); // Actualizar la posición de la pala. this.paddle.update(); }, // Este método se encarga de gestionar las colisiones del juego. checkCollisions: function() { // Comprobar colisiones... // Comprobar colisiones pala-paredes. this.paddle.checkCollisions(this.canvas); // Comprobar colisiones bola-paredes y bola-pala. // this.ball.checkCollisions(this.canvas, this.paddle, this.bricks); this.ball.checkCollisions(this.game); // Comprobar si el usuario ha ganado. if (this.bricks && this.bricks.length <= 0) { this.gamewin(); } }, // Este método se encarga de dibujar los elementos del juego. drawAll: function() { // Dibujar todo... // Borro el canvas. this.context.clearRect(0, 0, this.canvas.width, this.canvas.height); // Dibujar bola. if (this.ball) { this.ball.draw(this.context); } // Dibujar pala. if (this.paddle) { this.paddle.draw(this.context); } // Dibujar los bloques. if (this.bricks) { var total_bricks = this.bricks.length; while (total_bricks--) { this.bricks[total_bricks].draw(this.context); } } }, lose: function() { // Estado vida perdida. this.actualstate = this.states.GAME_LIFELOST; this.lives--; this.updateLives(); if (this.lives <= 0) { this.gameover(); } else { // Reset ball. this.ball.resetPosition(); // Reset paddle. this.paddle.resetPosition(); // Pausamos el juego. this.pauseGame(); } }, gameover: function() { this.actualstate = this.states.GAME_OVER; this.resetAll(); }, gamewin: function() { this.actualstate = this.states.GAME_WIN; this.resetAll(); }, resetAll: function() { this.ball = null; this.bricks = null; this.paddle = null; this.drawAll(); } }; }()); var Breakout = Game.initialize(); |
Cambios en la clase Ball
En la clase Ball, también he hecho varios cambios:
- He almacenado en «initial_data», los valores iniciales (posición, velocidad y ángulo) de la bola para poder utilizarlo en el método «resetPosition».
- He cambiado el método «checkCollisions». Ahora recibe como parámetro una instancia de la clase Game. Esto permite que cuando la bola impacta con el borde inferior del canvas, llame al método «lose» para perder una vida, reiniciar la bola y pala y pausar el juego.
- He añadido el método «resetPosition». Este método se encarga de restaurar la bola a sus valores iniciales.
Este es el código completo de la clase Ball:
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 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236 237 238 239 240 241 242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273 |
// Constructor de la bola. function Ball(x, y, radius, angle, speed, color) { // Valores por defecto. var DEFAULTS = { x: 150, y: 200, 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; // Guardo los valores iniciales para poder reiniciarlos. this.initial_data = { x: this.x, y: this.y, angle: this.angle, speed: this.speed }; console.log('¡Bola creada!'); }; // Sobreescribimos el prototype para añadir métodos. Ball.prototype = { constructor: Ball, // Método draw que se encarga 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(); }, // Método update que se encarga de actualizar la posición de la bola. update: function() { if (this.angle >= 360) { this.angle -= 360; } else if (this.angle < 0) { this.angle += 360; } // 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; }, // Método checkCollisions que se encarga de comprobar las colisiones. checkCollisions: function(game) { var canvas = game.canvas; var paddle = game.paddle; var bricks = game.bricks; if (canvas !== undefined) { // 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 { // Vida perdida. game.lose(); return; } this.angle = 360 - this.angle; this.update(); } } if (paddle !== undefined) { this.intercept(paddle, true); } if (bricks !== undefined) { var total_bricks = bricks.length, brick; while (total_bricks--) { brick = bricks[total_bricks]; if (this.intercept(brick, false)) { if (bricks[total_bricks].hit(game)) { bricks.splice(total_bricks, 1); } game.updateScore(); } } } }, // 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 se encarga de reiniciar a los valores iniciales. resetPosition: function() { this.x = this.initial_data.x; this.y = this.initial_data.y; this.speed = this.initial_data.speed; this.angle = this.initial_data.angle; } }; |
Cambios en la clase Paddle
He hecho unos cambios muy parecidos a los hechos en la clase Ball:
- He almacenado en «initial_data», los valores iniciales de la pala (posición y velocidad).
- Para las teclas de movimiento, utilizo la clase Game en la que he guardado los códigos de estas.
- El método «resetPosition» se encarga de restaurar los valores iniciales de la pala.
Aquí viene el código de esta clase:
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 |
// Constructor de la pala. function Paddle (x, y, width, height, color) { // Valores por defecto. var DEFAULTS = { x: 185, y: 425, width: 50, height: 10, color: '#ff0000', speed: 5 }; // 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; // move_left y move_right se utilizarán para comprobar la dirección // en la que mover la pala. this.move_left = false; this.move_right = false; // Velocidad a la que se moverá la pala. this.speed = DEFAULTS.speed; this.vX = 0; // Valores iniciales de la pala. this.initial_data = { x: this.x, y: this.y, speed: this.speed }; // Iniciamos los eventos de teclado. this.initEvents(); console.log('¡Pala creada!'); }; Paddle.prototype = { constructor: Paddle, // Método initEvents que se encarga de iniciar la captura de eventos. initEvents: function () { var self = this; // Si el usuario pulsa una tecla. utils.addListener(window, 'keydown', function(e) { // Tecla flecha izquierda. if (e.keyCode == Game.KEYS.KEY_LEFT) { self.move_left = true; self.update(); } // Tecla flecha derecha. else if (e.keyCode == Game.KEYS.KEY_RIGHT) { self.move_right = true; self.update(); } }); // Si el usuario suelta una tecla. utils.addListener(window, 'keyup', function(e) { // Tecla flecha izquierda. if (e.keyCode == Game.KEYS.KEY_LEFT) { self.move_left = false; self.update(); } // Tecla flecha derecha. else if (e.keyCode == Game.KEYS.KEY_RIGHT) { self.move_right = false; self.update(); } }); }, // Método draw que se encarga de dibujar la pala. draw: function (context) { context.fillStyle = this.color; context.fillRect(this.x, this.y, this.width, this.height); }, // Método update que se encarga de actualizar la posición de la pala. update: function () { // Si el movimiento es hacia la izquierda y vX es positivo. if (this.move_left && this.vX >= 0) { // Cambio el signo de vX. this.vX = this.speed * -1; } // Si el movimiento es a la derecha y vX es negativo. else if (this.move_right && this.vX <= 0) { // Cambio el signo de vX. this.vX = this.speed; } // Si no hay que ir ni a izquierda ni a derecha. vX = 0. if (!this.move_left && !this.move_right) { this.vX = 0; } // Actualizamos la posición en el eje X. this.x += this.vX; }, // Método checkCollisions que se encarga de gestionar las colisiones entre bola y paredes. checkCollisions: function(canvas) { if (canvas === undefined) { return; }; // Comprobar si colisiona con pared derecha. if ((this.x + this.width) >= canvas.width) { this.x = canvas.width - this.width; } // Comprobar si colisiona con pared izquierda. else if (this.x <= 0) { this.x = 0; }; }, // Este método se encarga de reiniciar la pala a sus valores iniciales. resetPosition: function() { this.x = this.initial_data.x; this.y = this.initial_data.y; this.speed = this.initial_data.speed; } }; |
Esto ya se parece más a un juego
Esto ya es un juego. Tengo un menú, un play, un game over, puntuación, un principio y un fin… Ahora lo ideal sería hacer más niveles de juego, meter sonidos, imágenes, «power-up», … Todo lo que hace de un juego algo que te engancha.
Aquí tenéis el enlace de la demostración: VER DEMO
Algunas de las fuentes que he utilizado:
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.