Uso práctico de vectores: Pong (y un poco de sonido)

Escrito por el 10 Abril, 2013

Tengo pendientes de realizar tutoriales sobre un juego estilo shooter en 2D y otro de carreras de coches, pero ví que hacía un intensivo uso de vectores en ellos, asi que rebusqué entre los muchos experimentos que tengo hechos desde hace tiempo buscando algún minijuego con el que poder hacer una introducción al mundo de los vectores, y lo encontré: Un Pong sencillito. Y éste es el resultado:
pong

Aquí teneis un enlace para poder probarlo online, y aquí está el zip con todo el código necesario para descargarlo y jugar en local. Las palas se mueven con W y S, y los cursores.

Con éste famoso minijuego aprenderemos a usar vectores (matemáticos) orientados a los videojuegos, y su increíble utilidad para posicionar, rotar y mover entidades por el mundo, entre otras muchas funciones. Y ya que estamos, veremos cómo meter sonidos en nuestros juegos javascript usando el nuevo elemento AUDIO de HTML5. Y por si mis queridos lectores no tuvieran hermanos ni amigos con los que jugar, también incluiremos una inteligencia artificial tan estúpida que podría llegar fácilmente a presidente del gobierno.

Una introducción a los vectores

¿Qué es un vector? Para el que no lo sepa (arderás en el infierno), un vector es una herramienta geométrica que usan los físicos para representar magnitudes de fuerzas en un eje de coordenadas de cualquier dimensión. La dimensión de un eje de coordenadas es el número de coordenadas que se necesitan para representar un punto en dicho eje. Por ejemplo, en un eje de coordenadas 2D se suele utilizar X e Y para representar cada una de las dimensiones. En un eje 3D, se usa X, Y y Z. Pero también hay ejes de coordenadas de 4, 5, 6 , infinitas dimensiones. Un vector es un conjunto de valores donde cada componente representa a uno de los ejes del sistema de coordenadas donde se utiliza. En dos dimensiones, el vector tendrá dos componentes (uno para X y otro para Y), y en tres dimensiones, 3, y así sucesívamente. Lógicamente, para los videojuegos se utilizan las de 2 o 3 dimensiones. Como nuestro juego será 2D, hablaremos de ejes de coordenadas y vectores de dos dimensiones.

grid1Asi que básicamente un vector es una línea que va desde el orígen de coordenadas (0,0) a cualquiera de los infinitos puntos contenidos en un eje de coordenadas. Por ejemplo, el vector V(8,6) se representa como la imagen que tenemos a la derecha. La línea roja es el vector en sí. Su componente X, que también se le conoce como Vx, es 8, y su Vy es 6.
El módulo de un vector es la longitud que tiene la línea roja, asi que si conoces el teorema de pitágoras, sabrás que su valor corresponde a la raíz cuadrada de la suma de sus componentes al cuadrado.

Sobre los vectores se pueden calcular un montón de operaciones matemáticas, como sumas, restas, multiplicaciones, etc, pero las dejaremos para un futuro tutorial íntegramente dedicado a los vectores, que será tremendamente aburrido. Es una promesa.

Y ahora mira la imagen del vector y piensa. ¿Para qué podemos usar vectores en nuestros juegos?

Por ejemplo, para posicionar elementos. Sí, las entidades a dibujar hasta ahora les asociábamos un par de atributos, normalmente X e Y para indicar sus posiciones en el eje de coordenadas que usa el juego. Y un vector 2D está definido precisamente por una X y una Y. ¿Podríamos usar un vector para almacenar las posiciones de las entidades? Por supuesto, eso es lo que haremos con nuestro Pong. ¿Alguna cosa más? Pues sí, como verás, el vector es una flecha, y las flechas son estupendas para indicar direcciones y longitudes. Cojonudo para manejar la dirección y cantidad de movimiento que tiene la pelota en cada vuelta del bucle.

Un vector se puede representar como hemos visto, indicando un valor para cada uno de los componentes. Pero si te fijas en la imagen, sabiendo el valor del módulo, y el ángulo que forma sobre el eje X (o Y) se puede obtener el valor de sus componentes mediante operaciones trigonométricas sencillas (módulo por el coseno uno, módulo por el seno el otro). Así que podemos rotar vectores indicándoles ángulos, por ejemplo, en un juego de coches en los que representamos su dirección con un vector, para girar el volante a derecha o izquierda.

¿Y qué ganamos con ello? En este tutorial, no mucho, ya que todo lo podríamos haber programado manejando X e Y por separado, como hemos hecho hasta ahora; aunque el manejo de la bola se hubiera hecho un poco más difícil. Pero en futuros tutoriales se podrá ver que realizando ciertas operaciones con los vectores, se pueden extraer datos muy útiles. Por ejemplo, si tenemos dos vectores diferentes que representan la posición de dos objetos distintos, la resta de esos vectores nos dará automáticamente la distancia y la dirección que hay entre dichos objetos. Tremendamente útil.

Veremos más operaciones sobre los vectores sobre la marcha.

Eventos de teclado

Hasta ahora, a excepción de el tercer tutorial de Three.JS, los eventos de teclado los solía meter en la clase juego, metiendo en todos ellos un código repetitivo que no tenía mucho que ver realmente con la clase principal del juego. Ahora he decidido sacarlo a un archivo aparte. El código es muy sencillo y si has seguido mis tutoriales, no te costará entenderlo:

var Direccion = { QUIETO:0, ARRIBA: 1, ABAJO:2 };
function Controles(){    
    this.ARRIBA=false;
    this.ABAJO=false;
    this.W=false;
    this.S=false;

    var self=this;
    document.body.onkeydown=function(e){
        switch(e.keyCode)
        {
            case 38: //Arriba
                e.preventDefault();
                self.ARRIBA=true;
                break;
            case 40: //Abajo
                e.preventDefault();
                self.ABAJO=true;
                break;
            case 87: //W
                e.preventDefault();
                self.W=true;
                break;
            case 83: //S
                e.preventDefault();
                self.S=true;
                break;
           
        }
    };
    document.body.onkeyup=function(e){
        switch(e.keyCode)
        {
            case 38: //Arriba
                e.preventDefault();
                self.ARRIBA=false;
                break;
            case 40: //Abajo
                e.preventDefault();
                self.ABAJO=false;
                break;
            case 87: //W
                e.preventDefault();
                self.W=false;
                break;
            case 83: //S
                e.preventDefault();
                self.S=false;
                break;
           
        }
    };
}

Los movimientos que pueden tener las palas son arriba, abajo, o dejarlas quietas. Me interesa decirle al juego de alguna manera, qué tipo de movimiento tiene que hacer la pala en la siguiente vuelta del bucle, así que creo un objeto javascript Direccion, que lo usaré como si fuera un enumerador, facilitando así la lectura del código en las clases que necesiten que les indique una dirección válida. En vez de pasarles un 1, que no nos dice nada, les pasaré un Dirección.ARRIBA, mucho más descriptivo.

Justo debajo está el nuevo objeto Controles donde como su nombre indica, controlaré las pulsaciones de todas las teclas que me interese controlar. El jugador 1 usará la W y la S para mover la pala, y el jugador 2, los cursores.

La primera implementación de la clase Vector2D

Antes de empezar a programar, me surgió una duda. ¿Las operaciones de los vectores que harán? ¿Devolver un nuevo vector con la solución de la operación, o aplicará la operación al vector que ejecuta el método, cambiándole los valores de sus componentes? Yo opté por devolver nuevos vectores; el vector que llama a las operaciones nunca se actualiza. Ésto tiene como ventaja que puedo realizar operaciones sobre los vectores para “experimentar” con sus resultados, sin modificar el vector original. Si lo que quiero es actualizar de verdad el vector, solo tengo que hacer un v=v.operacion();

Como desventaja, esta solución usará más memoria RAM para ejecutarse, porque cuando haga la asignación que puse antes, el objeto vector V original se quedará en memoria sin que ninguna variable le apunte a él, hasta que el recolector de basura de javascript libere ese trozo de memoria.

Hay otra tercera solución, que es meter dos versiones de la misma operación: Una actualiza el vector que llama a la operación y no devuelve nada, y la otra, devuelve un nuevo vector con la operación realizada sobre él.

Todo depende de lo que necesite tu juego. Yo he optado por devolver nuevos vectores, pero tú puedes elegir el modo que te resulte más útil.

El constructor del vector es trivial. Necesita dos argumentos, los valores de los componentes X e Y.

function Vector2D(x,y){
    this.x=x;
    this.y=y;
}

Como dijimos antes, a un componente se le puede calcular su módulo, también conocido como longitud, con pitágoras. Nótese que no uso Math.pow para elevar al cuadrado cada componente. Está comprobado que hacer una multiplicación X * X es mucho más rápido que Math.pow(x,2).

Vector2D.prototype.longitud=function(){
    return (Math.sqrt(this.x*this.x+this.y*this.y));
};

Un vector se puede multiplicar por un escalar, y un número escalar es sólo un número real. Es decir, es multiplicar un vector por un número cualquiera, y se hace multiplicando por separado cada componente del vector por ese número escalar.

Vector2D.prototype.escalar=function(e){
    return (new Vector2D(this.x*e,this.y*e));
};

La suma de vectores no es mas que sumar cada componente con el equivalente del otro vector.

Vector2D.prototype.sumar=function(v){
    return (new Vector2D(this.x+v.x,this.y+v.y));
};

Ahora viene un método del que todavía no hemos hablado: Normalizar un vector. ¿Qué es esta operación? Pues es hacer que el módulo de un vector valga 1. ¿Y para qué sirve eso? Pues es realmente útil, por ejemplo, para mover las entidades si tenemos su dirección en un vector normalizado, y sabemos la distancia que pueden moverse en línea recta. Si multiplicamos un vector normalizado por dicha distancia, haremos que el vector tenga el módulo igual a la distancia, mediante la multiplicación escalar. Asi que si sumamos ese vector a otro vector que representa una posición, ¡lo moverá en la dirección indicada a la distancia exacta que queramos!

Normalizar un vector es dividir cada una de sus componentes por el módulo del vector. ¡Pero no tenemos un método para dividir vectores por un escalar! Bueno, todos sabemos, o deberíamos si no somos unos ceporros, que X/M es lo mismo que X* 1/M, que significa multiplicar X por la inversa de M.

No haría falta decirlo, pero ¡cuidado con las divisiones por cero!

Vector2D.prototype.normalizar=function(){
    var vec;
    var lon=this.longitud();
    if (lon!=0)
    {
        vec=this.escalar(1/lon);
    }
    else
    {
        vec=new Vector2D(0,0);
    }
    return vec;
};

Y ahora una de los métodos más útiles, el que nos servirá para rotar los vectores sobre su origen (0,0). Las operaciones trigonométricas de JavaScript funcionan con radianes, pero por comodidad, nos interesa usar grados, asi que hacemos una conversión previa. Además, al resultado le hacemos un redondeo a dos decimales.

Vector2D.prototype.rotar=function(angulo){
    var radianes=angulo*Math.PI/180;
    var x=this.x*Math.cos(radianes)-this.y*Math.sin(radianes);
    var y=this.x*Math.sin(radianes)+this.y*Math.cos(radianes);
    x=Math.round(x*100);
    if (x!=0)
    {
        x/=100;
    }
    y=Math.round(y*100);
    if (y!=0)
    {
        y/=100;
    }
    return new Vector2D(x,y);
};

También podemos obtener el ángulo que forma el vector con el eje X, usando la operación Math.atan2 de javascript (recuerda que devuelve radianes, por lo que me interesa e hacerle una sencilla conversión a grados, con los que trabajo más cómodo).

Vector2D.prototype.getAngulo=function(){
    return Math.atan2(this.y,this.x)*180/Math.PI;
};

También necesitaremos el típico método que clona el array (devuelve una copia exacta del array).

Vector2D.prototype.clonar=function(){
    return new Vector2D(this.x,this.y);
};

Por último, he incluído una operación que sólo nos vale para el PONG, y que no tendría sentido en otros juegos. Es un método que nos devuelve el ángulo que forma el vector sobre el eje X, pero siempre expresado en valores entre -90 y 90, ambos inclusive. El otro método devuelve valores entre -180 y 180. Ya veremos porqué queremos el ángulo así.

Vector2D.prototype.getAnguloRelativo=function(){
    var angulo = this.getAngulo();
    if (angulo>=180) angulo-=180;
    if (angulo>90) angulo=180-angulo;
    if (angulo<=-180) angulo+=180;
    if (angulo<-90) angulo=-180-angulo;
    return angulo;
};

Primer uso de los vectores: Las posiciones de las palas

En anteriores tutoriales usábamos atributos X e Y por separado para almacenar las posiciones de las entidades. Ahora veremos que usar un vector es un poco más cómodo. En el juego tenemos dos palas, que tienen forma de rectángulos. Cada una sólo puede moverse hacia arriba o hacia abajo. De cada pala nos interesa saber su posición, su anchura, su altura, y la dimensión vertical que tiene el mundo para conocer cuándo estamos llegando a sus límites, para no mover la pala fuera del mundo.

function Pala(x,mundoAlto){
    this.mundoAlto=mundoAlto;
    this.ancho=20;
    this.alto=80;

    this.pos=new Vector2D(x,mundoAlto/2);

    this.velocidad=200;
}

Como queremos que la pala comience justo en el medio, la coordenada Y no la necesitamos. Sabiendo cuánto mide el alto del mundo, es trivial obtener la posición central. Notar que la posición de la pala está justo en su centro, no en su vértice superior derecho como en otros tutoriales.

A continuación toca mover la pala, con un sencillo código que te tiene que resultar familiar. A partir del tiempo delta y la velocidad de la pala, obtenemos la distancia que tiene que moverse según la dirección indicada (aquí puedes ver uno de los usos del enumerador Direccion que ayuda a la legibilidad del código).

Pala.prototype.mover=function(delta,direccion){
    var distancia=Math.round(delta*this.velocidad);
    switch(direccion)
    {
        case Direccion.ARRIBA:
            this.pos.y-=distancia;
            this.pos.y=Math.max(this.alto/2,this.pos.y);
            break;
        case Direccion.ABAJO:
            this.pos.y+=distancia;
            this.pos.y=Math.min(this.mundoAlto-this.alto/2,this.pos.y);
            break;
    }
};

Y por último, el método que dibuja el rectángulo en el contexto dado, vuelvo a repetir, sabiendo que el vector posición apunta al centro de la pala en vez de a uno de sus vértices.

Pala.prototype.dibujar=function(contexto){
    contexto.fillStyle="white";
    contexto.fillRect(this.pos.x-this.ancho/2,this.pos.y-this.alto/2,this.ancho,this.alto);
};

Y con ésto, ya tenemos las palas terminadas, veámos ahora la pelota, donde se hará un uso más provechoso de los vectores. En la pelota se usan sonidos, asi que haremos una breve introducción antes.

Sonidos en JavaScript

HTML5 trae una nuevo elemento HTML, la etiqueta

Para reproducir un sonido de la forma más sencilla posible, sólo necesitamos una cosa: Un objeto del tipo AUDIO. Como con los objetos tipo IMAGE, a un AUDIO le asociamos un SRC con el archivo OGG, y javascript lo cargará. Cuando termine de cargarlo, se llamará a su onload, y así sabremos que el sonido está disponible para reproducirse.

Para reproducirlo, tenemos que ejecutar su método play(). Y para pararlo, su stop(). El objeto AUDIO tiene muchos más atributos y métodos, puedes verlos aquí, pero para este minijuego no necesitamos más.

Cuando tenemos muchos sonidos, conviene hacer un almacén de sonidos que se encargue de cargarlos todos antes de arrancar el juego, como hicimos con nuestro almacén de imágenes que vimos en el tutorial de tiles. Pero como en el pong sólo tenemos tres sonidos, y cada uno tiene muy poco peso, he optado por una solución mucho más directa.

Voy a extraer la codificación base64 de cada sonido OGG y ponersela directamente al SRC, en vez de una ruta URL al archivo. Así no hay que cargar nada, el sonido estará metido dentro del javascript. Las imágenes se pueden cargar con un src=”ruta del archivo en un servidor web”, pero también con un src=”data:image/jpg;base64,AJHFAKSHFASKJHFJAHSFH…” que es la imagen directamente codificada en un lenguaje que sirve para enviar datos por internet, ya que crea una cadena que es perfectamente posible enviarla a través de la URL (no mete símbolos raros).

Asi que ya podemos ver el javascript donde están contendidos los sonidos:

var SONIDOS=
{
   "beep"   : new Audio("data:audio/ogg;base64,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"),
   "gol"    : new Audio("data:audio/ogg;base64,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"),
   "plop"   : new Audio("data:audio/ogg;base64,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")
};

Es un pequeño objeto con tres atributos, que son objetos del tipo audio de cada sonido. Luego, cuando quiera reproducir un sonido, tengo que llamar a SONIDOS.beep.play() y el sonido se escuchará por los altavoces.

Pero ¿cómo he sido capaz de obtener el base64 de cada archivo de sonido? Pues con un PHP que procesaba cada sonido y como salida me escribía el javascript que necesito:

<?php
Header("content-type: application/x-javascript");
function data_uri($file)
{
  $contents = file_get_contents($file);
  $base64   = base64_encode($contents);
  return ('data:audio/ogg;base64,' . $base64);
}
?>
var sonidos =
{
   "beep"   : new Audio("<?php echo data_uri('beep.ogg');?>"),
   "gol     : new Audio("<?php echo data_uri('gol.ogg');?>"),
   "plop"   : new Audio("<?php echo data_uri('plop.ogg');?>")
};

Una pequeña función abre al archivo ogg indicado, lo codifica en base64, y me devuelve la cadena resultante, la cual yo la imprimo dentro de cada objeto Audio de mi objeto SONIDO de javascript. Es evidente que este PHP y los archivos de sonido estaban en la misma carpeta, viendo que no les he puesto ruta ninguna. Luego, en el juego, NO SE NECESITAN. Es decir, una vez que tengas creado tu javascript de los SONIDOS, los archivos de audio y este PHP puedes borrarlos porque no se van a usar más. Sólo son una herramienta para el programador, espero que quede bien claro.

Segundo uso de los vectores: La dirección de la pelota

La pelota es la entidad que tiene el movimiento libre por todo el escenario. La pelota puede rebotar con las paredes superior e inferior del escenario, pero no con los laterales. Si alcanza alguno de los laterales, será gol en favor del jugador que está en el lado opuesto. Para evitar los goles, cada jugador tiene una pala. La pelota puede rebotar con las palas. Además, para dar mayor dificultad al juego (y mejorar sensiblemente su jugabilidad), la pelota incrementará su velocidad cada vez que rebote con una pala, y también cambiará su dirección según con la zona de la pala que impacte. Es decir, cuanto más cerca le demos a la pelota con una esquina de la pala, más cambiará la dirección de la pelota hacia esa dirección. Por ejemplo, si la pelota viene de arriba a abajo y le damos con la parte inferior de la pala, la pelota rebotará con un ángulo aún más inclinado. Pero si le damos con la parte superior, la pelota saldrá mas recta (paralela al eje X). Con éstos cambios, además de aumentar la diversión, evitamos que las partidas sean repetitivas.

Podíamos haber echo la pelota como una circunferencia, y haber utilizado el detector de colisiones entre rectángulos y círculos, pero he querido ser fiel al pong original, y usaremos un cuadrado, lo que además hará mucho más sencilla y rápida la detección de colisiones con el algoritmo que ya usamos en el Space Invaders. Entremos en materia.

La pelota necesitará un vector que apunte a su posición actual, otro que apunte a su posición de inicio (realmente no es necesario, ya que podemos saber donde está en el centro del mundo cuando queramos), las medidas de mundo para saber cuándo alcanzamos sus límites, la velocidad actual y la velocidad inicial (cuando empieza una partida) de la pelota, el incremento de velocidad que queremos meterle a la pelota cuando choca con una pala, y otro vector donde almacenar la dirección y cantidad de movimiento que tiene que realizar la pelota en cada vuelta del bucle. Además, para evitar que la pelota tenga un ángulo demasiado paralelo al eje Y, que haría que la partida fuera horrible, también tenemos un atributo con el ángulo máximo absoluto que puede tener la pelota con respecto al eje X.

function Pelota(mundoAncho,mundoAlto,pong){
    this.mundoAncho=mundoAncho;
    this.mundoAlto=mundoAlto;
    this.posIni=new Vector2D(mundoAncho/2, mundoAlto/2);
    this.pos;
    this.pong=pong;

    this.lado=15;
    this.velocidad=300;
    this.velocidadIni=300;
    this.incrementoVelocidad=10;
    this.maxAngulo=60;
    this.velocidad=this.velocidadIni;
    this.vector;
    this.sacar(-1);
}

El constructor no tiene nada. Asignamos valores por defecto a varios de sus atributos, y llamamos al método sacar que iniciará una partida colocando la pelota en el centro del mapa dándole una dirección inicial y una velocidad inicial. Si la dirección indicada en el argumento es negativa, sacará hacia la izquierda. Si es positiva, hacia la derecha. Esto nos servirá también para hacer que después de un gol, saque el jugador que lo ha metido.

Con lo dicho, fijaos cómo creamos el vector2D para indicar la dirección. Como el saque es paralelo al eje X, el componente Y del vector siempre es cero. Notar que la longitud o módulo del vector SIEMPRE corresponde con la velocidad de la pelota.

Pelota.prototype.sacar=function(direccion){
    this.pos=this.posIni.clonar();
    this.velocidad=this.velocidadIni;
    this.vector=new Vector2D(direccion*this.velocidad,0);
};

El método que incrementa la velocidad de la pelota tampoco tiene mucho. Al cambiar la velocidad, hay que hacer que la longitud del vector corresponda con la nueva velocidad. ¿Cómo se hace eso? Muy sencillo. Se coge el vector dirección, se normaliza para que su módulo sea 1, y se le multiplica por el valor que queramos que tenga su módulo. Así obtenemos un nuevo vector que apunta exactamente en la misma dirección que antes, pero ahora es más largo.

Pelota.prototype.incrementarVelocidad=function(){
        this.velocidad+=this.incrementoVelocidad;
        this.vector=this.vector.normalizar().escalar(this.velocidad);
};

Ésto es la esencia del movimiento de la pelota. El atributo vector contiene dos informaciones muy útiles: La dirección en la que tiene que moverse (el ángulo que forma con el eje X), y la cantidad de distancia que debe moverse (el módulo del vector). Pero aplicarlos es muy sencillo, ¡sólo tenemos que sumar el vector posición y el vector dirección para mover la pelota en la dirección deseada y la distancia deseada! Veámos el método mover.

Pelota.prototype.mover=function(delta){
    var mitad= this.lado/2;

    this.pos=this.pos.sumar(this.vector.escalar(delta));

    if (this.pos.x - mitad < 0)
    {
        this.pong.puntuaciones[1]++;
        SONIDOS.gol.play();
        this.sacar(1);
    }
    if (this.pos.y - mitad < 0)
    {
        this.pos.y= mitad;
        this.vector.y*=-1;
       
        SONIDOS.plop.play();
    }
    if (this.pos.x + mitad > this.mundoAncho)
    {
        this.pong.puntuaciones[0]++;
        SONIDOS.gol.play();
        this.sacar(-1);
    }
    if (this.pos.y + mitad > this.mundoAlto)
    {
        this.pos.y=this.mundoAlto - mitad;
        this.vector.y*=-1;
       
        SONIDOS.plop.play();
    }
    var vector;
    if (this.vector.x<0 && this.colisiona(this.pong.palas[0]))
    {
        this.vector.x*=-1;
        this.incrementarVelocidad();

        vector=this.vector.clonar();
        this.vector=this.vector.rotar(this.pos.y-this.pong.palas[0].pos.y);
       
        SONIDOS.beep.play();
    }
    else if (this.vector.x>0 && this.colisiona(this.pong.palas[1]))
    {
        this.vector.x*=-1;
        this.incrementarVelocidad();

        vector=this.vector.clonar();
        this.vector=this.vector.rotar(this.pong.palas[1].pos.y-this.pos.y);
       
        SONIDOS.beep.play();
    }
    var angulo=this.vector.getAnguloRelativo();

    if (angulo>this.maxAngulo || angulo <-this.maxAngulo)
    {
        this.vector=vector;
    }

};

Parémonos a analizar su segunda línea:

this.pos=this.pos.sumar(this.vector.escalar(delta));

Aquí está toda la magia del vector dirección y el vector posición. El vector dirección (this.vector), como ya hemos dicho, contiene también la información de la distancia que tiene que moverse (velocidad), pero ojo, EN UN SEGUNDO. Como estamos utilizando el tiempo delta, solo tenemos que escalar ese vector a la los segundos transcurridos desde el último movimiento. Así obtenemos otro vector que al sumarlo al vector posición, moverá la pelota por el mundo adecuadamente. Ojo, acuérdate de que nuestros métodos de vectores NO ACTUALIZAN al vector que los ejecuta, así que el vector this.vector original no será afectado por al ejecutar su método escalar(). Y por eso, el this.pos hay que asignarle el resultado de la suma de él mismo con otro vector.

Ahora ya podemos ver el resto del método mover de la pelota. Si has entendido lo que acabo de explicar, no tiene ninguna complicación. Veámoslo por partes:

Después de mover la pelota, comprobamos si se ha salido por la parte izquierda o derecha de la pantalla. En ese caso es GOL del jugador 2 o 1. Así que incrementamos su puntuación, reproducimos el sonido que indica gol, y sacamos.

if (this.pos.x - mitad < 0)
{
      this.pong.puntuaciones[1]++;
      SONIDOS.gol.play();
      this.sacar(1);
}
if (this.pos.x + mitad > this.mundoAncho)
{
      this.pong.puntuaciones[0]++;
      SONIDOS.gol.play();
       this.sacar(-1);
}

Esto detecta si la pelota choca con la parte superior o inferior del mapa. Colocamos la pelota dentro de los límites del mundo (teniendo en cuanta que la posición es su centro), cambiamos la dirección de la pelota (que sólo es cambiarle el signo a su componente del eje Y, algo que conseguimos rápidamente multiplicando su componente Y por -1), y reproducimos el sonido de chocar con la pared.

if (this.pos.y - mitad < 0)
{
      this.pos.y= mitad;
      this.vector.y*=-1;
       
      SONIDOS.plop.play();
}
if (this.pos.y + mitad > this.mundoAlto)
{
     this.pos.y=this.mundoAlto - mitad;
      this.vector.y*=-1;
       
    SONIDOS.plop.play();
}

A continuación, detectamos su hay colisión de la pelota con alguna de las palas. Si la detecta, además de incrementar la velocidad y cambiar el sentido de la dirección de la pelota (esta vez sobre el componente X), rotaremos la dirección de la pelota un poco, según la zona de la pala con la que le he pegado a la bola. Si le pegamos con el centro, la componente Y de la pala y la pelota en el momento de la colisión serán iguales, asi que su resta será cero. Y si rotamos un vector 0 grados… no lo movemos. Es decir, pegándole con el centro de la pala no variamos su rebote normal. Pero por cada píxel que nos alejemos del centro de la pala, le estaremos metiendo grados a la rotación del vector. Si le pegamos por arriba de la pala, es una rotación negativa, y si es hacia abajo, una rotación positiva (recuerda que el eje Y del canvas crece hacia abajo). Es una forma sencilla y divertida de cambiar sensiblemente la dirección del rebote, que hará que los buenos jugadores busquen pegarle a la pelota con el pico de la pala para complicarle la vida al otro jugador. Y para terminar, reproducimos el sonido de la colisión entre pelota y pala.

Observa que tenemos una variable local, vector, donde almacenamos el vector dirección que tenía la pelota antes de rotarla.

var vector;
if (this.vector.x<0 && this.colisiona(this.pong.palas[0]))
{
    this.vector.x*=-1;
    this.incrementarVelocidad();

    vector=this.vector.clonar();
    this.vector=this.vector.rotar(this.pos.y-this.pong.palas[0].pos.y);
   
    SONIDOS.beep.play();
}
else if (this.vector.x>0 && this.colisiona(this.pong.palas[1]))
{
    this.vector.x*=-1;
    this.incrementarVelocidad();

    vector=this.vector.clonar();
    this.vector=this.vector.rotar(this.pong.palas[1].pos.y-this.pos.y);
   
    SONIDOS.beep.play();
}

Esa variable local vector nos servirá para deshacer la rotación del vector dirección en el caso de que el ángulo de la pelota sea demasiado pronunciado, lo que haría muy tediosa la partida porque la pelota tardaría mucho en cruzar todo el ancho del mundo, pues rebotaría muchas veces con la parte superior e inferior del mapa. Para calcular el ángulo usamos aquél extrañoo método personalizado que metimos en Vector2D, que nos da ángulos absolutos de la dirección de la pelota con respecto al eje X. Si va hacia arriba, es negativo, y si va hacia abajo, positivo. Si en ángulo actual de la pelota sobrepasa el máximo, ponemos la copia del vector que guardamos anteriormente.

var angulo=this.vector.getAnguloRelativo();
if (angulo>this.maxAngulo || angulo <-this.maxAngulo)
{
    this.vector=vector;
}

Y con ésto prácticamente nos hemos liquidado la clase Pelota, pues sólo nos queda implementar el método que dibuja el cuadrado, y el detector de colisiones entre rectángulos, el más sencillo de todos. Ambas cosas deberíamos estar hartos de hacerlas.

Pelota.prototype.dibujar=function(contexto){
    contexto.fillStyle="white";
    contexto.fillRect(this.pos.x-this.lado/2,this.pos.y-this.lado/2,this.lado,this.lado);
};
Pelota.prototype.colisiona=function(pala){
    var mitad=this.lado/2;
    if (this.pos.x + mitad < pala.pos.x - pala.ancho/2) return false;
    if (this.pos.y + mitad < pala.pos.y - pala.alto/2) return false;
    if (this.pos.x - mitad > pala.pos.x + pala.ancho/2) return false;
    if (this.pos.y - mitad > pala.pos.y + pala.alto/2) return false;
    return true;
};

Pong, la clase principal

Por fin llegamos a la última clase que tiene el juego, la que contiene el loop, el bucle principal. No tiene nada complicado, así que seré breve.

function Pong(ia){
    this.controles = new Controles();
    this.ia=ia;

    this.canvas=document.getElementById("canvas");
    this.canvas.width=800;
    this.canvas.height=500;
    this.canvas.style.backgroundColor="black";

    this.contexto=this.canvas.getContext('2d');

    this.palas=[new Pala(30,this.canvas.height), new Pala(this.canvas.width-30,this.canvas.height)];
    this.pelota=new Pelota(this.canvas.width,this.canvas.height,this);

    this.puntuaciones=[0,0];

    this.tiempoTranscurrido=Date.now();
    this.loop();
}

Cargamos los controles, el canvas, el contexto, un array de dos posiciones para las palas, la pelota, las puntuaciones, el tiempo transcurrido para calcular delta, y llamamos al loop. El argumento ia es el checkbox con el que indicar si usar o no una inteligencia artificial para el jugador 2.

Pong.prototype.dibujarMundo=function(){
    var trozos=31;
    var trozo = Math.round(this.canvas.height/trozos);
    var mitad=Math.round(this.canvas.width/2);

    this.contexto.lineWidth = 10;
    this.contexto.strokeStyle = "white";
    for (var i=0;i<trozos;i++)
    {
        if (i%2==1) continue;
        this.contexto.beginPath();
        this.contexto.moveTo(mitad,trozo*i);
        this.contexto.lineTo(mitad,trozo*(i+1));
        this.contexto.stroke();
    }

    //Puntuacion
    this.contexto.fillStyle="white";
    this.contexto.font = "bold 50px monospace";
    this.contexto.fillText((this.puntuaciones[0]<10?"0":"")+""+this.puntuaciones[0],this.canvas.width/2-80,40);
    this.contexto.fillText((this.puntuaciones[1]<10?"0":"")+""+this.puntuaciones[1],this.canvas.width/2+20,40);
}

El método dibujar mundo es el encargado de pintar la raya punteada que cruza el mapa verticalmente por su centro. Además pinta las puntuaciones en la parte superior, metiéndole un cero si la cifra solo tiene un dígito. Para dibujar la raya punteada, lo que he hecho ha sido dividir el alto del mapa en 31 trozos, y mediante un bucle voy pintando una línea con la longitud de cada trozo. Como quiero pintar una sí, y otra no, lo que hago es controlar la vuelta del bucle que estoy ejecutando, y si es impar, no pinto nada. Así se crean los huecos.

Una IA muy sencilla

A continuación viene el bucle principal. Lo partiré en trozos para explicarlo más fácilmente.

Pong.prototype.loop=function(){
    var direccion1=Direccion.QUIETO;
    var direccion2=Direccion.QUIETO;

    var delta=(Date.now() - this.tiempoTranscurrido)/1000;
    this.tiempoTranscurrido=Date.now();

    //Movemos las entidades
    if (this.controles.W && !this.controles.S)
    {
        direccion1=Direccion.ARRIBA;
       
    }
    else if(!this.controles.W && this.controles.S)
    {
        direccion1=Direccion.ABAJO;
    }

    if (ia.checked)
    {
        if (this.pelota.pos.y-this.pelota.lado/2 <= this.palas[1].pos.y)
        {
            direccion2=Direccion.ARRIBA;
        }
        if (this.pelota.pos.y+this.pelota.lado/2 >= this.palas[1].pos.y)
        {
            direccion2=Direccion.ABAJO;
        }
    }
    else
    {
        if (this.controles.ARRIBA && !this.controles.ABAJO)
        {
            direccion2=Direccion.ARRIBA;
        }
        else if(!this.controles.ARRIBA && this.controles.ABAJO)
        {
            direccion2=Direccion.ABAJO;
        }
    }
    this.palas[0].mover(delta,direccion1);
    this.palas[1].mover(delta,direccion2);

Para empezar, lo que hacemos es calcular el tiempo delta, como siempre, y mover las palas según las teclas pulsadas. Para ello, almaceno en las variables locales direccion1 y dirrecion2 la dirección deseada para cada pala. Con el jugador 1 siempre miro las teclas pulsadas para obtenerla. Para el jugador 2, miraré el checkbox de la IA. Si está marcado, quiere decir que la inteligencia artificial deberá elegir una dirección. He programado una IA muy simple. Miro donde están los centros de la pala y la pelota, y muevo la pala en consecuencia. Esto hará que la pala contínuamente siga la trayectoria de la pelota. Para evitar el tembleque contínuo que se produce cuando la Y de la pelota es igual a la Y de la pala (ya que la pala siempre se movería o arriba o abajo), hago una pequeña comprobación para detectar si la Y de la pala está en cualquier parto de todo el rango Y que ocupa la bola (desde su vértice superior al vértice inferior).

Es una IA muy fácil de vencer, porque si le metes mucha inclinación a la pelota, cuando la pelota tenga cierta velocidad, no podrá alcanzarla. Además, tampoco buscará pegarle con el pico de la pala para complicarle la vida al jugador.

Una IA más avanzada calcularía la trayectoria de la pelota en cuanto tú le pegues. e iría a esa posición. Incluso podría calcular con qué zona de la pala pegarle para ponerte las cosas más difíciles. Os reto que en los comentarios me dejéis un código que mejore la IA de la pala. Incluso si hay suficientes lectores con los suficientes cojones para postear sus códigos, se podría hacer una competición que hiciera jugar a las IA entre sí. Pero tranquilos, debido a la poca cantidad de lectores que tengo, dudo de que esto suceda.

Si no está marcado el checkbox, entonces se mirarán si están apretados los cursores.

Los QUANTUM, o cómo mejorar la detección de colisiones

Si sois avispados sabréis que cuanta más velocidad tenga la pelota, más distancia se desplazará en una vuelta del bucle. Esto podría causar un problema gordo: Que la pelota, cuando tenga una velocidad enorme, atraviese la pala completamente en un solo movimiento, por lo tanto no detectaría la colisión y se marcaría gol.

La solución ideal sería dividir la trayectoria que sigue la bola en un solo movimiento píxel a píxel, e ir calculando si la pelota colisiona con alguna pala en cada uno de esos pasos. Esos trozos son los que conoceremos como QUANTUM (un guiño a la mecánica cuántica). Pero ir comprobando píxel a píxel es demasiado costoso, así que lo que haremos será dividir el tiempo delta en trozos más pequeños. Entonces, en vez de mover la pelota una sola vez el tiempo delta, moveremos la pelota un tiempo por ejemplo 0.005 tantas veces como resulte la parte entera de la división de delta entre 0.005. Y por si la división sobraran decimales, con una operación MOD también los podríamos tener en cuenta.

var quantum=Math.floor(delta/0.005);
for (var i=0;i<quantum;i++)
{
    this.pelota.mover(0.005);
}
quantum=delta-quantum*0.005;
this.pelota.mover(quantum);

Ejecutando el juego en mi PC, suelo tener un delta de 0.02 por vuelta. Así que con un 0.005 estoy haciendo 4 movimientos en cada vuelta del bucle para comprobar colisiones. Si por ejemplo, antes la pelota atravesaba la pala completamente cuando alcanzaba una velocidad de 2000, ahora necesitará alcanzar los 8000 para que se repita el error. Hemos pospuesto el problema a una velocidad que probablemente nunca se alcance en el juego, a no ser que jueguen dos chinos. Eso sí, a costa de más procesamiento de CPU, siempre hay que pagar un precio. Vuestra es la responsabilidad de decidir cuándo usar CUAMTUMS asumiendo el coste de las operaciones extra que se realizan.

//Dibjuamos las entidades
    this.contexto.clearRect(0,0,this.canvas.width,this.canvas.height);
    this.dibujarMundo();

    this.palas[0].dibujar(this.contexto);
    this.palas[1].dibujar(this.contexto);
    this.pelota.dibujar(this.contexto);

    var angulo=this.pelota.vector.getAnguloRelativo();

    log("Velocidad: " +this.pelota.velocidad + " // Angulo: " + angulo);

    var self=this;
    requestAnimationFrame(function(){ self.loop(); });
}

Por último dibujamos el mundo, las palas y la pelota, y mostramos un poco de información de la pelota en el div de logs útil para testear y experimentar con el juego en marcha. En vez de un intervalo, como en mis antiguos tutoriales, estoy usando la nueva función requestAnimationFrame, que trae HTML5. Básicamente funciona como un setTimeout mejorado. No necesitamos indicarle ningún tiempo. Será JavaScript el que decida cuándo llamar, optimizando al máximo los recursos (CPU) de la máquina donde se está ejecutando el navegador web. En ordenadores más lentos, requestAnimationFrame tendrá un retardo más grande que en ordenadores rápidos. Ësto no es un motivo de preocupación, porque gracias a la técnica del tiepo delta, estamos garantizando que la velocidad a la que funciona el juego sea idéntica en cualquier PC, como explicamos en su día en el tutorial del Space Invaders.

La primera vez que hago mención a requestAnimationFrame es en el segundo tutorial de Three.jS que podeis encontrar aquí. Entrad y leer la subsección “Un poquito de movimiento”, a la mitad del artículo, donde hablo más detenidamente de ella.

Cada navegador la implementa con un nombre distinto, asi que con un sencillo código JavaScript que he metido en el index.html, hago que siempre se llame a esa función independientemende del nombre que tenga. Incluso si no existe, entonces crearemos en su lugar un setTimeout a 60 FPS.

<html>
    <head>
        <title>Pong JavaScript</title>
        <script type="text/javascript" src="js/Sonidos.js"></script>
        <script type="text/javascript" src="js/Controles.js"></script>
        <script type="text/javascript" src="js/Vector2D.js"></script>
        <script type="text/javascript" src="js/Pelota.js"></script>
        <script type="text/javascript" src="js/Pala.js"></script>
        <script type="text/javascript" src="js/Pong.js"></script>
        <script type="text/javascript">
        function log(texto,nueva){
            var capa=document.getElementById("log");
            if (nueva)
                capa.innerHTML+="<br/>"+texto;
            else
                capa.innerHTML=texto;
        }    

        window.requestAnimationFrame = (function(){
          return  window.requestAnimationFrame       ||
                  window.webkitRequestAnimationFrame ||
                  window.mozRequestAnimationFrame    ||
                  function( callback ){
                    window.setTimeout(callback, 1000 / 60);
                  };
        })();

        window.onload=function(){
            var iaInput=document.getElementById("ia");
            var pong = new Pong(iaInput);
        }
        </script>
    </head>
    <body>
        <canvas id="canvas"></canvas>
        <div id="log"></div>

        Jugar contra la IA: <input type="checkbox" id="ia" />
    </body>
</html>

Y ésto es to, ésto es to, ésto es todo, amigos.

Volver al índiceIr al siguiente tutorial

Etiquetas: , , , , , ,

Comentarios (3)

  • Buscando vectores y he llegado aki, excelente explicación muy buena pagina. Aunque demasiado novato como para hacer una implementacion de la i.a. 🙁

  • buen tutorial, aprendi bastante, aunque yo lo queria aplicar en JAVA, pero pronto lo sabre aplicar. Con respecto a la IA, al igual que jp yo todavia soy noob xD.

  • Que lastima que no se produjera la competicion de IAs!!

¿Tienes algo que decir?

Gestionado con Wordpress y Stripes Theme Entradas (RSS) | Comentarios (RSS)