Realizando tu primer juego en JavaScript – Segunda parte

Este artículo es la segunda parte de mi tutorial para hacer un pequeño juego con Javascript. Realizando tu primer juego en JavaScript – Primera parte.

En esta segunda parte vamos a poner en marcha el funcionamiento de nuestro juego, dándole mas animación, versatilidad y movimiento. Sin mas que decir, ¡Vamos a comenzar con esta segunda parte!

Vamos a ponernos al día

Como ya sabrás, tenemos mucho adelantado de este tutorial. Sigue el siguiente enlace y mira el código fuente completo de este tutorial . Absolutamente todo lo conseguirás siguiendo esa URL. Así que no esperes mas y continuemos con nuestro tutorial.

Creando nuestra increíble nave espacial

Ahora que el fondo está hecho, ¡por fin podemos empezar a armar nuestra nave espacial!

Vamos a crear un archivo Player.js y agregarlo a index.html como un script (igual que como hicimos con CloudManager).

Aquí está una (hermosa) nave espacial diseñada en pocos minutos, además que la puedes añadir a tu carpeta de assets y en la función loader de main.js:

Básicamente, queremos que esta nave aparezca cerca del borde izquierdo de la pantalla. También necesitamos que su ancla (punto de posición del objeto) esté en el centro del sprite, y como es quizás demasiado grande, podemos reescalarla como te muestro aquí abajo:


class Player
{
constructor()
{
this.sprite = new  PIXI.Sprite(PIXI.loader.resources["assets/spaceship.png"].texture);
this.sprite.interactive = true;

this.sprite.anchor.set(0.5, 0.5);
this.sprite.position.set(renderer.width * 0.2, renderer.height * 0.4);
this.sprite.scale.set(0.4, 0.4);

stage.addChild(this.sprite);
}
}

Por supuesto, tenemos que declarar este objeto como una variable global del archivo main.js, para que pueda ser instanciado en el juego:


var stage = new PIXI.Container();
var cloudManager;
var player;
...

… y en la función init del mismo archivo (justo después de CloudManager):


player = new Player();

Ahora deberías ver la brillante nave espacial navegando.

Sin embargo, si esperas unos segundos, deberías tener algunos problemas:

¡La nave espacial está volando detrás de las nubes!

Sí, porque de hecho, el último objeto creado se mueve detrás del anterior. Las nubes están desovando después de que la nave espacial haya sido instanciada, así que necesitamos que el gestor de nubes genere nubes en la parte inferior de la lista de objetos. Para ello, sólo tenemos que actualizar un poco el addChild de la clase CloudManager:


stage.addChildAt(esta.nube, 0);

addChildAt nos permite pasar un segundo parámetro, que es la posición en la lista de objetos del escenario. Cuanto más lejos esté, más lejos estará el sprite en la pantalla.

«0» significa el primer índice en la lista de objetos, así que cada vez que creamos una nueva nube, la añadimos en la primera posición de la lista, asegurando que aparecerá detrás de la nave espacial.

Ahora que está hecho, podemos iniciar los controles de la nave espacial.

Mover la nave espacial

Volvamos a la clase de Player.js y agreguemos algunas líneas en el constructor:


window.addEventListener('keydown', this.onKeyDown.bind(this));
window.addEventListener('keyup', this.onKeyUp.bind(this));

Esto permite que el juego detecte los eventos del teclado (cuando se pulsa y suelta una tecla). Cuando ocurre uno de estos eventos, se ejecuta el método al final de cada línea.

Nota: Al añadir «.bind(this)» al final de la función, pasamos el contexto del objeto a él, lo que significa que todavía podemos acceder a sus propiedades llamando » this «. Si no lo escribimos de esta manera, la función callback no es capaz de determinar con qué objeto estamos trabajando. Sólo recuerda que si una de tus funciones de devolución de llamada recibe un error «undefined this», es posible que tengas que atar tu objeto de esta manera.

Vamos a definir 2 métodos vacíos en la misma clase (los rellenaremos dentro de un rato):


onKeyDown(key) = {};
onKeyUp(key) = {};

También necesitamos 3 variables para mover la nave espacial: sus direcciones horizontal y vertical (X e Y) y la velocidad (todas ellas en el constructor):


this.directionX = 0;
this.directionY = 0;
this.speed = 8;


this.keyCodes = {37: -1, 38: -1, 39: 1, 40: 1};

Este objeto es básicamente una asociación de códigos clave y valores de dirección:

  • 37 es la tecla de flecha izquierda, para mover nuestra nave espacial hacia la izquierda necesitamos disminuir su posición en X.
  • 38 es la tecla de flecha arriba, para mover nuestra nave espacial a la parte superior necesitamos disminuir su posición en Y.
  • 39 es la tecla de flecha a la derecha, para mover nuestra nave espacial a la derecha necesitamos aumentar su posición en X.
  • 40 es la tecla de flecha abajo, para mover nuestra nave espacial al fondo necesitamos aumentar su posición en Y.

Cada vez que se pulsa una tecla, se recupera la dirección correspondiente:


onKeyDown(key)
{
if (key.keyCode == 37 || key.keyCode == 39)
this.directionX = this.keyCodes[key.keyCode];
else if (key.keyCode == 38 || key.keyCode == 40)
this.directionY = this.keyCodes[key.keyCode];
}

Se ve bastante raro, pero vamos a explicar eso: si el código de la tecla es 37 o 39 (que significa tecla de flecha izquierda o derecha), entonces simplemente fijamos la dirección de la X. Si es 38 o 40, puedes adivinar lo que está pasando (sí, en realidad es la dirección vertical). Eso es todo!

Vamos a añadir el método de actualización del reproductor (como el Cloud Manager, ¿recuerdas?):


update()
{
this.sprite.position.x += this.directionX * this.speed;
this.sprite.position.y += this.directionY * this.speed;
}

… y por supuesto, no olvides llamarlo en la función de bucle de main.js


player.update();

Si guardas y recargas el juego, (debería) funcionar bien, excepto que la nave sigue moviéndose cuando soltamos las teclas de dirección. Es obvio porque no hemos completado la función onKeyUp, que está capturando la clave que acaba de ser liberada. Así que de esta manera podemos detenerlo ajustando la directionX o directionY a 0.

Sólo hay un pequeño problema: ¿qué pasa si ponemos la dirección a 0 porque hemos soltado la tecla izquierda, pero la tecla derecha sigue pulsada?

Sí, la nave espacial se va a detener, y tendríamos que presionar la tecla derecha de nuevo para movernos, lo cual no es muy lógico.

Así que, ¿por qué no comprobar si la tecla derecha o izquierda sigue pulsada, para que podamos reajustar la dirección a esta tecla pulsada anteriormente?

Eso es lo que vamos a hacer. Y para ello necesitamos almacenar el estado de cada tecla en un booleano, pulsado o soltado (verdadero o falso). Pongamos todos estos estados en una variable de objeto en el constructor de reproductores:


this.keyState = {37: false, 38: false, 39: false, 40: false};

Si una tecla (identificada por su propio código) es presionada, entonces simplemente cambiamos su estado a true, y si es liberada la configuramos a false. Fácil, ¿verdad?

Añadamos esta línea al principio de onKeyDown:

this.keyState[key.keyCode] = true;

…y este en onKeyUp:


this.keyState[key.keyCode] = false;

Ahora podemos continuar con la lógica onKeyUp: si se suelta una de las teclas horizontales o verticales pero se sigue pulsando la opuesta, cambiamos la dirección a la última. Si ambos son liberados, detendremos la nave:


if (!this.keyState[37] && this.keyState[39])
this.directionX = this.keyCodes[39];
else if (this.keyState[37] && !this.keyState[39])
this.directionX = this.keyCodes[37];
else this.directionX = 0;

if (!this.keyState[38] && this.keyState[40])
this.directionY = this.keyCodes[40];
else if (this.keyState[38] && !this.keyState[40])
this.directionY = this.keyCodes[38];
else this.directionY = 0;

Nota: Hay múltiples maneras de manejar el movimiento de la nave espacial, esta ciertamente no es la más limpia, pero creo que es una de las más simples de entender.

Estamos listos, ¡Guardar y recargar y disfrutar!

Actualización: ¡La nave espacial puede salir de la pantalla! Intente comprobar su posición antes de actualizarla. Puede comprobar el resultado aquí.

Lanzamiento de cohetes

Ahora que podemos controlar la nave, queremos que dispare cohetes.

Comencemos por añadir el sprite en la carpeta de activos:

PIXI.loader.add([
"assets/cloud_1.png",
"assets/cloud_2.png",
"assets/spaceship.png",
"assets/rocket.png"
]).load(init);

Crea un nuevo archivo llamado Rocket.js en la carpeta src y añádelo en index.html con los otros tipos:


<script src="../src/lib/pixi.min.js"></script>
<script src="../src/Player.js"></script>
<script src="../src/CloudManager.js"></script>
<script src="../src/Rocket.js"></script>
<script src=“../src/main.js"></script>

Volvamos a la clase de Jugadores. Queremos que sea capaz de disparar un cohete si pulsa el botón de la barra espaciadora. La función onKeyDown ya está captando estos eventos, así que sólo necesitamos hacer una condición con el código de clave que queremos (barra espaciadora en este ejemplo):

onKeyDown(key)
{
this.keyState[key.keyCode] = true;

if (key.keyCode == 32) {
let rocket = new Rocket(this.sprite.position.x,
this.sprite.position.y);
}

if (key.keyCode == 37 || key.keyCode == 39)
this.directionX = this.keyCodes[key.keyCode];
else if (key.keyCode == 38 || key.keyCode == 40)
this.directionY = this.keyCodes[key.keyCode];
}

Estamos dando la posición de la nave espacial en el constructor de cohetes, así que en el interior nos limitamos a fijar su posición a la de la nave espacial (con un desplazamiento para que aparezca delante de la nave en lugar de en el centro).

Así que vamos, inicializa Rocket.js con el constructor:


class Rocket
{
constructor(x, y)
{
this.sprite = new     PIXI.Sprite(PIXI.loader.resources["assets/rocket.png"].texture);

this.sprite.anchor.set(0.5, 0.5);
this.sprite.position.set(x + 40, y);

stage.addChild(this.sprite);
}
}

Muy bien, si guardas y recargas, puedes disparar cohetes (estáticos) presionando la barra espaciadora!

Para que se muevan, necesitamos crear una variable de array que contenga todos los cohetes existentes en el juego (no sabemos cuántos de ellos están en el escenario):

let _list = new Array();

class Rocket
{
static get list() { return _list; }
static set list(value) { _list = value; }

…

La variable _list se encuentra fuera de la clase porque es estática, lo que significa que su valor es único y no es sólo la propiedad de un objeto (a diferencia de esto). Sin embargo, podemos conseguirlo y configurarlo como queramos (con las dos primeras líneas de la clase).

Podemos empujar el objeto actual dentro de esta lista (dentro del constructor) y declarar la variable de velocidad en el mismo tiempo:


this.speed = 20;
Rocket.list.push(this)

…. y también añadir el método de actualización:

update()
{
this.sprite.position.x += this.speed;

if (this.sprite.position.x > renderer.width * 1.1) {
this.sprite.destroy();
Rocket.list.splice(Rocket.list.indexOf(this), 1);
}
}

Esto es básicamente lo mismo que antes, actualizamos la posición x del cohete (y no la y porque no se mueve verticalmente) y al igual que las nubes, lo eliminamos cuando sale fuera de los límites de la pantalla, excepto que esta vez es el borde derecho.

Después de eso, en el bucle de main.js, sólo tenemos que analizar la lista de cohetes y llamar a la función de actualización para cada elemento:


function loop()
{
cloudManager.update();
player.update();

Rocket.list.map((element) =>
{
element.update();
});

requestAnimationFrame(loop);
renderer.render(stage);
}

¡Ahora guarda, recarga, y pruébalo!

Está disparando, pero no es automático. Tienes que presionar la tecla para cada cohete y cuando no te mueves, es un poco raro porque está disparando muy rápido. Lo que queremos es poder disparar automáticamente cuando se mantiene pulsada la tecla, con una velocidad ajustable.

Vamos a definir dos nuevas variables en el constructor de Jugadores: la velocidad de disparo (que puede ser modificada) y el Enfriamiento, que va a ser el valor del temporizador:

this.fireSpeed = 10;
this.fireCooldown = 0;

También necesitamos actualizar keyState para añadir la tecla de espacio, porque queremos saber si está pulsada o no:


this.keyState = {32: false, 37: false, 38: false, 39: false, 40: false};

Esta es la función que estamos usando para disparar (tenemos que llamarla en la actualización del reproductor):


updateFire()
{
if (this.fireCooldown < this.fireSpeed)
this.fireCooldown++;

if (this.keyState[32] && this.fireCooldown >=   this.fireSpeed)
{
let rocket = new Rocket(this.sprite.position.x,   this.sprite.position.y);
this.fireCooldown = 0;
}
}

Es bastante simple: simplemente incrementamos el temporizador de 0 a la velocidad de disparo que fijamos y si se pulsa la tecla y el temporizador ha alcanzado el valor, generamos un cohete y reajustamos el temporizador a 0.

Esta función se ejecuta permanentemente en el bucle de actualización del reproductor:


update()
{
this.sprite.position.x += this.directionX * this.speed;
this.sprite.position.y += this.directionY * this.speed;

this.updateFire();
}

¡Trabajo hecho! Si lo desea más rápido, sólo tiene que disminuir la velocidad de disparo (y aumentarla para una velocidad más lenta).