Autor: José María Amusquívar Poppe

Para el desarrollo de esta práctica se ha solicitado la realización de una versión similar al videojuego de los 70 llamado Pong (figura 1).


  1. Introducción
  2. Propuesta de diseño
  3. Recursos empleados
  4. Desarrollo del código
    1. Variables empleadas
    2. Función settings()
    3. Función setup()
    4. Función selectPlayer()
    5. Función resetGame()
    6. Función resetScore()
    7. Función drawBoard()
    8. Funciones de dibujado restantes
    9. Función keyPressed()
    10. Función de dibujado general draw()
    11. Función mousePressed()
  5. Resultados obtenidos
  6. Ejecución en vivo
  7. Referencias


Introducción

Pong es un videojuego perteneciente a la primera generación de videoconsolas publicado por Atari, y está basado en el deporte de tenis de mesa, razón por la cual, para su implementación en Processing, será necesario disponer de una mesa de juego dividida en dos, un marcador para dos jugadores, dos palas y una pelota. A diferencia del tenis de mesa, este videojuego mantiene su campo de juego cerrado, es decir, la pelota rebotará en las paredes laterales por lo que la pelota no podrá salir del campo a menos que uno de los dos jugadores falle su tirada.

El objetivo del juego es conseguir el mayor número de puntos, lo cual se consigue cuando el oponente falla su tirada dejando que la pelota se salga del campo de juego.



Propuesta de diseño

El diseño original del Pong es el que se puede observar en la figura 1. Este diseño está constituido por un tablero horizontal de color negro dividido en dos por una línea blanca, dejando a cada lado una pala en forma vertical y su respectivo puntaje en la zona superior de cada campo de juego.

El diseño elegido para este proyecto es distinto al mencionado anteriormente, aunque respeta sus mecánicas de juego. El diseño elegido tiene su inspiración en las mesas de Ping Pong clásicas, con su característico color azul de fondo y palas rojas, empleando un divisor central en forma de red, y un sistema de puntaje que se ve reflejado en el campo de cada jugador. Además, la orientación del tablero se ha cambiado de horizontal a vertical con el objetivo de hacer más cómodo los controles de las palas, tal como se aprecia en la figura 2.

Puesto que la disposición del tablero es vertical, los controles de las palas están compuestos por teclas orientadas horizontalmente, tales como la tecla “A” y “D” para la pala superior, y el cursor “Izquierdo” y “Derecho” para la pala inferior. Además de estas teclas, también existen otras dos que cumplen determinadas funciones, como por ejemplo la tecla “P” que sirve para iniciar o pausar el juego, y la tecla “R” para reiniciar un juego.

Finalmente, para que el juego sea más entretenido, se ha añadido un número de rondas a jugar que deberá ser proporcionado por el usuario.



Recursos empleados

Para la realización de esta práctica se ha empleado Processing1, que se define como un lenguaje de programación y entorno de desarrollo integrado de código abierto basado en Java. Este lenguaje se ha utilizado para el desarollo de forma local, sin embargo, para poder publicar el proyecto a través de internet es necesario emplear p5.js2, que se define como una librería JavaScript perteneciente al lado del cliente que posibilita la creación de experiencias interactivas y gráficas, basado en el núcleo de Processing.

Por tanto, el proyecto ha sido desarrollado totalmente en Processing y, dado que p5.js tiene su base en el primero, su respectiva conversión es simple. Para realizar esta conversión se he utilizado la herramienta online HerokuApp3.

Para que el videojuego sea más animado se han añadido tres tipos de sonidos, haciendo uso de la librería processing.sound en Processing. Estos tres sonidos se corresponden con el choque de la pelota contra las paredes, el choque de la pelota contra las palas y, en tercer lugar, un sonido cuando un jugador pierde. Estos sonidos fueron extraídos de la página FesliyanStudios4.

Para obtener el GIF del videojuego se optó por la librería gifAnimation, sin embargo, debido a problemas de lentitud en la captura de frames, se optó por obtener el GIF grabando la pantalla y pasándolo a dicho formato final.

Y, finalmente, para obtener datos introducidos por el usuario se ha empleado la librería javax.swing. El objetivo de su uso fue solicitar al usuario el número de rondas que desea jugar, además de mostrar un cartel informativo señalando el jugador ganador.



Desarrollo del código

A continuación se procederá a explicar el código realizado en Processing:

Variables empleadas

Para conseguir que el juego funcione adecuadamente, se ha empleado una gran cantidad de variables, cada una con una respectiva función, tal y como se puede observar en el siguiente fragmento de código:

//---------Especiales--------//
SoundFile pingPong, knockBall, gameOver; //Variables para la reproducción de sonidos
GifMaker gifFile; //Variable para la creación del GIF

//---------Configuración de pantalla----------//
int sizeX = 400; int sizeY = 500; //Dimensión de la ventana

//---------Palas------//
int supXPala, supYPala, infXPala, infYPala; //Posiciones X e Y de ambas palas
int jump, sizePala; //Valocidad de movimiento de las palas, y longitud de ellas

//---------Pelota--------//
int cx, cy, radius; //Posición X e Y de la pelota, y radio de la misma
int incY, incX, fastBall; //Incremento y velocidad de la pelota

//------Configuración del campo de juego-------//
int tableX, tableY, border; //Dimensiones de la zona de juego, tiene un borde que lo rodea
      
//-------Variables de estado-------//
boolean running; //Variable que señala si se está o no ejecutando el juego
boolean pauseGame = true; //Variable que señala si el juego ha sido pausado o no
boolean upWin; //Variable que señala qué jugador ha ganado el punto

//---------Variables de pintado del puntaje----------//
int middleX = (sizeX/2) + (sizeX/2)/2; //Variable de posición X, común para el centrado del marcador    
int middleYSup = (sizeY/2) - (sizeY/2)/2; //Variable de posición Y para el centrado del marcador superior   
int middleYInf = (sizeY/2) + (sizeY/2)/2; //Variable de posición Y para el centrado del marcador inferior

//----------Variables especiales-------//
int scoreSup, scoreInf; //Variables que almacenan el puntaje de los jugadores
int randDir = -1; //Variable que alterna la orientación de los saques de la pelota
int rounds; //Variable que guarda el número de rondas elegido  


Función settings()

Esta función es necesaria si se desea tratar las dimensiones de la ventana como variables, ya que esta función es ejecutada antes que la función setup(), consiguiendo así guardar las dimensiones de la ventana en variables para usos futuros.

void settings() {
  size(sizeX, sizeY);
}


Función setup()

En esta función se han inicializado las dimensiones del campo de juego, así como el borde de este. Además, es en esta función en la que se realiza la carga de los sonidos en sus respectivas variables, y se programa el funcionamiento del capturador de GIF.

void setup(){ 
  //------Variables del campo de juego--------//
  border = 20;
  tableX = sizeX - (2*border);
  tableY = sizeY - (2*border);
  
  //--------Carga de sonidos-------//
  pingPong = new SoundFile(this, "sounds/ping-pong.mp3"); 
  gameOver = new SoundFile(this, "sounds/game-over.mp3"); 
  knockBall = new SoundFile(this, "sounds/knock-ball.mp3"); 
  
  //--------Configuración del capturador de GIF--------//
  gifFile = new GifMaker(this, "ping_pong.gif");
  gifFile.setRepeat(0);
}


Función selectPlayer()

Esta función se ejecuta en cada inicio de punto, obteniendo una posición aleatoria para la pelota en el eje X, y comprobando qué jugador perdió el punto para que sea éste quién realice el saque. Además, también se alterna la dirección inicial de la pelota, empleando la variable randDir para este cometido, la cual cambia el signo de la posición del eje X de la pelota, consiguiendo que ésta empiece su trayectoria hacia la derecha o hacia la izquierda.

void selectPlayer () {
  //-----Posición aleatoria de la pelola------// 
  cx = (int) random(border + radius, sizeX - border - radius);

  //-----Selección del siguiente saque-----//
  if (!upWin){
    cy = infYPala - radius;  
    incY = -fastBall;      
  }else{
    cy = supYPala + 10 + radius;
    incY = fastBall;     
  }

  //-------Cambio de orientación de la pelota----//
  incX =  randDir*fastBall; 
  randDir = -1 * randDir;
}


Función resetGame()

Esta función se encarga de reiniciar las posiciones de las palas y el de la pelota, llamando para ello a la función mencionada anteriormente selectPlayer.

void resetGame(){  
  //Palas
  sizePala = 80;
  supXPala = (sizeX/2) - (sizePala/2);
  supYPala = border;
  infXPala = (sizeX/2) - (sizePala/2);
  infYPala = tableY + 10;
  jump = 20; //Velocidad de palas  

  radius = 10;
  fastBall = 3;
  selectPlayer();  
}  


Función resetScore()

Función que reinicia el puntaje una vez se ha alcanzado el objetivo de rondas.

void resetScore() {      
  scoreSup = 0;
  scoreInf = 0;
}


Función drawBoard()

Esta función, además de dibujar el tablero, también añade el texto de ayuda en la zona superior de la ventana.

void drawBoard(){
  background(223, 184, 125); //Fondo de juego (bordes). Tono marrón

  //-----Texto de ayuda superior-----//
  textSize(12);
  text("Presione R para reiniciar, P para pausar o iniciar", (sizeX/2) - 130, border/2 + 5); //Texto de ayuda

  //-------Dibujado del tablero de juego-------//
  fill(125, 171, 223); //Fondo de mesa. Tono azul
  rect(border, border, tableX, tableY); //Forma de mesa rectangular

  //------Dibujado de las divisiones del tablero----//
  stroke(255); //Color de trazado de mesa (líneas). Blanco
  line(sizeX/2, border, sizeX/2, sizeY-20); //Trazado de mesa

  //---------Dibujado del separador central----------//
  fill(125, 223, 125); //Color de trazado de separación de mesa. Tono verde
  rect(border, (sizeY/2)-5, tableX, 10); //Trazado de separación de mesa
}


Funciones de dibujado restantes

A continuación se presentan las tres funciones de dibujado restantes, que incluye el dibujado de las palas, el de la pelota, y el del marcador.

void drawPala() {
  fill(255, 0, 0); //Color de pala. Rojo
  rect(supXPala, supYPala, sizePala, 10); //Dibujado de la pala superior
  rect(infXPala, infYPala, sizePala, 10); //Dibujado de la pala inferior
}

void drawBall() {
  fill(255); //Color blanco
  circle(cx, cy, radius*2); //Dibujado de la pelota según el diámetro=2*radius
}

void drawScore(){
  fill(255); //Color blanco
  textSize(60); //Tamaño de las letras

  //------Dibujado del nombre y puntaje del jugador superior--------//
  text("P1", (sizeX - middleX) - 30, middleYSup + 30);
  text(scoreSup, middleX - 30, middleYSup + 30); 

  //------Dibujado del nombre y puntaje del jugador inferior--------//
  text("P2", (sizeX - middleX) - 30, middleYInf + 30); 
  text(scoreInf, middleX - 30, middleYInf + 30); 
}


Función keyPressed()

La función keyPressed() es una función que se encuentra ya definida en Processing, por lo que tratar las distintas pulsaciones de teclas es una tarea sencilla. Pues las condiciones se dividen en dos, la primera controla si se ha pulsado alguna tecla de control (CODED), mientras que la segunda verifica si se ha pulsado alguno de los caracteres de acción requerido. Según qué tecla haya sido pulsada, se ejecuta una acción específica, desde cambiar las posiciones de ambas palas, como la acción de pausar el juego o, inclusive, de reiniciarlo.

void keyPressed() {
  if (key == CODED){
    if (keyCode == LEFT && !pauseGame){
      if (infXPala > border) infXPala -= jump;
    }else if (keyCode == RIGHT && !pauseGame){
      if (infXPala + sizePala < sizeX-border) infXPala += jump;
    }
  } else {
    if (key == 'A' || key == 'a' && !pauseGame){
      if (supXPala > border) supXPala -= jump;
    }else if (key == 'D' || key == 'd' && !pauseGame){
      if (supXPala + sizePala < sizeX-border) supXPala += jump;
    }else if (key == 'R' || key == 'r'){
      pauseGame = false;
      resetGame(); 
    }else if (key == 'P' || key == 'p'){
      pauseGame = !pauseGame;
    }
  }
}


Función de dibujado general draw()

Es en esta función en la que se añade la instrucción gifFile.addFrame(), que se encarga de capturar cada frame de la ejecución. Prosiguiendo con la estructura de esta función, se puede considerar que está dividida en tres secciones distinguidas según un condicionante. La primera sección se ejecuta cuando el juego ha sido pausado (tecla P), o cuando se ha iniciado el juego por primera vez. Esta sección se encarga de mostrar el mensaje de pausa que incluye los controles del juego.

if (pauseGame){ 
    //------Dibujado del juego-------//
    drawBoard();
    drawPala();
    drawBall(); 
    drawScore();

    //------Mensaje señelando la pausa-----//
    textSize(40);
    fill(255, 0, 0);
    text("PAUSA", (sizeX/2) - 64, (sizeY/2) + 14);

    //------Mensaje que indican los controles del juego-------//
    textSize(15);
    text("Pulse P para iniciar", (sizeX / 2) - 64, (sizeY / 2) - 40);
    text("CONTROLES", (sizeX / 2) - 44, (sizeY / 2) + 40);
    text("Pala superior: A y D", (sizeX / 2) - 64, (sizeY / 2) + 60);
    text("Pala inferior: Izq. y Der.", (sizeX / 2) - 70, (sizeY / 2) + 80);
    return;
  }

La segunda sección es ejecutada cuando el juego ha terminado, o cuando se ha iniciado el juego por primera vez, justo después de haber pulsado la tecla de inicio “P”. Esta sección se encarga de solicitar al jugador el número de rondas deseado, procesarlo, reiniciar el juego por si no se trata de la primera vez, y cambiar la variable de estado “running” a verdadero, señalando, de este modo, que el juego ha empezado a ejecutarse.

if (!running){
    //-------Solicitud del número de rondas--------//
    String roundsNumber = JOptionPane.showInputDialog(null, "¿Cuántas rondas?", "Rondas", JOptionPane.QUESTION_MESSAGE); 

    //----Comprobación de errores y conversión a entero-----//
    if (roundsNumber == null) System.exit(0);
    if (roundsNumber != null && !roundsNumber.matches("\\d+")) return; 
    rounds = Integer.parseInt(roundsNumber);
    if (rounds <= 0) return;

    //--------Reinicio del juego y señal de activación------//
    resetGame(); 
    resetScore();
    running = true;    
  }

La tercera sección de la función draw() se encarga de mantener actualizado el dibujado del tablero, es decir, se encarga de actualizar la posición de la pelota, además de comporbar si ésta debe rebotar o se ha salido del área de juego.

if (running){
   //-------Dibujado del videojuego-------//
   drawBoard();
   drawPala();
   drawBall(); 
   drawScore();

   //-------Actualización normal de la pelota--------//
   cy += incY; 
   cx += incX;

   //-------Comprobación del choque de la pelota contra las palas--------// 
   if (cy - radius <= supYPala + 10 && cx >= supXPala && cx <= supXPala + sizePala ||
       cy + radius >= infYPala && cx >= infXPala && cx <= infXPala + sizePala) {    
        incY =- incY;     
        pingPong.play();

   //-------Comprobación del choque de la pelota contra las paredes laterales---------//
   } else if (cx + radius >= sizeX-border || cx - radius <= border) {
       incX =- incX;     
       knockBall.play();

   //-------Comprobación del choque de la pelota contra las esquinas de las palas--------//
   }else if (cx + radius >= supXPala && cx - radius <= supXPala + sizePala && cy - radius <= supYPala + 10 || 
             cx + radius >= infXPala && cx - radius <= infXPala + sizePala && cy + radius >= infYPala) {
       incX =- incX;
       incY =- incY;
       pingPong.play();

   //----------Se comprueba si la posición de la pelota está fuera del área de juego----------//
   }else if(cy < border || cy > sizeY-border){  

     //--------Actualización del puntaje según la posición Y de la pelota-------//
     if (cy < border) {
       scoreInf++;
       upWin = true;
     }else{
       scoreSup++;
       upWin = false;
     }
       gameOver.play(); //Sonido reproducido al perder el punto

       //------Comprobación si se ha alcanzado el número de rondas deseado-------//
       if (scoreSup == rounds || scoreInf == rounds){
         String message;
         if (scoreSup == rounds) {
           message = "Ha ganado el jugador 1";
         }else{
           message = "Ha ganado el jugador 2";
         }  

         //-------Presentación del anuncio del ganador correspondiente-------//
          JOptionPane.showMessageDialog(null, message, "Ganador", JOptionPane.INFORMATION_MESSAGE);           
          running = false; //Cambio de estado, el juego ya no se ejecuta
          return;
       }   

       //-------Reinicio del tablero para cada punto-------//
       resetGame();  
       drawBoard();
       drawPala();
       drawBall(); 
       drawScore();
       pauseGame = true; //Se certifica que cada punto empiece pulsando "P"
   }
 }  

La detección de choques de la pelota se ha dividido en tres, el primero se corresponde con el choque contra las paredes laterales, en este caso, la pelota deberá cambiar de signo a su posición X; el segundo se corresponde con el choque contra la parte plana de las palas, en este caso, la pelota deberá cambiar de signo a su posición Y; mientras que el tercer caso de choque se corresponde con el choque de la pelota con las esquinas de las palas, en este caso, la pelota deberá cambiar de signo a ambas componentes, puesto que la pelota deberá retornar por la misma trayectoria que llegó.


Función mousePressed()

Se ha empleado esta función configurada de Processing para capturar la señal que indica que se quiere finalizar la creación del GIF.

void mousePressed () {
  gifFile.finish(); 
}


Para consultar el código fuente del videojuego, puede dirigirse al siguiente enlace:

Consultar código fuente



Resultados obtenidos

A continuación se muestra la ejecución del videojuego, tanto en Processing como en P5.js.

UI completa con Processing UI reducida con P5.js
El juego solicita el número de rondas El juego puede comenzar directamente



Ejecución en vivo

Las teclas 'A' y 'D' controlan la pala superior, el curso 'Izquierdo' y 'Derecho' la pala inferior
La ejecución sólo está disponible para ordenadores
Se debe hacer 'click' sobre el recuadro del videojuego para poder jugarlo


El juego anterior es una versión acortada del juego original desarrollado, si desea jugar la versión completa, acceda al siguiente enlace:

Al abrir el enlace se le solicitará un número de rondas que desea jugar


Versión completa del juego



Referencias