Búsqueda de sitios web

Aprende Java creando un juego arcade clásico


Practica cómo estructurar un proyecto y escribir código Java mientras te diviertes creando un juego divertido.

Como estudiante de segundo semestre en sistemas y medios digitales en la Universidad Federal de Ceará en Brasil, me dieron la tarea de rehacer el clásico juego Atari 2600 Breakout de 1978. Todavía estoy en mi infancia en el aprendizaje del desarrollo de software, y esto fue una experiencia desafiante. También fue provechoso porque aprendí mucho, especialmente sobre la aplicación de conceptos orientados a objetos.

(Vaneska Karen, CC BY-SA 4.0)

Te explicaré cómo logré este desafío y, si sigues las instrucciones paso a paso, al final de este artículo tendrás las primeras piezas de tu propio juego clásico Breakout.

Eligiendo Java y TotalCross

Varios de mis cursos utilizan Processing, un motor de software que utiliza Java. Java es un lenguaje excelente para aprender conceptos de programación, en parte porque es un lenguaje fuertemente tipado.

A pesar de tener la libertad de elegir cualquier lenguaje o marco para mi proyecto Breakout, elegí continuar en Java para aplicar lo que aprendí en mis cursos. También quería usar un marco para no tener que hacer todo desde cero. Consideré usar Godot, pero eso significaría que apenas necesitaría programar.

En cambio, elegí TotalCross. Es un kit de desarrollo de software (SDK) de código abierto y un marco con un motor de juego simple que genera código para dispositivos Linux Arm (como Raspberry Pi) y teléfonos inteligentes. Además, como trabajo para TotalCross, tengo acceso a desarrolladores con mucha más experiencia que yo y conozco muy bien la plataforma. Parecía ser el camino más seguro y, a pesar de algunos conflictos, no me arrepiento en lo más mínimo. Fue genial desarrollar todo el proyecto y verlo ejecutarse en el teléfono y en la Raspberry Pi.

Remake innovador creado con Java y TotalCross ejecutándose en Raspberry Pi 3 Modelo B. (Vaneska Karen, CC BY-SA 4.0)

Definir la mecánica y estructura del proyecto.

Al comenzar a desarrollar cualquier aplicación, y especialmente un juego, es necesario considerar las principales características o mecánicas que se implementarán. Vi el juego Breakout original varias veces y jugué algunas versiones en Internet. Luego definí la mecánica del juego y la estructura del proyecto en base a lo que aprendí.

Mecánica de juego

  1. La plataforma se mueve hacia la izquierda o hacia la derecha, según la orden del usuario. Cuando llega a un final, choca contra la "pared" (borde).
  2. Cuando la pelota golpea la plataforma, regresa en la dirección opuesta a la que vino.
  3. Cada vez que la pelota golpea un "ladrillo" (azul, verde, amarillo, naranja o rojo), el ladrillo desaparece.
  4. Cuando todos los ladrillos del nivel 01 han sido destruidos, aparecen otros nuevos (en la misma posición que el anterior) y la velocidad de la bola aumenta.
  5. Cuando todos los ladrillos del nivel 02 han sido destruidos, el juego continúa sin obstáculos en la pantalla.
  6. El juego termina cuando cae la pelota.

Estructura del proyecto

  • RunBreakoutApplication.java es la clase responsable de llamar a la clase que hereda GameEngine y ejecuta el simulador.
  • Breakout.java es la clase principal, que hereda de la clase GameEngine y "ensambla" el juego, donde llamará objetos, definirá posiciones, etc.
  • El paquete sprites es donde van todas las clases responsables de los sprites (por ejemplo, la imagen y el comportamiento de los bloques, la plataforma y la pelota).
  • Los paquetes util contienen clases utilizadas para facilitar el mantenimiento del proyecto, como constantes, inicialización de imágenes y colores.

Ponte manos a la obra con el código

Primero, instale el complemento TotalCross desde VSCode. Si está utilizando otro entorno de desarrollo integrado (IDE), consulte la documentación de TotalCross para obtener instrucciones de instalación. 

Si está utilizando el complemento, simplemente presione Ctrl+P, escriba totalcross y haga clic en Crear nuevo proyecto. . Complete la información solicitada:

  • Nombre de carpeta: gameTC
  • ArtifactId: com.totalcross
  • Nombre del proyecto: Breakout
  • Versión de TotalCross: 6.1.1 (o la más reciente)
  • Crear plataformas: -Android y -Linux_arm (seleccione las plataformas que desee)

Al completar los campos anteriores y generar el proyecto, si está en la clase RunBreakoutApplication.java, al hacer clic derecho sobre ella y hacer clic en "ejecutar", se abrirá el simulador y "¡Hola mundo!" Aparecerá en su pantalla si ha creado su proyecto Java con TotalCross correctamente.

(Vaneska Karen, CC BY-SA 4.0)

Si tienes algún problema, consulta la documentación o pide ayuda a la comunidad TotalCross en Telegram.

Una vez configurado el proyecto, el siguiente paso es agregar las imágenes del proyecto en Recursos > Sprites. Cree dos paquetes llamados util y sprites para trabajar en ellos más adelante.

La estructura de su proyecto será:

(Vaneska Karen, CC BY-SA 4.0)

Ve detrás de escena

Para que sea más fácil mantener el código y cambiar las imágenes a los colores que deseas usar, es una buena práctica centralizar todo creando clases. Coloque todas las clases para esta función dentro del paquete util.

Constantes.java

Primero, cree la clase constants.java, que es donde residen los patrones de ubicación (como el borde entre la pantalla y donde comienza la plataforma), la velocidad, la cantidad de bloques, etc. Esto es bueno para jugar, cambiar números y comprender dónde cambian las cosas y por qué. Es un gran ejercicio para quienes recién comienzan con Java.

package com.totacross.util;

import totalcross.sys.Settings;
import totalcross.ui.Control;
import totalcross.util.UnitsConverter;

public class Constants {
    //Position
    public static final int BOTTOM_EDGE = UnitsConverter.toPixels(430 + Control.DP);
    public static final int DP_23 = UnitsConverter.toPixels(23 + Control.DP);
    public static final int DP_50 = UnitsConverter.toPixels(50 + Control.DP);
    public static final int DP_100 = UnitsConverter.toPixels(100 + Control.DP);

    //Sprites
    public static final int EDGE_RACKET = UnitsConverter.toPixels(20 + Control.DP);
    public static final int WIDTH_BALL =  UnitsConverter.toPixels(15 + Control.DP);
    public static final int HEIGHT_BALL =  UnitsConverter.toPixels(15 + Control.DP);

    //Bricks
    public static final int NUM_BRICKS = 10;
    public static final int WIDTH_BRICKS = Settings.screenWidth / NUM_BRICKS;
    public static final int HEIGHT_BRICKS = Settings.screenHeight / 32;

    //Brick Points
    public static final int BLUE_POINT = 1;
    public static final int GREEN_POINT = 2;
    public static final int YELLOW_POINT = 3;
    public static final int DARK_ORANGE_POINT = 4;
    public static final int ORANGE_POINT = 5;
    public static final int RED_POINT = 6;
}

Si quieres saber más sobre la unidad de densidad de píxeles (DP), te recomiendo leer la descripción de Material Design.

Colores.java

Como sugiere el nombre, esta clase es donde defines los colores utilizados en el juego. Recomiendo nombrar las cosas según el propósito del color, como fondo, color de fuente, etc. Esto facilitará la actualización de la paleta de colores de su proyecto en una sola clase.

package com.totacross.util;

public class Colors {
    public static int PRIMARY = 0x161616;
    public static int P_FONT = 0xFFFFFF;
    public static int SECONDARY = 0xE63936;
    public static int SECONDARY_DARK = 0xCE3737;
}

Imágenes.java

La clase images.java es sin duda la más utilizada.

package com.totacross.util;

import static com.totacross.util.Constants.*;
import totalcross.ui.dialog.MessageBox;
import totalcross.ui.image.Image;


public class Images {

    public static Image paddle, ball;
    public static Image red, orange, dark_orange, yellow, green, blue;

    public static void loadImages() {
        try {
            // general
            paddle = new Image("sprites/paddle.png");
            ball = new Image("sprites/ball.png").getScaledInstance(WIDTH_BALL, HEIGHT_BALL);

            // Bricks
            red = new Image("sprites/red_brick.png").getScaledInstance(WIDTH_BRICKS, HEIGHT_BRICKS);
            orange = new Image("sprites/orange_brick.png").getScaledInstance(WIDTH_BRICKS, HEIGHT_BRICKS);
            dark_orange = new Image("sprites/orange2_brick.png").getScaledInstance(WIDTH_BRICKS, HEIGHT_BRICKS);
            yellow = new Image("sprites/yellow_brick.png").getScaledInstance(WIDTH_BRICKS, HEIGHT_BRICKS);
            green = new Image("sprites/green_brick.png").getScaledInstance(WIDTH_BRICKS, HEIGHT_BRICKS);
            blue = new Image("sprites/blue_brick.png").getScaledInstance(WIDTH_BRICKS, HEIGHT_BRICKS);

        } catch (Exception e) {
            MessageBox.showException(e, true);
        }
    }
}

El método getScaledInstance() manipulará la imagen para que coincida con los valores pasados a través de la constante. Intenta cambiar estos valores y observa el impacto en el juego.

Resumen

En este punto, su proyecto debería verse así:

(Vaneska Karen, CC BY-SA 4.0)

Crea tu primer sprite

Ahora que el proyecto está estructurado correctamente, estás listo para crear tu primera clase en el paquete de sprites: paddle.java, que es la plataforma, el objeto de interacción del usuario.

paleta.java

La clase paddle.java debe heredar de sprite, que es la clase responsable de los objetos en los juegos. Este es un concepto fundamental en el desarrollo de motores de juegos, por lo que al heredar de los sprites, el marco TotalCross ya se ocupará de delimitar el movimiento dentro de la pantalla, detectar colisiones entre sprites y otras funciones importantes. Puedes consultar todos los detalles en Javadoc.

En Breakout, la paleta se mueve en el eje X a una velocidad determinada por el comando del usuario (mediante la pantalla táctil o el movimiento del mouse). La clase paddle.java es responsable de definir este movimiento y la imagen del sprite (la "cara"):

package com.totacross.sprites;

import com.totacross.util.Images;

import totalcross.game.Sprite;
import totalcross.ui.image.ImageException;

public class Paddle extends Sprite {
  private static final int SPEED = 4;

  public Paddle() throws IllegalArgumentException, IllegalStateException, ImageException {
    super(Images.paddle, -1, true, null);
  }

  //Move the platform according the speed and the direction
  public final void move(boolean left, int speed) {
    if (left) {
      centerX -= SPEED;
    } else {
      centerX += SPEED;
    }

    setPos(centerX, centerY, true);
  }
}

Indicas la imagen (Images.paddle) dentro del constructor, y el método move (una característica de TotalCross) recibe la velocidad definida al comienzo de la clase. Experimenta con otros valores y observa lo que sucede con el movimiento.

Cuando la paleta se mueve hacia la izquierda, el centro de la paleta en cualquier momento se define como ella misma menos la velocidad, y cuando se mueve hacia la derecha, es ella misma más la velocidad. En última instancia, defines la posición del objeto en la pantalla.

Ahora tu objeto está listo, por lo que debes agregarlo en la pantalla e incluir el movimiento del usuario para llamar al método move y crear movimiento. Haga esto en su clase principal, Breakout.java.

Agregar interacción en pantalla y de usuario

Al construir el motor de tu juego, debes concentrarte en algunos puntos estándar. En aras de la brevedad, agregaré comentarios en el código.

Básicamente, eliminarás el método initUI() generado automáticamente y, en lugar de heredar de MainWindow, lo heredarás de GameEngine. Aparecerá un "rojo" en el nombre de su clase, así que simplemente haga clic en la lámpara o en el símbolo de sugerencia para su IDE y haga clic en Agregar métodos no implementados. Esto generará automáticamente el método onGameInit(), que es responsable del momento en que comienza el juego, es decir, el momento en que se llama a la clase breakout.

Dentro del constructor, debes agregar el tipo de estilo (MaterialUI) y el tiempo de actualización en la pantalla (70), y señalar que el juego tiene una interfaz ( gameHasUI=verdadero;).

Por último, pero no menos importante, debes iniciar el juego a través de this.start() en onGameInit() y concentrarte en algunos otros métodos:

  • onGameInit() es el primer método llamado. En él, debes inicializar los sprites y las imágenes (Images.loadImages) y decirle al juego que puede iniciar.
  • onGameStart()se llama cuando se inicia el juego. Establece la posición inicial de la plataforma (en el centro de la pantalla en el eje X y debajo del centro con un borde en el eje Y).
  • onPaint() es donde dices lo que se dibujará para cada cuadro. Primero, pinta el fondo de negro (para no dejar rastros de los sprites), luego muestra los sprites con .show().
  • Los métodos onPenDrag y onPenDown identifican cuándo el usuario mueve la paleta (arrastrando un dedo en una pantalla táctil o moviendo el mouse mientras presiona el botón izquierdo). Estos métodos cambian el movimiento de la paleta a través del método setPos(), que activa el método move en la clase Paddle.java. Tenga en cuenta que el último parámetro del método racket.setPos es true para limitar con precisión el movimiento de la pala dentro de la pantalla para que nunca desaparezca del campo de visión del usuario.
package com.totacross;

import com.totacross.sprites.Paddle;
import com.totacross.util.Colors;
import com.totacross.util.Constants;
import com.totacross.util.Images;

import totalcross.game.GameEngine;
import totalcross.sys.Settings;
import totalcross.ui.MainWindow;
import totalcross.ui.dialog.MessageBox;
import totalcross.ui.event.PenEvent;
import totalcross.ui.gfx.Graphics;

public class Breakout extends GameEngine {

    private Paddle racket;

    public Breakout() {
        setUIStyle(Settings.MATERIAL_UI);
        gameName = "Breakout";
        gameVersion = 100;
        gameHasUI = true;
        gameRefreshPeriod = 70;

    }

    @Override
    public void onGameInit() {
        setBackColor(Colors.PRIMARY);
        Images.loadImages();

        try {
            racket = new Paddle();

        } catch (Exception e) {
            MessageBox.showException(e, true);
            MainWindow.exit(0);
        }
        this.start();
    }
    public void onGameStart() {
        racket.setPos(Settings.screenWidth / 2, (Settings.screenHeight - racket.height) - Constants.EDGE_RACKET, true);
    }

     //to draw the interface
     @Override
     public void onPaint(Graphics g) {
         super.onPaint(g);
         if (gameIsRunning) {
             g.backColor = Colors.PRIMARY;
             g.fillRect(0, 0, this.width, this.height);

             if (racket != null) {
                 racket.show();
             }
         }
     }
     //To make the paddle moving with the mouse/press moviment
     @Override
     public final void onPenDown(PenEvent evt) {
         if (gameIsRunning) {
             racket.setPos(evt.x, racket.centerY, true);
         }
     }

     @Override
     public final void onPenDrag(PenEvent evt) {
         if (gameIsRunning) {
             racket.setPos(evt.x, racket.centerY, true);
         }
     }
}

ejecuta el juego

Para ejecutar el juego, simplemente haga clic en RunBreakoutApplication.java con el botón derecho del mouse, luego haga clic en ejecutar para ver cómo se ve.

(Vaneska Karen, CC BY-SA 4.0)

Si desea ejecutarlo en una Raspberry Pi, cambie los parámetros en la clase RunBreakoutApplication.java a:

        TotalCrossApplication.run(Breakout.class, "/scr", "848x480");

Esto establece el tamaño de la pantalla para que coincida con la Raspberry Pi.

(Vaneska Karen, CC BY-SA 4.0)

¡El primer sprite y la mecánica del juego están listos!

Próximos pasos

En el próximo artículo, mostraré cómo agregar el objeto de bola y hacer colisiones. Si necesitas ayuda, llámame al grupo comunitario de Telegram o publica en el foro de TotalCross, donde estoy disponible para ayudarte.

Si pones en práctica este artículo, comparte tu experiencia en los comentarios. ¡Todos los comentarios son importantes! Si lo deseas, marca TotalCross como favorito en GitHub, ya que mejora la relevancia del proyecto en la plataforma.

Artículos relacionados: