¡Buenas!

Y bienvenidos a esta cuarta parte de Creando un shooter en HTML5. Si no has leído las anteriores partes, te recomiendo que lo hagas: PARTE 1, PARTE 2, PARTE 3.

 

PARTE 4: Movimiento

En esta ocasión nos concentraremos en el movimiento del jugador por la escena utilizando el teclado.

 

CONTROL DE TECLADO

En JavaScript, la manera “nativa” de detectar cuando una tecla ha sido presionada es utilizando un event keypress o similares (keydown, keyup, etc.). Sin embargo, nuestro juego corre “step a step”, y utilizar callbacks a eventos para cada comprobación puede llegar a ser muy molesto, principalmente por los problemas que traería el “cambio” de ámbito del evento principal a dichos callbacks. Es por eso que necesitamos de una función que podamos ejecutar en cualquier step del juego, y nos permita comprobar si determinada tecla se ha presionado.

 

Esto realmente es muy fácil de hacer, sólo tenemos que utilizar un objeto que haga de “mapa” de teclas: Cuando se presiona una tecla, añadirla (o establecer su valor a true) al mapa, y cuando ésta misma se suelta, eliminarla (o establecer su valor a false) del mapa. Luego, usaríamos una pequeña función para comprobar si una tecla se está presionando, revisando dicho mapa.

 

Para ello, utilizaremos un objeto principal al que llamaremos keyboard. Este contendrá una propiedad (el objeto que hará de mapa) y dos métodos: Uno que inicie un par de eventos para activar o desactivar las teclas, y otra función para revisar si la tecla está en el mapa.

//esto iría en assets.js
//---------CONTROL DE TECLADO-----------
var keyboard = {
    map: {},
    start: function() {

        window.addEventListener('keydown', function(ev) {
            var key = ev.which || ev.keyCode;
            keyboard.map[key] = true;
        });

        window.addEventListener('keyup', function(ev) {
            var key = ev.which || ev.keyCode;
            keyboard.map[key] = false;
        });

    },
    check: function(key) {
        return keyboard.map[key] == true; 
    }
};

Después de esto, creemos un objeto constante que contenga el valor numérico de algunas teclas comunes del teclado, para ahorrarnos trabajo:

//también en assets.js
const vk = {
    left: 37,
    up: 38, 
    right: 39, 
    down: 40,
    enter: 13,
    space: 32,
    tab: 9
};

El valor numérico de una tecla de letra puede hallarse fácilmente con el método String.charCodeAt().

 

MOVIMIENTO DEL JUGADOR

Vamos al meollo: Comencemos a programar el movimiento del jugador.
(lo siguiente irá en game.js)

 

Lo primero que tenemos que hacer es iniciar el control del teclado, dentro del evento load del documento. No eliminaremos el código del capítulo anterior, trabajaremos sobre él.

keyboard.start();

Lo que sí reemplazaremos serán los eventos de la ya creada entidad objPlayer; los que programamos anteriormente eran sólo para probar el funcionamiento.

 

Ahora, definamos en el Proto del objJugador una propiedad speed que nos defina la velocidad a la que se moverá cualquier instancia de éste:

objPlayer.Proto.speed = 4;

Después, asignémosle aleatoriamente una subimagen a la instancia creada, mediante el evento Create:

objPlayer.Proto.event_create = function() {

    //Escogeremos una subimagen aleatoria al inicio.
    //Esta selección aleatoria debemos hacerla DENTRO del evento create, para que sea
    //independiente en cada instancia.
    this.image_index = Math.floor(8 * Math.random()); 

    //Un objeto con dos arreglos en donde se almacenará la lista de teclas presionadas.
    //Esto para evitar que el jugador se quede quieto cuando se presionan simultáneamente
    //dos teclas contrarias como izquierda y derecha, o arriba y abajo.
    //La propiadad 'x' almacena las teclas horizontales (izquierda y derecha), y la propiedad
    //'y' las verticales.
    this.key_list = {x: [], y:[]};
    //No hay diferencia entre declarar este objeto en el evento create de esta misma forma,
    //o declararlo usando Proto, por fuera del evento.
};

Ya explicaré mejor lo que hace este arreglo key_list.

 

Continuemos: vamos a programar el evento Step. En este evento controlaremos el movimiento con las teclas del jugador. Utilizaré un código un tanto largo, pero lo explicaré paso por paso más adelante:

objPlayer.Proto.event_step = function() {
    //Obtener las teclas que se están presionando
    var kleft  = keyboard.check(vk.left);
    var kright = keyboard.check(vk.right);
    var kup    = keyboard.check(vk.up);
    var kdown  = keyboard.check(vk.down);

    var sumx = kright - kleft; //Obtener dirección horizontal (1, -1 o 0)
    var sumy = kdown - kup; //Obtener dirección vertical (1, -1 o 0)

    //Si no hay movimiento horizontal PERO se están presionando ambas teclas
    if(!sumx && kleft && kright) {
        //Mover en la dirección contraria de la ÚLTIMA tecla presionada
        sumx = -this.key_list.x[this.key_list.x.length - 1];

        if(!sumx) {//Si aún así no hay movimiento (tal vez la lista todavía está vacía)
            sumx = [-1, 1][ Math.floor(Math.random() * 2) ]; //Mover en una dirección aleatoria
            this.key_list.x.push(sumx); //Añadir dicha dirección a la lista
        }
    }

    else if(kleft || kright) {
        //De lo contrario (si hay movimiento),
        if(this.key_list.x[this.key_list.x.length - 1] !== sumx)
            //Añadir la dirección horizontal (-1, 0 o 1) a la lista
            this.key_list.x.push(sumx);
    }
    else //De lo contrario (si no hay movimiento)
        this.key_list.x.splice(0, this.key_list.x.length); //Vaciar la lista para liberar memoria


    //Si no hay movimiento vertical PERO se están presionando ambas teclas
    if(!sumy && kup && kdown) {
        //Mover en la dirección contraria de la ÚLTIMA tecla presionada
        sumy = -this.key_list.y[this.key_list.y.length - 1];

        if(!sumy) {//Si aún así no hay movimiento (tal vez la lista todavía está vacía)
            sumy = [-1, 1][ Math.floor(Math.random() * 2) ]; //Mover en una dirección aleatoria
            this.key_list.y.push(sumy); //Añadir dicha dirección a la lista
        }
    }

    else if(kup || kdown) { //De lo contrario (si aún hay movimiento),
        if(this.key_list.y[this.key_list.y.length - 1] !== sumy)
            //Añadir la dirección vertical (-1, 0 o 1) a la lista
            this.key_list.y.push(sumy);
    }
    else //Si no hay movimiento
        this.key_list.y.splice(0, this.key_list.y.length);

    var angl = Math.atan2(sumy, sumx); //Obtener el ángulo del movimiento

    var spdx = this.speed * Math.cos(angl) * Math.abs(sumx);
    var spdy = this.speed * Math.sin(angl) * Math.abs(sumy);

    //Mover la instancia
    this.x += spdx;
    this.y += spdy;

    //Obtener la subimagen dependiendo del ángulo. Cada PI/4 es una subimagen más,
    //en sentido antihorario.
    var img = -4 * angl/Math.PI;
    img = img < 0 ? 8 + img : img;

    //Si el jugador se está moviento, cambiar su subimagen
    if(spdx || spdy)
        this.image_index = img;

    //Mantener al jugador siempre dentro de la pantalla.
    if(this.x < 0)
        this.x = Canvas.width;
    if(this.y < 0) this.y = Canvas.height; if(this.x > Canvas.width)
        this.x = 0;
    if(this.y > Canvas.height)
        this.y = 0;

};

Tal vez a la primera parezca un poco confuso el código del movimiento, pero vamos por partes:

 

La primera pieza de código es ésta:

    var kleft  = keyboard.check(vk.left);
    var kright = keyboard.check(vk.right);
    var kup    = keyboard.check(vk.up);
    var kdown  = keyboard.check(vk.down);

En realidad es la más sencilla de entender. Simplemente asignamos a cada una de las variables un booleano (true o false) que nos dice si cada tecla se está presionando.

 

Lo que hacemos después es tomar el valor de cada tecla como si fuera un vector, con una forma algo así:

Cada tecla representa uno de los cuatro vectores que están en la imagen anterior. En el plano de nuestra escena, el origen se encuentra en la ezquina superior izquierda del canvas, lo que significa que “hacia arriba” y “hacia la izquierda” es “más negativo”, mientras que “hacia abajo” y “hacia la derecha” es “más positivo”.

 

Tomando en cuenta eso, lo que haremos será sumar los vectores activos sumando cada una de sus componentes para obtener las componentes (horizontal y vertical) finales del vector resultante. Esto, por ejemplo, haría que si se presionan al tiempo izquierda y arriba, el jugador vaya en diagonal hacia esas direcciones. De ahí este código:

    var sumx = kright - kleft; //Obtener dirección horizontal (1, -1 o 0)
    var sumy = kdown - kup; //Obtener dirección vertical (1, -1 o 0)

Sin embargo, hay un pequeño gran problema con esto: Cualquiera de las dos componentes puede dar 0 en dos ocasiones distintas: Si no se está presionando ninguna de las teclas opuestas, o si ambas teclas opuestas se están presionando a la vez. Esto presenta una molestia de jugabilidad para el usuario, pues se vería obligado a soltar una tecla y presionar otra para poder mover al jugador.

 

Para solucionar esto, usaremos el objeto key_list que declaramos atrás: En key_list.x almacenaremos las teclas del movimiento horizontal (izq. y der.), y en key_list.y las del movimiento vertical.

 

Comprobaremos tres partes con cada componente (las mismas tres para cada una): Si se están presionando las teclas opuestas al tiempo, si se está presionando sólo una de ellas, o si no se está presionando ninguna.

 

Si se da el segundo caso (si se está presionando sólo una de ellas) añadiremos la componente del movimiento (sumx o sumy según corresponda) al arreglo que corresponda.

 

Si se da el tercer caso (es decir, que no se presiona ninguna) simplemente vaciaremos el arreglo que corresponda para liberar memoria.

 

El realmente importante es el primer caso, cuando se presionan las dos teclas opuestas simultáneamente. Este caso se subdivide en dos más: Cuando antes de presionar las dos teclas se estaba presionando una sola y cuando el jugador pasa inmediatamente de no presionar ninguna a presionar las dos al tiempo.

 

Si sucede lo primero, simplemente tomaremos la dirección en la que se estaba moviendo antes (mirando el arreglo), y moveremos al jugador en la dirección contraria.

 

Si sucede lo segundo, no tenemos forma de saber cuál era la tecla anterior (porque no la hay), así que moveremos aleatoriamente al jugador.

Todo esto es lo que hace esta pieza de código:

    //Si no hay movimiento horizontal PERO se están presionando ambas teclas
    if(!sumx && kleft && kright) {
        //Mover en la dirección contraria de la ÚLTIMA tecla presionada
        sumx = -this.key_list.x[this.key_list.x.length - 1];

        if(!sumx) {//Si aún así no hay movimiento (tal vez la lista todavía está vacía)
            sumx = [-1, 1][ Math.floor(Math.random() * 2) ]; //Mover en una dirección aleatoria
            this.key_list.x.push(sumx); //Añadir dicha dirección a la lista
        }
    }

    else if(kleft || kright) {
        //De lo contrario (si hay movimiento),
        if(this.key_list.x[this.key_list.x.length - 1] !== sumx)
            //Añadir la dirección horizontal (-1, 0 o 1) a la lista
            this.key_list.x.push(sumx);
    }
    else //De lo contrario (si no hay movimiento)
        this.key_list.x.splice(0, this.key_list.x.length); //Vaciar la lista para liberar memoria


    //Si no hay movimiento vertical PERO se están presionando ambas teclas
    if(!sumy && kup && kdown) {
        //Mover en la dirección contraria de la ÚLTIMA tecla presionada
        sumy = -this.key_list.y[this.key_list.y.length - 1];

        if(!sumy) {//Si aún así no hay movimiento (tal vez la lista todavía está vacía)
            sumy = [-1, 1][ Math.floor(Math.random() * 2) ]; //Mover en una dirección aleatoria
            this.key_list.y.push(sumy); //Añadir dicha dirección a la lista
        }
    }

    else if(kup || kdown) { //De lo contrario (si aún hay movimiento),
        if(this.key_list.y[this.key_list.y.length - 1] !== sumy)
            //Añadir la dirección vertical (-1, 0 o 1) a la lista
            this.key_list.y.push(sumy);
    }
    else //Si no hay movimiento
        this.key_list.y.splice(0, this.key_list.y.length);

Después de hacer esto, los componentes horizontal y vertical estarán correctamente ajustados, así que es momento de mover al jugador.

 

Para hacer esto, primeramente obtendremos el ángulo del vector resultante:

var angl = Math.atan2(sumy, sumx); //Obtener el ángulo del movimiento

Y después multiplicaremos este ángulo por la velocidad del jugador para obtener lo que debe desplazarse éste horizontal o verticalmente.

    var spdx = this.speed * Math.cos(angl) * Math.abs(sumx);
    var spdy = this.speed * Math.sin(angl) * Math.abs(sumy);

    //Mover la instancia
    this.x += spdx;
    this.y += spdy;

La razón por la que multipliqué spdx y spdy por el valor absoluto de los componentes horizontal y vertical, es para que el resultado final dé 0 (y la instancia no se mueva) si no se está presionando ninguna tecla.

 

El resto del código es fácil de entender, así que no pararé a explicarlo, principalmente porque ya me cansé de escribir :v xD.

 

Y POR ESO MISMO, ¡Esto fue todo por hoy!

 

CLIC AQUÍ para ver la parte 5.

 

Como siempre, dejo un enlace para que puedan ver el funcionamiento del código: CLICK AQUÍ.