En esta tercera parte, voy a integrar sonidos, música ambiente, animaciones y una interfaz de usuario.
Corrigiendo más bugs…
De nuevo, revisando la segunda parte, me di cuenta que las naves enemigas seguían chocando unas con otras, y este comportamiento no es el que quería implementar. En la segunda parte de esta serie de artículos, expliqué cómo utilizando la propiedad «skipCollide» las colisiones serían detectadas pero no serían tratadas, pero no es así. Las colisiones eran detectadas y sí eran tratadas. Las naves al colisionar no avanzaban más, pero no se paraban sino que adecuaban su velocidad a la nave que iba delante. Esto no era el comportamiento que yo pretendía implementar, lo que yo quiero es que las naves enemigas se ignoren mutuamente y sigan adelante adelantándose unas a otras si fuera necesario. Para eso, la propiedad correcta es la misma que he utilizado para el objeto «Bullet», es «sensor: true».
Sonidos y música
Lo primero que voy a añadir es el sonido y la música ambiente. Para eso, necesito añadir una nueva dependencia, el módulo «Audio». Este módulo activa el soporte Web Audio o el soporte para HTML5 Audio dependiendo del navegador en el que se ejecute el juego. He creado con Bfxr algunos efectos de sonido que utilizaré en mi juego, los he convertido a ogg, mp3 y wav para poder soportar un número amplio de navegadores. La música ambiente es obra de Isak Martinsson.
Lo más interesante de este añadido es la forma de reproducir el efecto de sonido de la explosión de una nave enemiga o la del propio jugador. En los objetos «Enemy» y «Player», he añadido el código para escuchar el evento «destroy» que es lanzado cuando estos objetos son destruidos. En el método «destroyed» reproduzco el sonido deseado.
Animaciones
Quizás no puedan llamarse animaciones pero estas dos imágenes para el objeto «Enemy» y otras dos para el objeto «Player», me han permitido probar el módulo de animaciones de Quintus, el módulo «Anim». En la definición de las animaciones, presta especial atención a la definición del «trigger». Yo pensaba de manera equivocada que lo que se definía en «trigger», era una función a la que había que llamar una vez ejecutada la animación pero no, se refiere al evento lanzado después de ejecutar la animación. Lo que hay que hacer entonces es escuchar ese evento y definir un callback que se va a ejecutar cada vez que se escuche este evento. Utilizando este «trigger», podré sacar provecho para contabilizar el número de naves enemigas destruidas por el jugador.
Q.State o propiedades globales al juego
Quintus permite almacenar propiedades globales a tu juego que serán accesibles desde cualquier parte de forma sencilla. Estas propiedades son destinadas a almacenar cosas tan comunes como la puntuación, las vidas restantes, … En el caso de mi juego, utilizaré «State» para almacenar cuatro propiedades:
- «enemimes_destroyed»: Contiene el número de naves enemigas destruidas por disparos del jugador.
- «enemies_missed»: Contiene el número de naves enemigas que han desaparecido debajo del jugador.
- «enemies_alive»: Contiene el número de naves enemigas que todavía están vivas.
- «total_enemies»: Contiene el número total de naves enemigas.
Interfaz de usuario
Finalmente, he creado una sencilla interfaz de usuario para que el jugador pueda ver cuantos enemigos siguen vivos y pendientes de aparecer, cuantos ha destruido con sus disparos y cuantos han desaparecido por debajo de él. Mi interfaz de usuario consta de un simple contenedor con tres labels:
- El primer label contendrá las naves enemigas destruidas por el jugador.
- El segundo label contendrá las naves enemigas que han desaparecido (y muerto) saliendo del canvas por debajo del jugador.
- El tercer label contendrá las naves enemigas que quedan todavía por destruir.
Me parece que en los comentarios del código del archivo quintus_ui.js hay una errata, pues en la definición del objeto «Container», se dice que las propiedades «x» e «y» se refieren a las coordenadas de la esquina superior izquierda del contenedor. Por lo que he podido probar, estas propiedades se refieren al centro del contenedor y no a esa posición. Si alguien sabe algo más del tema, por favor deja un comentario para aclararlo. Intentaré contactar con los creadores para comprobar si es una errata o estoy equivocado.
Actualización 24/03/2015: He contactado con ellos y efectivamente es un error.
El código completo y con comentarios por supuesto
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 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379 |
<!doctype html> <html> <head> <meta charset="UTF-8"> <title>¿Cómo hacer un juego Shoo'Em Up con Quintus? - Parte 3</title> <style> body { padding: 0; margin: 0; background: #fff; } canvas { background: #000; } </style> </head> <body> <!-- Cargando quintus y todos sus módulos --> <script src="js/quintus.js"></script> <script src="js/quintus_2d.js"></script> <script src="js/quintus_anim.js"></script> <script src="js/quintus_audio.js"></script> <script src="js/quintus_input.js"></script> <script src="js/quintus_scenes.js"></script> <script src="js/quintus_sprites.js"></script> <script src="js/quintus_tmx.js"></script> <script src="js/quintus_touch.js"></script> <!-- Se me olvidó incorporar el archivo quintus_ui.js. --> <script src="js/quintus_ui.js"></script> <script> window.addEventListener("load", function() { var Q = Quintus({ development: true }) /** * En la tercera parte, necesito los módulos Audio, Anim y UI. * El módulo UI requiere que el módulo Touch sea cargado también. */ .include("Sprites, Scenes, 2D, Input, Audio, Anim, UI, Touch") .setup({ width: 320, height: 480 }) .controls() /** * Esto activa el soporte para Web Audio o HTML5 Audio. * ¡El sonido está activado ahora! */ .enableSound(); Q.gravityY = 0; var SPRITE_PLAYER = 1; var SPRITE_BULLET = 2; var SPRITE_ENEMY = 3; Q.MovingSprite.extend("Player", { init: function(p) { this._super(p, { sheet: "player", sprite: "player", type: SPRITE_PLAYER, collisionMask: SPRITE_ENEMY, speed: 300 }); /** Añado el componente animation al objeto. */ this.add("animation"); /** Cuando el objeto Player es creado, la animación inicial es "alive". */ this.play("alive"); Q.input.on("fire", this, "shoot"); /** Escucho el evento "destroy" para poder reproducir el sonido de la explosión en el callback "destroyed". */ this.on("destroy"); }, step: function(dt) { var p = this.p; if (Q.inputs['left'] && (p.x - p.w/2) > 0) { p.vx = -p.speed; } else if (Q.inputs['right'] && (p.x + p.w/2) < Q.width) { p.vx = p.speed; } else { p.vx = 0; } p.x += p.vx * dt; }, shoot: function() { var p = this.p; this.stage.insert(new Q.Bullet({ x: p.x, y: p.y - p.w/2, vy: -200 })) }, destroyed: function() { // Reproduzco el sonido de la explosión. Q.audio.play("explosion.wav"); } }); Q.MovingSprite.extend("Enemy", { init: function(p) { this._super(p, { sheet: "enemy", sprite: "enemy", type: SPRITE_ENEMY, collisionMask: SPRITE_BULLET | SPRITE_PLAYER, /** * Aquí cometí un error, "skipCollide" hace que los objetos "Enemy" * sigan colisionando entre ellos, necesito usar "sensor", así los * objetos "Enemy" se ignorarán mutuamente. */ sensor: true }); /** Añado el componente animación al objeto "Enemy". */ this.add("2d, animation"); /** La primera animación es "alive". */ this.play("alive"); this.on("hit"); /** Escucho el evento "destroy" también aquí. */ this.on("destroy"); /** Escucho el evento lanzado por la animación "dead" cuando esta termina. */ this.on("updatedestroyed"); }, /** * Cuando los enemigos están fuera del canvas, por abajo, quiero que mueran. */ step: function(dt) { if (this.p.y > Q.height) { this.updatemissed(); } }, hit: function(col) { if (col.obj.isA("Player")) { // Esta vez, el objeto "Player" también muere en caso de colisión. col.obj.play("dead"); // En vez de llamar al método "destroy", ejecuto la animación "dead". // El método "destroy" será llamado desde el callback. this.play("dead"); } else if (col.obj.isA("Bullet")) { // Aquí también reproduzco la animación "dead" destruyendo el objeto "Enemy". this.play("dead"); col.obj.destroy(); } }, /** * Esta función actualiza la propiedad "enemies_destroyed" y hace una llamada al método "destroy". */ updatedestroyed: function() { // Actualizo la propiedad "enemies_destroyed" incrementándola en una unidad. Q.state.inc('enemies_destroyed', 1); // Después ¡muere! this.destroy(); }, /** * Esta función actualiza la propiedad "enemies_missed" y hace una llamada al método "destroy". */ updatemissed: function() { // Actualizo la propiedad "enemies_missed" incrementándola en una unidad. Q.state.inc('enemies_missed', 1); // Después ¡muere! this.destroy(); }, /** * Esta función es llamado cada vez que el método "destroy" es ejecutado y destruido. */ destroyed: function() { // Reproduzco el sonido de la explosión. Q.audio.play("explosion.wav"); // Actualizo la propiedad "enemies_alive" decrementándola en una unidad. Q.state.dec("enemies_alive", 1); } }); Q.MovingSprite.extend("Bullet", { init: function(p) { this._super(p, { sheet: "bullet", sprite: "bullet", type: SPRITE_BULLET, collisionMask: SPRITE_ENEMY, sensor: true }); this.add("2d"); // Reproduzco el sonido del disparo cada vez que un nuevo objeto "Bullet" es creado. Q.audio.play("shoot.wav"); }, step: function(dt) { if (this.p.y < 0) { this.destroy(); } } }) Q.scene("level1", function(stage) { /** * Necesito cambiar la posición del jugador por culpa de * la nueva interfaz de usuario que he creado. */ var player = stage.insert(new Q.Player({ x: Q.width/2, y: Q.height - 70 })); var num_enemies = Math.floor(Math.random() * 10 + 30); var enemies = new Array(num_enemies); var minX = 30; var maxX = Q.width - 30; for (var i=0; i < num_enemies; i++) { enemies.push(stage.insert(new Q.Enemy({ x: Math.floor(Math.random() * (maxX - minX + 1)) + minX, y: -(Math.random() * 50) - (100*i), vy: Math.random() * 75 + 100 }))); } /** * Empieza el bucle de música ambiente. */ Q.audio.play("music.wav", { loop: true }); /** * En Q.State guardaré las propiedades globales de mi juego como: * - total_enemies: Número total de enemigos. * - enemies_destroyed: Número de enemigos destruidos por el jugador. * - enemies_missed: Número de enemigos que no ha destruido el jugador pero que están muerto. * - enemies_alive: Número de enemigos vivos todavía. */ Q.state.reset({ total_enemies: num_enemies, enemies_destroyed: 0, enemies_missed: 0, enemies_alive: num_enemies }); }); /** * Voy a extender el objeto "Text" de "UI" para crear el label "EnemiesDestroyed". * Las propiedades por defecto deben de ser el label inicial, el color y el tamaño. */ Q.UI.Text.extend("EnemiesDestroyed", { init: function(p) { this._super(p, { label: "Destroyed: " + Q.state.get("enemies_destroyed"), color: "white", size: "14" }); /** Necesito extender porque quiero escuchar los cambios de la variable en el "State". */ Q.state.on("change.enemies_destroyed", this, "update_label"); }, /** * Con esta función actualizo el label. */ update_label: function(enemies_destroyed) { this.p.label = "Destroyed: " + enemies_destroyed; } }); /** * Lo mismo que antes pero con el label "EnemiesMissed". */ Q.UI.Text.extend("EnemiesMissed", { init: function(p) { this._super(p, { label: "Missed: " + Q.state.get("enemies_missed"), color: "white", size: "14" }); /** Necesito extender porque quiero escuchar los cambios de la variable en el "State". */ Q.state.on("change.enemies_missed", this, "update_label"); }, /** * Con esta función actualizo el label. */ update_label: function(enemies_missed) { this.p.label = "Missed: " + enemies_missed; } }); /** * Lo mismo que antes pero con el label "EnemiesAlive". */ Q.UI.Text.extend("EnemiesAlive", { init: function(p) { this._super(p, { label: "Still alive: " + Q.state.get("enemies_alive") + " / " + Q.state.get("total_enemies"), color: "white", size: "14" }); /** Aquí escuho los cambios de "enemies_alive". */ Q.state.on("change.enemies_alive", this, "update_label"); }, /** Actualizo el label. */ update_label: function(enemies_alive) { this.p.label = "Still alive: " + enemies_alive + " / " + Q.state.get("total_enemies"); } }); /** * Esta escena es para la interfaz de usuario sólo. */ Q.scene("hud", function(stage) { /** Primero, voy a crear un "Container" que contendrá los labels. */ var container = stage.insert(new Q.UI.Container({ x: Q.width/2, y: Q.height - 25, w: Q.width, h: 50, fill: "red", radius: 0 })); /** Ahora voy a insertar los tres labels uno encima de otro. */ container.insert(new Q.EnemiesDestroyed({ x: -container.p.x/2, y: -20, })); container.insert(new Q.EnemiesMissed({ x: 75, y: -20 })); container.insert(new Q.EnemiesAlive({ x: 0, y: 0, })); }); /** * En vez de utilizar una cadena, voy a utilizar ahora un array de cadenas. * Las dos formas de hacerlo son válidas. */ Q.load([ "sprites.png", "sprites.json", /** Cargando los sonidos cuando disparas. */ "shoot.wav", "shoot.ogg", "shoot.mp3", /** Cargando los sonidos cuando un enemigo o el jugador mueren. */ "explosion.wav", "explosion.ogg", "explosion.mp3", /** Cargando la música que se reproduce de fondo. */ "music.wav", "music.ogg", "music.mp3" ], function() { Q.compileSheets("sprites.png", "sprites.json"); /** * Voy a definir la animación de un enemigo. Defino las dos animaciones "alive" y "dead". * "alive" es el frame de la posición 0, rate: 1/3 y no quiero que se reproduzca en bucle. * Sólo hay un frame por lo que no tendría sentido. * "dead" es el frame de la posición 1, rate 1/4 y no quiero que se reproduzca en bucle tampoco. * También quiero que el evento "updatedestroyed" sea lanzado cuando la animación haya finalizado, * entonces el enemigo será destruido después de la animación. */ Q.animations("enemy", { alive: { frames: [0], rate: 1/3, loop: false }, dead: { frames: [1], rate: 1/4, loop: false, trigger: "updatedestroyed" } }); /** * Ahora la animación del jugador. Es lo mismo pero lanzando el evento "destroy". */ Q.animations("player", { alive: { frames: [0], rate: 1/3, loop: false }, dead: { frames: [1], rate: 1/4, loop: false, trigger: "destroy" } }); Q.stageScene("level1"); Q.stageScene("hud", 3, Q('Player').first().p); }); }); </script> </body> </html> |
No, esto no es el final. Aún puedo hacer varias cosas y esto es lo que me reservo para la cuarta parte de este tutorial:
- Menú de inicio con botón Jugar, Créditos.
- Hacer un game over y volver al menú de inicio.
- Pausar y salir de la pausa del menú.
- Intentaré crear dos o tres niveles.
- Si tengo tiempo, probaré a crear power ups, quizás añadir otro tipo de enemigo.
Este artículo forma parte de una serie:
- ¿Cómo hacer un juego Shoo’Em Up con Quintus? – Parte 1
- ¿Cómo hacer un juego Shoo’Em Up con Quintus? – Parte 2
- ¿Cómo hacer un juego Shoo’Em Up con Quintus? – Parte 3
Las imágenes utilizadas son de SpriteLib. Los efectos de sonidos fueron creados con bfxr y eres libre de reutilizarlos si quieres. La música ambiente es obra de Isak Martinsson.