Este artículo comenta en detalle el código fuente de un remake del clásico Arkanoid, también conocido como Breakout, escrito totalmente en JavaScript.

Este tutorial tiene un doble objetivo. El primer objetivo es servir de introducción a Javascript, pero no creando una guía detallada del lenguaje, sino estudiando algunas de las características del mismo y viendo las posibilidades que le ofrecen los navegadores a la hora de crear y modificar dinámicamente el contenido de una página web. El segundo objetivo es mostrar como se puede hacer de una forma rápida un juego simple como el conocido Arkanoid, explicando en detalle todo el código fuente escrito para implementarlo.

Arkanoid
¡Jugar!
(abre una nueva ventana)
Actualización: Existe una nueva y mejorada versión 1.2

Código fuente

Al ser un programa escrito completamente en JavaScript, todo el código fuente se encuentra embebido dentro de la propia página HTML desde la que se ejecuta el juego, por lo que para acceder al mismo basta con seleccionar la opción «ver código fuente de la página» del navegador.

Clases en JavaScript

El programa utiliza una nomenclatura para las funciones de JavaScript que permite escribirlas de forma que su comportamiento se asemeje al de las clases que se utilizan en otros lenguajes de programación orientados a objetos como pueden ser Java o C++.

function Class() {
  this.attribute1 = 1;
  this.attribute2 = 2;

  this.method1 = function() {
  }

  this.method2 = function() {
  }
}

var object = new Class();
object.attribute1 = 0;
object.method1();

En este código de ejemplo, la función Class se puede interpretar como una clase que se compone de dos atributos y dos métodos. Atributos y métodos que pueden referenciarse desde fuera del ámbito de la función en la que se encuentran declarados gracias a que JavaScript utiliza variables libres en la definición de las funciones. Variables libres que se instancian dependiendo del contexto en el que se invocan. Es decir, la variable this apunta al objeto concreto desde el que se produce la llamada cuando se realiza una llamada al mismo.

En lo sucesivo, en vez de hablar constantemente de funciones JavaScript, se hablará de clases, atributos y métodos con el significado habitual utilizado en los lenguajes de programación orientados a objetos.

DOM

El programa utiliza DOM para cambiar dinámicamente el contenido de la página. DOM es el acrónimo de Document Object Model, y es una especificación implementada por muchos navegadores web para permitir a las funciones de JavaScript interactuar con los elementos de la página HTML en la que se encuentran. Lo que hace esta especificación es definir una forma de representar la página diviéndola en una jerarquía en forma de árbol, tomando el documento como un todo en la raíz y creando sub-árboles para cada uno de los elementos contenidos dentro de él. Gracias a este proceso de indexación se puede referenciar desde el código fuente a cualquier elemento concreto dentro de una página.

DOM permite trabajar con los elementos definidos estáticamente en la página HTML en tiempo de diseño o crear nuevos elementos de forma dinámica en tiempo de ejecución. Por ejemplo, para crear un nuevo elemento de tipo div habría que escribir el siguiente código:

div = document.createElement("div");

Y a esta nueva división se le podría asignar los atributos que normalmente se definen en tiempo de diseño, como su id por ejemplo:

div.id = "id";

De forma que posteriormente se podría volver a encontrar con la siguiente función:

div = document.getElementById("id");

El programa utiliza el atributo innerHTML para modificar dinámicamente las páginas. innerHTML permite especificar la definición y el contenido de cualquier elemento HTML. Por ejemplo, para cambiar el texto de una división se puede utilizar el siguiente código:

div.innerHTML = "text";

Otras tareas en cambio el programa las realiza utilizando directamente los atributos de estilo. Ya que, por ejemplo, para establecer el ancho de un determinado elemento HTML se puede escribir simplemente:

div.style.width = "20px";

Protocolo

Como parte del diseño general del programa se ha adoptado el siguiente protocolo (secuencia) de llamadas a funciones:

init: Función llamada cuando se inicializa el juego
start: Función llamada cuando se inicia una partida
stop: Función llamada cuando se termina una partida

El objetivo de este protocolo es permitir que cada objeto realice las tareas concretas que debe realizar dependiendo del evento que provoca la llamada, y servir como base para futuras ampliaciones mediante la incorporación de nuevos eventos y funciones.

Clases

El programa se compone de las siguientes clases:

Help
Score
Brick
Stick
Ball
Board
Game

En el resto de secciones de este artículo se explica en detalle el código fuente de cada una de ellas. El orden en que se presentan las clases se ha elegido de forma que resulte fácil de entender el conjunto evitando tener que saltar continuamente de una clase a otra.

Help

La clase Help representa la línea de texto de la parte inferior de la ventana en la que se muestran los mensajes de ayuda.

Esta clase aparece explicada en primer lugar porque en ella puede verse facilmente y de una forma práctica la implementación del protocolo que se explicó anteriormente.

function Help() {
  this.divHelp = document.getElementById("help");

El único atributo que contiene la clase es una referencia al elemento HTML que la representa. Debe ser claro que definida de esta forma sólo tiene sentido tener una instancia de esta clase, ya que si se creara más de una instancia todas ellas apuntarían al mismo elemento HTML y se sobreescribirán los mensajes las unas a las otras.

  this.init = function() {
    this.display("Press space bar to start");
  }

El método init se llama cuando se crea una instancia de la clase, y se limita a mostrar un mensaje en el que se indica que se pulse «espacio» para empezar a jugar.

this.start = function() {
    this.display("Use arrow keys to play", "help-tip");
  }

El método start se llama cuando se inicia una partida, y se limita a mostrar un mensaje indicando que la paleta se mueve con las teclas del cursor.

  this.stop = function() {
    this.display("Game Over");
  }

El método stop se llama cuando se termina la partida, y se limita a mostrar el típico mensaje de juego terminado.

  this.display = function(text, className) {
    if (this.divHelp.className != className)
      this.divHelp.className = className || "help-message";
    if (this.divHelp.innerHTML != text)
      this.divHelp.innerHTML = text;
  }
}

El único método propio del que consta esta clase, aparte de los del protocolo, es display. Muestra el texto pasado como primer parámetro en la línea de mensajes. El segundo parámetro contiene una cadena con el nombre de la clase CSS con el que se debe mostrar el texto. Si el nombre de la clase se omite se toma uno por defecto.

Notar que se usa el operador «||» para tomar el nombre de la clase CSS dada o un nombre por defecto. Esto es posible gracias a que en JavaScript todos los parámetros son opcionales. Los parámetros que figuran en la declaración de una función y que luego se omiten en las llamadas a la misma toman un valor undefined. Los valores undefined, al ser JavaScript un lenguaje debilmente tipado, se interpretan en función del contexto en el que aparecen, y en este caso concreto hacen que la expresión se interprete como «toma el valor dado si está definido o el valor constante opcional si no lo está».

Score

La clase Score representa la puntuación de la partida en curso y la máxima puntuación alcanzada hasta la fecha que aparecen en la parte superior de la ventana.

function Score() {
  this.divScore   = document.getElementById("score");
  this.divHiScore = document.getElementById("hi-score");
 
  this.score   = 0;
  this.hiScore = 0;

La clase contiene dos atributos en los que se guardan referencias a los elementos HTML en los que se escriben las puntuaciones, y otros dos atributos en los que se guardan los valores numéricos de dichas puntuaciones.

  this.init = function() {
    this.score = 0;
    this.loadHiScore();
    this.display();
  }

  this.start = function() {
    this.score = 0;
    this.loadHiScore();
    this.display();
  }

Los métodos del protocolo init y start contienen el mismo código porque se tienen que realizar las mismas tareas cuando se inicializa la clase y cuando se inicia una nueva partida. Inicializan a cero el valor de la puntuación actual, cargan el valor de la puntuación máxima guardada en una cookie y muestran ambos valores por pantalla.

  this.stop = function() {
    if (this.score > this.hiScore) {
      this.hiScore = this.score;
      this.saveHiScore();
    }
  }

El método stop del protocolo comprueba al finalizar una partida si la puntuación alcanzada durante la partida es mayor que la máxima puntuación actual, si lo es la actualiza y la guarda en una cookie.

  this.addToScore = function(points) {
    this.score += points;
    this.display();
  }

El método addToScore existe para que pueda ser llamado desde fuera de la clase con objeto de actualizar la puntuación de la partida en curso.

  this.display = function() {
    this.displayScore();
    this.displayHiScore();
  }
 
  this.displayScore = function() {
    var text = this.formatScore(this.score);
    if (text != this.divScore.innerHTML)
      this.divScore.innerHTML = text;
  }
 
  this.displayHiScore = function() {
    var text = this.formatScore(this.hiScore);
    if (text != this.divHiScore.innerHTML)
      this.divHiScore.innerHTML = text;
  }

El método display es el encargado de mostrar la puntuación por pantalla. Realiza una llamada a displayScore, que muestra la puntuación de la partida en curso, y displayHiScore, que muestra la máxima puntuación alcanzada hasta la fecha.

Los valores numéricos se formatean con el método formatScore en cadenas de texto con seis dígitos de longitud.

  this.saveHiScore = function() {
    var expires = new Date;
    expires.setFullYear( expires.getFullYear() + 1)
    document.cookie = "hiscore=" + escape( String(this.hiScore) )
                    + ";expires=" + expires.toGMTString();
  }

  this.loadHiScore = function() {
    var from = document.cookie.indexOf("hiscore=");
    if (from != -1) {
      from += "hiscore=".length;
      var to = document.cookie.indexOf(";", from);
      if (to == -1)
        to = document.cookie.length;
      this.hiScore = Number( unescape( document.cookie.substring(from, to) ) );
    }
  }

Los métodos saveHiScore y loadHiScore se utilizan para guardar y recuperar de una cookie la máxima puntuación alcanzada hasta la fecha.

Las cookies son ficheros de texto, normalmente de muy reducido tamaño, en los que se pueden guardar valores cuando un usuario visita una página con el objeto de volver a recuperarlos posteriormente cuando el usuario vuelve a visitar la misma página web. En la práctica, el contenido de una cookie no es más que una cadena de texto que contiene una lista de valores separados por el carácter «;» y a la que se puede acceder desde JavaScript a través de document.cookie.

El método saveHiScore guarda en document.cookie una lista con dos valores. El primer valor es la puntuación «hiscore» y el segundo valor es la fecha de expiración de la cookie «expires». El segundo valor es necesario ya que por defecto las cookies expiran cuando se cierra la sesión del navegador con la que se está consultando la página web, y para evitarlo se cambia a un año contando desde la fecha actual.

El método loadHiScore se limita a recuperar la puntuación guardada previamente en la cookie con saveHiScore.

  this.formatScore = function(points) {
    var digits = String(points);
    while(digits.length != 6)
      digits = "0" + digits;
    return(digits);
  }
}

El último método de la clase es formatScore, que se encarga de convertir a texto la puntuación dada completando con ceros por la izquierda hasta tener una longitud de seis dígitos.

Brick

Cada instancia de la clase Brick representa un ladrillo en el tablero de juego. Posiblemente sea la clase más sencilla de todas. Se limita a almacenar en sus atributos el elemento HTML que lo representa junto con su posición y tamaño.

En la inicialización de cada uno de los atributos de tipo numérico se llama a la función global pxToNumber que elimina la cadena «px» del final del texto que recibe como argumento y retorna el número contenido en la cadena resultante. Es necesario utilizarla porque se está trabajando con pixels, y para indicar las posiciones y tamaños en CSS se utiliza expresiones del tipo «32px» para indicar un valor de 32 pixels.

El atributo broken es una variable de tipo boolean que indica si el ladrillo ha sido golpeado por la pelota o no. Se inicializa con false para indicar que no ha sido golpeado y se cambia a true cuando la pelota lo golpea.

El único método de la clase es getPoints, que retorna los puntos a sumar a la puntuación cuando se golpea el ladrillo con la pelota. Se ha escogido un valor de 7 por ser un número primo que genera puntuaciones dificiles de ver como se han generado cuando crecen un poco.

function Brick(div) {
  this.div = div;

  this.left   = pxToNumber(this.div.style.left);
  this.top    = pxToNumber(this.div.style.top);
  this.width  = pxToNumber(this.div.style.width);
  this.height = pxToNumber(this.div.style.height);

  this.broken = false;

  this.getPoints = function() {
    return(7);
  }
}

Stick

La clase Stick representa la paleta que mueve el jugador. En su constructor recibe una referencia al tablero, que almacena como un atributo de la clase, para poder obtener el tamaño del tablero más tarde e impedir que la paleta se salga por los lados cuando se mueve.

function Stick(board, div) {
  this.board = board;
  this.div   = div;
 
  this.left   = pxToNumber(this.div.style.left);
  this.top    = pxToNumber(this.div.style.top);
  this.width  = pxToNumber(this.div.style.width);
  this.height = pxToNumber(this.div.style.height);
 
  this.speed = 5;

Los atributos de esta clase son prácticamente los mismos que los vistos para la clase que representa los ladrillos. El atributo speed almacena la velocidad en pixels con la que se mueve la paleta.

  this.onKeyDown = function(keyCode) {
    switch(keyCode){
      case 37: this.move(-this.speed); break;
      case 39: this.move( this.speed); break;
    }
  }

El método onKeyDown recibe las pulsaciones del teclado que le envía la clase Game, que se verá posteriormente. El código de tecla 37 se corresponde con la flecha izquierda y el código 39 con la flecha derecha. Lo único que hace este método es llamar a la función que se encarga de mover la paleta pasándole el número de pixels que debe moverse.

  this.move = function(delta) {
    var newLeft = this.getNewLeft(delta);
    if (this.left != newLeft) {
      this.left = newLeft;
      this.div.style.left = String(this.left) + "px";
    }
  }
 
  this.getNewLeft = function(delta) {
    var newLeft = Math.max(this.left + delta, this.board.minLeft);
    return( Math.min(newLeft, this.board.maxLeft - this.width) );
  }
}

El método move se encarga de mover la paleta horizontalmente. Primero llama a getNewLeft, que calcula la nueva posición de la paleta evitando que se salga por la izquierda o la derecha del tablero de juego, y a continuación mueve la paleta a su nueva posición.

Para evitar que la paleta se salga por la izquierda se toma el mayor valor entre la nueva posición de la paleta y el borde izquierdo del tablero. Para evitar que se salga por la derecha se toma el mínimo valor entre la nueva posición de la paleta y el borde derecho del tablero.

Esta forma tan simple de mover la paleta tiene un problema: no se está comprobando si la posición de la paleta ocupa la misma posición que la pelota. Si se juega un rato es fácil conseguir que la pelota quede «atrapada» dentro de la paleta. He preferido dejarlo así en esta primera versión para no complicar el código en exceso. De hecho, la interacción entre la paleta y la pelota es uno de los puntos que claramente han de mejorarse.

Ball

La clase Ball representa la pelota. Su declaración es idéntica a la de la clase Stick. Recibe una referencia al tablero de juego y otra al elemento HTML que lo representa. Almacena su posición y tamaño, y tiene dos atributos más, la velocidad de avance horizontal y la velocidad de avance vertical en pixels.

function Ball(board, div) {
  this.board = board;
  this.div   = div;

  this.left   = pxToNumber(this.div.style.left);
  this.top    = pxToNumber(this.div.style.top);
  this.width  = pxToNumber(this.div.style.width);
  this.height = pxToNumber(this.div.style.height);
 
  this.speedLeft = -2;
  this.speedTop  = -5;

La pelota se mueve en respuesta a los eventos del reloj que recibe periódicamente desde la clase Game, que se verá posteriormente, en el método OnTimer:

  this.onTimer = function() {
    var hit = this.move();
    this.changeDirection(hit);
  }

El movimiento de la pelota es un poco más complejo que el de la paleta porque tiene que rebotar contra las paredes, los ladrillos y la paleta. Y cuando se produce una colisión se debe cambiar la dirección de la misma para simular el rebote de una forma efectiva.

El método move de la pelota es el más largo de este programa, aunque podría dividirse en funciones más pequeñas. Su principio de funcionamiento se basa en comprobar si se produce alguna colisión en todas las posiciones intermedias por las que debe pasar la pelota desde su posición actual hasta su posición destino.

  this.move = function() {
    var left    = this.left;
    var top     = this.top;
    var newLeft = this.left;
    var newTop  = this.top;
    var hit     = 0;

    var deltaLeft = this.getNewLeft(this.speedLeft) - this.left;
    var deltaTop  = this.getNewTop(this.speedTop)   - this.top;
    var delta     = Math.max( Math.abs(deltaLeft), Math.abs(deltaTop) );

La primera parte del método se limita a declarar e inicializar variables con la posición actual, la posición destino y los desplazamientos efectivos en pixels a realizar en cada uno de los ejes. La variable delta almacena el mayor de los dos desplazamientos a realizar en cada eje.

    for (var i = 0; i < delta; ++ i) {
      left += (deltaLeft / delta);
      if ( this.board.checkHits(left, top, this.width, this.height) ) {
        hit = 1;
        break;
      }
       
      top += (deltaTop  / delta);
      if ( this.board.checkHits(left, top, this.width, this.height) ) {
        hit = 2;
        break;
      }

      newLeft = left;
      newTop  = top;
    }

A continuación se realiza un bucle en el que se van actualizando en cada iteración las variables, para obtener las posiciones intermedias por las que ha de pasar la pelota, y comprobando si se produce una colisión en cada una de estas posiciones intermedias. Es decir, si la pelota ha de desplazarse 2 pixeles horizontalmente y 5 verticalmente, entonces se harán 5 iteraciones, en cada una de las cuales la pelota se desplazará 2/5 pixels horizontales y 5/5 pixels verticales.

La variable hit se carga con el valor 1 para indicar que se ha producido una colisión al intentar desplazar lateralmente la pelota, y con el valor 2 para indicar que se ha producido una colisión al intentar desplazarla verticalmente.

    this.moveLeft( Math.round(newLeft) );
    this.moveTop( Math.round(newTop) );
   
    return(hit);
  }

El resto del método se limita a mover la pelota a la nueva posición calculada.

  this.getNewLeft = function(delta) {
    var newLeft = Math.max(this.left + delta, this.board.minLeft);
    return( Math.min(newLeft, this.board.maxLeft - this.width) );
  }

  this.getNewTop = function(delta) {
    var newTop = Math.max(this.top + delta, this.board.minTop);
    return( Math.min(newTop, this.board.maxTop - this.height) );
  }
 
  this.moveLeft = function(newLeft) {
    if (this.left != newLeft) {
      this.left = newLeft;
      this.div.style.left = String(this.left) + "px";
    }
  }
     
  this.moveTop = function(newTop) {
    if (this.top != newTop) {
      this.top = newTop;
      this.div.style.top = String(this.top) + "px";
    }
  }

Los cuatro métodos que siguen al de mover deberían ser fáciles de entender porque son muy similares a los explicados para la clase de la paleta. Calculan la nueva posición de la pelota evitando que se salga por los bordes, y cambian la posición del elemento HTML que la representa.

  this.changeDirection = function(hit) {
    if ( (hit == 1) || (this.left == this.board.minLeft) || (this.left + this.width == this.board.maxLeft) )
      this.speedLeft = -this.speedLeft;
    if ( (hit == 2) || (this.top == this.board.minTop) || (this.top + this.height == this.board.maxTop) )
      this.speedTop = -this.speedTop;
  }
}

El último método de la clase es el encargado de cambiar la dirección de la pelota dependiendo de si se produce una colisión con los bordes de la ventana o con algún ladrillo.

Board

La clase Board representa el tablero de juego. Es la encargada de instanciar los ladrillos, la pelota y la paleta, y contiene los métodos encargados de detectar las colisiones entre ellos.

function Board() {
  this.divBoard = document.getElementById("div-board");
 
  this.minLeft = 0;
  this.maxLeft = this.divBoard.clientWidth;
  this.minTop  = 0;
  this.maxTop  = this.divBoard.clientHeight;
 
  this.bricks = new Array();
  this.stick  = null;
  this.ball   = null;

Los atributos de esta clase incluyen una referencia al elemento HTML que representa el tablero de juego, así como su posición y dimensiones, un array de ladrillos, la paleta y la pelota.

  this.init = function() {
    this.destroy();
    this.createBricks();
    this.createStick();
    this.display();
  }
 
  this.start = function() {
    this.destroy();
    this.createBricks();
    this.createStick();
    this.createBall();
    this.display();
  }
 
  this.stop = function() {
  }

La función init del protocolo crea los ladrillos, la paleta y llama al método display para que los muestre por pantalla. La llamada al método destroy sirve para mantener el criterio de destruir los objetos actuales antes de crear otros nuevos.

La función start del protocolo destruye los posibles objetos existentes, crea nuevos y los muestra por pantalla. La única diferencia con la función init es que se crea la pelota con el objetivo de que de inicio la partida.

Y como se observa, en la función stop del protocolo no es necesario hacer ningún proceso.

  this.restart = function() {
    this.destroyBall();
    this.createBricks();
    this.createBall();

    this.display();
  }

El método restart es llamado cuando se acaban todos los ladrillos del tablero y se tienen que volver a poner para continuar la partida. La pelota en curso se destruye y se vuelve a crear una nueva.

  this.createBricks = function() {

    var rows    = 6,  columns = 7;
    var width   = 20, height  = 5;
    var minLeft = 2,  minTop  = 20;

    for (var i = 0; i < rows; ++ i)
      for (var j = 0; j < columns; ++ j) {
        var id        = "brick" + String( (i * rows) + j);
        var className = "brick";
        var left      = minLeft + (j * (width + 1) );
        var top       = minTop + (i * (height + 1) );
       
        this.bricks.push( new Brick( this.createDiv(id, className, left, top, width, height) ) );
      }
  }

  this.createStick = function() {
    this.stick = new Stick(this, this.createDiv("stick1", "stick", 60, 190, 20, 5) );
  }
 
  this.createBall = function() {
    var left = 70 + Math.round( Math.random() * 50);
    this.ball = new Ball(this, this.createDiv("ball1", "ball", left, 180, 4, 4) );
  }

  this.createDiv = function(id, className, left, top, width, height) {
    var div = document.createElement("div");
   
    div.id           = id;
    div.className    = className;
    div.style.left   = String(left)   + "px";
    div.style.top    = String(top)    + "px";
    div.style.width  = String(width)  + "px";
    div.style.height = String(height) + "px";
   
    return(div);
  }

Todos los métodos que empiezan con la palabra «create» se utilizan para crear los distintos elementos HTML que representan los ladrillos, la pelota y la paleta. Todos los elementos HTML son divisiones de tipo div y son creados y añadidos a la página web en tiempo de ejecución.

El método createBricks crea los ladrillos mediante dos bucles anidados en los que en cada iteración se crea un nuevo ladrillo. Notar que la posición y el tamaño de los ladrillos se deciden de forma dinámica mientras que el color se toma de una clase CSS.

El método createStick crea la paleta que aparece siempre en un misma posición y con un mismo tamaño.

El método createBall crea la pelota y lo único destacable en él es que la posición horizontal inicial se genera de forma aleatoria para tratar de que cada partida empiece de una manera distinta.

Y por último, el método createDiv, que es una función auxiliar escrita para evitar tener que repetir el mismo código de creación de divisiones una y otra vez.

  this.destroy = function() {
    this.destroyBricks();
    this.destroyStick();

    this.destroyBall();
  }
 
  this.destroyBricks = function() {
    for (var i in this.bricks)
      this.divBoard.removeChild(this.bricks[i].div);
    this.bricks.length = 0;
  }
 
  this.destroyStick = function() {
    if (this.stick)
      this.divBoard.removeChild(this.stick.div);
    this.stick = null;   
  }

  this.destroyBall = function() {
    if (this.ball)
      this.divBoard.removeChild(this.ball.div);
    this.ball = null;   
  }

Los métodos que empiezan por la palabra «destroy» realizan la operación inversa a los métodos «create», es decir, destruyen los objetos previamente creados. El código es muy sencillo de entender y no requiere mayor explicación.

  this.display = function() {
    this.displayBricks();
    this.displayStick();
    this.displayBall();
  }
 
  this.displayBricks = function() {
    for (var i in this.bricks)
      this.divBoard.appendChild(this.bricks[i].div);
  }
 
  this.displayStick = function() {
    if (this.stick)
      this.divBoard.appendChild(this.stick.div);
  }
 
  this.displayBall = function() {
    if (this.ball)
      this.divBoard.appendChild(this.ball.div);
  }

Los métodos «display» se encargan de añadir a la página web los elementos HTML creados dinámicamente para que estos se vean por pantalla. Notar que los elementos se añaden directamente como hijos de la división que representa el tablero de juego.

  this.checkHits = function(left, top, width, height) {
    var hit = false;
    hit |= this.checkHitBricks(left, top, width, height);
    hit |= this.checkHitStick(left, top, width, height);
    return(hit);
  }
 
  this.checkHitBricks = function(left, top, width, height) {
    var hit = false;
    for (var i in this.bricks)
      if ( this.isHitting(this.bricks[i], left, top, width, height) ) {
        this.bricks[i].broken = true;
        hit = true;  
      }
    return(hit);
  }
 
  this.checkHitStick = function(left, top, width, height) {
    return( this.isHitting(this.stick, left, top, width, height) );
  }

Los métodos que empiezan por «checkHit» son los encargados de comprobar si se produce alguna colisión entre la pelota y los ladrillos, y la pelota y la paleta.

Tal y como se explicó cuando se vio la clase Ball, la pelota no se mueve directamente de su posición actual hasta su posición final en función de su velocidad, sino que va comprobando todas las posiciones intermedias de su trayectoria para evitar pasar por encima de algún objeto sin detectarlo. El método checkHits es el que va recibiendo estas posiciones intermedias para comprobar si se produce alguna colisión. Y aunque el tamaño de la pelota es constante durante todo el juego, se pasa como parámetro para hacer el método un poco más genérico y que sirva de base para futuras modificaciones.

El método checkHitBricks recorre la lista de ladrillos comprobando si ocurre alguna colisión con alguno de ellos. Si ocurre entonces se marca el ladrillo como roto poniendo a true su atributo broken.

El método checkHitStick, como su propio nombre indica, comprueba si se produce alguna colisión con la paleta.

  this.isHitting = function(token, left, top, width, height) {
    return( (token.left                < left + width ) &&
            (token.left + token.width  > left         ) &&
            (token.top                 < top  + height) &&
            (token.top  + token.height > top          ) );
  }

El método isHitting es el que realmente realiza la comprobación de colisiones. Es un método genérico que recibe dos objetos rectangulares, uno por referencia y el otro desglosado por los valores de sus atributos, y comprueba si sus áreas se solapan.

  this.ballOut = function() {
    return(this.ball.top + this.ball.height == this.maxTop);
  }
 
  this.bricksOut = function() {
    return(this.bricks.length == 0);
  }

Los métodos ballOut y bricksOut se utilizan para el control de fin de partida y de nivel. Detectan cuando la bola alcanza el borde inferior de la ventana, fin de partida, y cuando se rompen todos los ladrillos, fin de nivel.

  this.onKeyDown = function(keyCode) {
    this.stick.onKeyDown(keyCode);
  }

El método onKeyDown recibe las pulsaciones de teclado que le envia la clase Game, que se verá más tarde, y las reenvía a la paleta.

  this.onTimer = function() {
    this.ball.onTimer();
  }

El método onTimer recibe las llamadas periódicas que le envía la clase Game y las reenvía a la pelota.

  this.getPoints = function() {
    var points = 0;
    for (var i in this.bricks)
      if (this.bricks[i].broken)
        points += this.bricks[i].getPoints();
    return(points);
  }

El método getPoints se llama para obtener la suma de puntos obtenidos por los ladrillos rotos. Para ello se recorre el array de ladrillos y comprueba el atributo broken de cada uno de ellos. Si el atributo tiene valor true entonces el ladrillo está roto y se le pide sus puntos correspondientes.

  this.removeBroken = function() {
    var intact = new Array;
    for (var i in this.bricks)
      if (this.bricks[i].broken)
        this.divBoard.removeChild(this.bricks[i].div);
      else
        intact.push(this.bricks[i]);
    this.bricks = intact;
  }
}

El método removeBroken es un poco como una continuación del anterior. Se encarga de eliminar los ladrillos rotos, tanto de la pantalla como del array de la clase. Para ello se recorre el array de ladrillos y comprueba el atributo broken de cada uno de ellos. Si el atributo tiene valor true entonces el ladrillo está roto y se elimina de la pantalla el objeto HTML que lo representa, en caso contrario se deja como está.

Game

La clase Game es la que controla el desarrollo del juego. Es el contenedor y controlador de las distintas secciones del mismo.

Esta clase tiene un atributo llamado self que merece una explicación un poco más detallada que el resto. Este atributo sirve para solventar un problema de JavaScript con la resolución de ámbitos. Un problema que aparece cuando una clase recibe eventos, como los de teclado o reloj, a través de llamadas a sus funciones realizadas desde fuera de la clase. En el momento que se produce una de estas llamadas, JavaScript resuelve el valor this dentro del ámbito en el que se produce la llamada, y no dentro del objeto al que se está llamando, por lo que es necesario crear este atributo self para que dentro de los métodos se tenga una referencia al objeto que realmente pertenecen. El resto de atributos de la clase son más normales.

Los atributos score, board y help representan las distintas secciones que componen el juego y se instancian como objetos de las clases Score, Board y Help respectivamente.

El atributo state sirve para conocer en un momento dado en que estado se encuentra el juego. Un valor de 1 indica que se está esperando a que el usuario pulse «espacio» para empezar una partida, un valor de 2 que se está jugando una partida, y un valor de 3 que se está mostrando el mensaje «Game Over» al terminar una partida.

El atributo timeStop es una variable auxiliar que se utiliza para controlar el tiempo que está pausado el juego mostrando el mensaje «Game Over» al terminar una partida.

function Game() {
  var self = this;
 
  this.score = new Score;
  this.board = new Board;
  this.help  = new Help;

  this.state    = 1;
  this.timeStop = null;

El método run es el punto de entrada al programa. Es el encargado de poner en marcha el protocolo de llamadas de funciones, registrar el teclado para empezar a recibir los eventos de pulsación de teclas, y activar el temporizador que posibilita la ejecución de tareas de forma periódica.

  this.run = function() {
    self.init();
    self.registerKeyboard();
    self.activeTimer();
  }

Las funciones del protocolo se limitan a llamar a los correspondientes métodos homónimos de las distintas secciones del juego y cambiar el valor del atributo que indica el estado en el que se encuentra el juego.

  this.init = function() {
    self.score.init();
    self.board.init();
    self.help.init();
   
    self.state = 1;
  }
 
  this.start = function() {
    self.score.start();
    self.board.start();
    self.help.start();
   
    self.state = 2;
  }
 
  this.stop = function() {
    self.score.stop();
    self.board.stop();
    self.help.stop();
   
    self.state = 3;
    self.timeStop = new Date().getTime();
  }

El método stop realiza una tarea adicional consistente en inicializar el atributo timeStop con la hora en la que se detiene el juego.

  this.registerKeyboard = function() {
    document.onkeydown = self.onKeyDown;
  }
 
  this.onKeyDown = function(e) {
    var keyCode = self.getKeyCode(e);
    switch(self.state) {
      case 1: self.onKeyMenu(keyCode); break;
      case 2: self.onKeyGame(keyCode); break;
    }
  }

  this.getKeyCode = function(e) {
    return(e? e.keyCode: window.event.keyCode);

  }

El método registerKeyboard es el encargado de registrar el teclado para esta clase, es decir, de solicitar que se envie las pulsaciones de teclas cuando ocurran. Para ello basta con indicar en document.onkeydown la función a la que se tiene que llamar.

La función onKeyDown es la encargada de recibir las pulsaciones de teclado y llamar a un método u otro en función del estado en que se encuentra el juego. Si el juego está en la pantalla de inicio, esperando que el usuario pulse «espacio» para empezar, entonces se llama a onKeyMenu, y si el juego está en medio de una partida entonces se llama a onKeyGame.

El método getKeyCode es una función auxiliar que se utiliza para obtener el código de la tecla pulsada independientemente del navegador que se esté utilizando, ya que en algunos navegadores este código de tecla aparece como un atributo del parámetro que recibe la función, y en otros en cambio aparece como parte de una variable global. Este método comprueba si el parámetro está definido, o no, para tomar el atributo keyCode del parámetro o de la variable global window.event.keyCode.

El control por teclado, aunque fácil de implementar, tiene un grave problema: el retardo entre pulsaciones. Cuando se deja pulsada una tecla durante un tiempo se repite la pulsación de dicha tecla. Dicho tiempo entre pulsaciones es inapreciable y adecuado para el control de la paleta, excepto en la primera pulsación. La primera vez el tiempo de retardo es más prolongado y hace que la paleta tarde bastante en reaccionar. Este comportamiento es por diseño de los teclados y no puede cambiarse por código, aunque se puede intentar mitigar su efecto con diversas estrategias, como dotar la paleta de inercia o hacer que se mueva constantemente.

  this.onKeyMenu = function(keyCode) {
    switch(keyCode) {
      case 32: self.start(); break;
    }
  }
 
  this.onKeyGame = function(keyCode) {
    self.board.onKeyDown(keyCode);
  }

El método onKeyMenu se limita a esperar a que el usuario pulse «espacio» para dar comienzo al juego, y el método onKeyGame se limita a pasar todas las pulsaciones del teclado al tablero de juego.

  this.activeTimer = function() {
    setTimeout(self.onTimer, 35);
  }
 
  this.onTimer = function() {
    switch(self.state) {
      case 2: self.onTimerGame(); break;
      case 3: self.onTimerStop(); break;
    }
    self.activeTimer();
  }

Los últimos métodos de la clase se encargan de activar el temporizador y procesar los eventos asociados al mismo.

El método activeTimer activa el temporizador para que llame a la función onTimer cada 35 milisegundos. La función onTimer por su parte se limita a llamar a la función onTimerGame o a onTimerStop dependiendo del estado en el que se encuentre el juego.

  this.onTimerGame = function() {
    self.board.onTimer();
    self.score.addToScore( self.board.getPoints() );
    self.board.removeBroken();
   
    if ( self.board.bricksOut() )
      self.board.restart();
    if ( self.board.ballOut() )
      self.stop();
  }

  this.onTimerStop = function() {
    var currentTime = new Date().getTime();
    if (currentTime - self.timeStop > 2000) {
      self.help.display("Press space bar to start");
      self.init();
    }
  }

}

El método onTimerGame es el verdadero motor de las partidas, se encarga de transmitir el evento de reloj al tablero de juego para que se mueva la pelota, de sumar los posibles puntos resultantes de las colisiones entre la pelota y los ladrillos, de eliminar los ladrillos rotos, de comprobar si se rompieron todos los ladrillos para empezar un nuevo nivel, y de comprobar si la pelota tocó el fondo de la ventana para dar por terminada la partida.

El método onTimerStop es sólo un método auxiliar para hacer una pausa de 2 segundos cuando se acaba una partida y mientras se muestra el mensaje «Game Over».

Varios

La única función global que hay definida en el programa es pxToNumber, que como ya se comentó, elimina la cadena «px» del final de la cadena de texto dada y devuelve el número contenido en la cadena resultante.

function pxToNumber(s) {
  return( Number( s.substring(0, s.length - 2) ) );
}

Y la única variable global que hay declarada en el programa es la que representa el juego. Lo único que se hace con ella es declararla y ejecutar el método run para iniciar el juego. No se vuelve a referenciar en ningún otro sitio del programa.

var game = new Game;
game.run();

HTML y CSS

La estructura HTML que contiene la página del juego es muy sencilla, son sólo tres divisiones para la puntuación, el tablero y la ayuda respectivamente:

<div class="div-game">
  <div id="div-score" class="div-score">
    <span id="score" class="score"></span>
    <span id="hi-score" class="hi-score"></span>
  </div>

  <div id="div-board" class="div-board">
  </div>

  <div id="div-help" class="div-help">
    <span id="help" class="help"></span>
  </div>
</div>

Las clases CSS se encuentran declaradas dentro de la propia página HTML mediante la etiqueta <style>. Y lo único destacable es que se utiliza posicionamiento y dimensionamiento absolutos, y que se ha inicializado el atributo overflow con el valor hidden para conseguir que se visualicen correctamente las divisiones en los distintos navegadores soportados.

Sirva de ejemplo la clase utilizada para los ladrillos:

.brick {
  position: absolute;
  overflow: hidden;
  color: #000000;
  background: #FF0000;
}

Próximas Versiones

El primer cambio a realizar debería ser la corrección de los errores en la detección de colisiones.

Para mejorar la jugabilidad se debería intentar dotar de inercia al movimiento de la paleta para evitar los problemas asociados al uso del teclado. Quizás sería mejor usar el ratón en vez del teclado.

Para dar más variedad al juego se deberían añadir más niveles, ya que actualmente sólo tiene uno, y además no resulta visualmente muy atractivo al ser todos los ladrillos de un mismo color.

Y desde el punto de vista técnico, en cuanto a la implementación del código, habría que intentar hacer un tratamiento conjunto y uniforme de los elementos que intervienen en el juego. Es decir, de los ladrillos, la pelota y la paleta, con el objetivo de que resulte fácil añadir otros, como los populares items que caen de los ladrillos al romperlos y proporcionan alguna ventaja como pueden ser pelotas extras.