libGDX: Flappy Bird con Box2D en Java

En esta serie de tutoriales vamos a aprender como hacer un Flappy Bird básico en Box2d y libGDX. Para los que no lo saben fue un exitoso juego lanzado en el año 2013 para dispositivos móviles cuyo objetivo es dirigir un pájaro entre tuberías sin chocar con ellas.

Puedes jugar la versión que vamos a crear directamente en tu navegador y también puedes descargar el código fuente en github. Recuerda que puedes ver el videotutorial para ver paso a paso la creación del juego:

Definir el mundo

El primer paso es definir una resolución para nuestro juego, para nuestro flappy bird será de 480 x 800 píxeles. Es necesario recordar que en Box2D las unidades utilizadas son metros, segundos, etc. Por lo que definir una tubería de 300 pixeles de altura en Box2D sería como tener un edificio de 300 metros de altura, casi tan alto como el Empire State.

Es necesario crear una escala. En mi caso yo siempre utilizo 100 pixeles es igual a 1 metro. Después de hacer esta escala suena más lógico crear una tubería de 3 metros (300 pixeles).

El siguiente paso es definir los objetos del juego y el tamaño de sus cuerpos (Body) en metros. Si analizamos la siguiente imagen podemos ver que existen 3 objetos. El pajaro (Bird), la tubería (Pipe) y el objeto contador (Counter)

  • El pájaro será una figura circular y tendrá un radio de .25 metros.
  • Las tuberías serán figuras rectangulares con las siguientes medidas .85 x 4 metros.
  • El contador tendrá unas medidas de .1 x 1.85 metros.

También tenemos que definir las velocidades y aceleraciones que necesitaremos. Estos valores dependen mucho de cómo queremos que sea el juego, podemos jugar con los valores hasta encontrar los valores que nos gusten para nuestro juego.

  • La gravedad del mundo será (0 , -13) m/s². Un poco más de lo que tiene el planeta tierra.
  • Para el pájaro cada vez que vuela tendrá una velocidad de 5 m/s². Hay que tener en cuenta que esto solo afectará el movimiento en el eje Y, el movimiento en X será nulo.
  • Tanto las tuberías como el contador tendrán la misma velocidad que será de -2 m/s², como pueden ver tiene una velocidad negativa por lo que se moverán de derecha a izquierda.

Si se han dado cuenta el pájaro siempre está en la misma posición X y lo que en realidad se mueve son las tuberías.

Una vez que hemos definido nuestro mundo el siguiente paso es crear los recursos (Assets) necesarios. Hay que recordar que la resolución definida es de 480 x 800 por lo que los Assets dependen de esta resolución.

Para empezar tenemos los elementos de la interfaz gráfica:

Game over: 300 x 76 píxeles

Game over: 300 x 76 píxeles


Get ready: 300 x 86 píxeles

Get ready: 300 x 86 píxeles


Tap: 250 x 215 píxeles

Tap: 250 x 215 píxeles


Background: 480 x 800 píxeles

Background: 480 x 800 píxeles

A continuación los elementos del juego. Para el pájaro es necesario tener 3 imágenes para crear la animación de que está volando y en cuanto a las tuberías necesitamos una que esté hacia arriba y otra hacia abajo.

Frame 1

Frame 1


Frame 2

Frame 2


Frame 3

Frame 3



Una vez que tenemos todos los Assets de nuestro juego es necesario empaquetarlos para esto utilizaremos un programa llamado Texture packer y obtendremos un archivo que yo llamé atlasMap.txt y una imagen llamada atlasMap.png como se ve en la siguiente imagen:

Atlasmap generado con Texture Packer

Atlasmap generado con Texture Packer

Es necesario agregar estos dos archivos dentro de nuestra carpeta assets que se encuentra en el proyecto de Android. Ver imagen:

Implementando el juego

Una vez que tenemos los assets del juego el siguiente paso es la implementación del juego que es muy sencilla.

La clase MainFlappyBird

Es el punto de entrada del juego. Esta clase hereda de Game, desde aquí se cargan los recursos (Assets) y ponemos la pantalla del juego

La clase Assets

Esta clase contendrá referencias estáticas a objetos TextureRegion así como Animation, en pocas palabras en esta clase será la encargada de cargar nuestros assets para poder utilizarlos más adelante en el juego.

La clase Screens

La clase abstracta Screen sirve para evitar crear código que se repite en cada una de las pantallas que creemos, en el caso de este juego solo crearemos una pantalla por lo que su utilidad no se verá reflejada, pero en otros caso ahorra mucho tiempo y líneas de código.

Clases del juego

Antes de comenzar con la pantalla de juego (GameScreen) es necesario crear las clases que representan cada objeto del juego. Necesitaremos las siguientes:

  • Bird
  • Pipe
  • Counter

Cada una de estas clases guardará la información como son la posición, el estado actual y el tiempo acumulado en un estado.

Cuentan con una función update que actualizará cada una de sus variables de acuerdo a su comportamiento. También contendrán funciones que alteran su estado, por ejemplo en caso que el pájaro haga colisión con una tubería llamaremos la función hurt.

La clase Counter

El objetivo de este objeto es que cada vez que el contador colisiona con el pájaro vamos a incrementar la puntuación en uno.

Esta clase no tiene mucha ciencia ya que sólo almacenará la posición actual y el estado.

Cuando state==STATE_REMOVE indica que este objeto debe ser removido en la siguiente actualización de la física del juego como veremos más adelante.

La clase Pipe

Esta clase es muy similar a la clase contador una diferencia es que aquí tenemos un tipo de tubería, esto quiere decir que sí type==TYPE_UP se dibujara la tubería que va en la parte superior.

La clase Bird

Igual que en las clases Counter y Pipe la clase Bird es muy sencilla y parecida a estas. Cuando state==STATE_DEAD significa que el pájaro ha chocado con una tubería y decimos que el pájaro ha muerto.

Se ha agregado una función hurt que será llamada cuando se detecte una colisión entre un pájaro y una tubería lo que cambiará el estado del pájaro.

También tenemos la variable stateTime que sirve para guardar el tiempo acumulado en un estado y poder dibujar en pantalla el sprite correcto y crear la animación de un pájaro aleteando

La clase WorldGame

Esta es una de las clases más importantes. Como habíamos dicho anteriormente en Box2D se trabaja con metros y también habíamos definido el tamaño del mundo que sería 4.8 de ancho y 8 de altura.

Esta clase se puede encontrar en 2 estados STATE_RUNNING y STATE_GAME_OVER. El primero es cuando el juego está en curso y el segundo es cuando el juego ha finalizado.

Para saber cuando debemos crear otra tubería utilizamos la constante TIME_TO_SPAWN_PIPE que es el tiempo en segundos que tarda una tubería en aparecer y la variable timeToSpawnPipe acumula el tiempo transcurrido desde que apareció la última tubería.

Esta clase también tiene la información de otros objetos como son el pájaro, las tuberías, los cuerpos, la puntuación, etc.

La clase contiene las funciones para crear los cuerpos y relacionar los cuerpos con sus respectivos objetos. Para aprender más sobre crear cuerpos puedes ver este post Cuerpos y sprites (imágenes).

La función createBird como su nombre lo dice sirve para crear el pájaro.

private void createBird() {
   oBird = new Bird(1.35f, 4.75f);
   BodyDef bd = new BodyDef();

   bd.position.x = oBird.position.x;
   bd.position.y = oBird.position.y;
   bd.type = BodyType.DynamicBody;
   
   Body oBody = oWorldBox.createBody(bd);

   CircleShape shape = new CircleShape();
   shape.setRadius(.25f);

   FixtureDef fixture = new FixtureDef();
   fixture.shape = shape;
   fixture.density = 8;

   oBody.createFixture(fixture);
   oBody.setFixedRotation(true);
   oBody.setUserData(oBird);
   oBody.setBullet(true);

   shape.dispose();
}

Primero es necesario crear el objeto oBird y los parámetros que recibe son las coordenadas en X y Y que es el lugar donde queremos que se muestre el pájaro. Luego creamos un cuerpo que se encuentra en la misma posición que el objeto oBird, le ponemos una figura circular con radio de .25 metros y creamos una fixtura donde le damos densidad de 8.

Una parte muy importante de esta función es oBody.setUserData(oBird) con esta línea de código agregamos al cuerpo la información del pájaro, de esta forma sabremos que este cuerpo en especifico pertenece al pájaro.

Luego tenemos las funciones createRoof y createFloor estas simplemente ponen un cuerpo en la parte superior y otro en la inferior que actuaran como los límites del juego, el pájaro no puede atravesar estos cuerpos. Si el pájaro toca alguno de estos cuerpos el juego cambia al estado STATE_GAME_OVER.

La función addPipe será llamada cada vez que el tiempo acumulado en la variable timeToSpawnPipe alcance el valor de TIME_TO_SPAWN_PIPE. Esta función agrega la tubería inferior, la superior y el contador en medio de las tuberías. Es importante notar que la posición en X donde se agregan las tuberías siempre será la misma y la posición en Y es la que cambia.

La función addCounter es muy similar a addPipe la diferencia es que aquí agregamos el objeto contador, como este será invisible no creamos un arreglo donde almacenar el objeto y solamente se asigna al cuerpo con la función oBody.setUserData(obj).

A continuación la función update se encarga de actualizar cada uno de nuestros objetos, ya sea el pájaro, las tuberías o el contador. La función oWorldBox.step(delta, 8, 4) es llamada para comenzar la simulación de los cuerpos dentro del mundo. Dentro de esta función tenemos que revisar si el estado del pájaro es STATE_DEAD de ser verdadero ponemos el estado del mundo en STATE_GAME_OVER.

Luego tenemos la función deleteObjects sirve para eliminar los objetos y cuerpos que ya no se encuentran visibles en la pantalla. Simplemente itera entre cada cuerpo del mundo y revisa el estado si es necesario lo elimina del mundo.

La función updateBird se encarga de actualizar el objeto oBird, es importante recordar que el objeto body es el cuerpo del pájaro. La primera parte llama a la función oBird.update(delta, body) que como sabes actualiza la posición de acuerdo a la posición del cuerpo y actualiza el stateTime de oBird. A continuación revisamos si la variable jump es verdadera y el estado del pájaro es Bird.STATE_NORMAL se cumplen las dos condiciones cambiamos la velocidad del cuerpo en Y, esto hace que el pájaro se mueva hacia arriba y podremos evitar las tuberías.

Las funciones updatePipes y updateCounter son muy similares, para poder actualizarlas primero revisamos que el estado del pájaro sea igual a Bird.STATE_NORMAL de lo contrario el juego está por finalizar y ponemos sus velocidades en 0 para que ya no avancen. Si el estado del pájaro si es Bird.STATE_NORMAL llamamos la función update y enseguida revisamos si la posición actual es menor o igual a -5 esto sirve para saber si el objeto está fuera de la pantalla y removerlo después para que ya no ocupe más espacio en la memoria.

La clase interna Collisions

Sirve para detectar cuando dos cuerpos hacen contacto entre sí, la función beginContact se llama automáticamente cuando dos cuerpos inician contacto y aquí revisaremos si el pájaro colisionó con un objeto contador o con cualquier otra cosa. Lo primero es separar el objeto contact en las 2 fixturas que chocaron, después revisa la información para saber si alguna de estas 2 fixturas es el pájaro de ser verdadero llamamos la función beginContactBird esta función recibe 2 parámetros que son la fixtura del pájaro y la del otro objeto con el que se colisionó.

En la función beginContactBird() revisamos contra que se colisionó si fue el contador revisamos que el estado de este sea igual a Counter.STATE_NORMAL incrementamos la puntuación y podemos al contador en Counter.STATE_REMOVE para que en la siguiente actualización del mundo sea eliminado de la memoria. Si la colisión no fue con el contador solo revisamos si el estado del pájaro es Bird.STATE_NORMAL y llamamos la función oBird.hurt con lo que el estado cambiará a Bird.STATE_DEAD y en la siguiente actualización el estado del juego se cambiará a STATE_GAME_OVER.

La clase GameScreen

La siguiente clase es la clase GameScreen que en pocas palabras muestra el juego al jugador y le permite interactuar con el. Es muy importante notar que esta clase hereda de Screens. La clase GameScreen consiste de 3 estados: STATE_READY, STATE_RUNNING, STATE_GAME_OVER cada uno de estos estados tendrá su propia función update donde se realizarán tareas específicas.

Dependiendo del estado actual se mostrará algo diferente en la pantalla:

Si el estado es STATE_READY se llama la función updateReady donde revisamos si se a tocado la pantalla con la función Gdx.input.justTouched() en caso de que la condición sea verdadera desvanecemos las imágenes getReady y tap además de cambiar al estado STATE_RUNNING.

Si el estado es STATE_RUNNING se llama la función updateRunning donde tenemos que actualizar y pasar las acciones realizadas por el jugador al mundo. En este caso la única acción es cuando el usuario toca la pantalla el pájaro debe “saltar”. También revisamos si el estado del mundo es igual a STATE_GAME_OVER de que sea así ponemos el estado de la pantalla GameScreen en STATE_GAME_OVER y agregamos la imagen de gameOver al stage.

Si el estado es STATE_GAME_OVER cuando se toca la pantalla en vez de cambiar el estado se pone una nueva pantalla GameScreen con la función game.setScreen(new GameScreen(game)) esto inicia otra vez el juego.

Para dibujar en pantalla nuestros objetos del juego así como la puntuación tenemos la función draw al igual que la función update esta se llama automáticamente. Lo primero que se hace aquí es renderer.render(delta) que dibuja en la pantalla todos los objetos de nuestro objeto oWorld enseguida actualizamos la cámara y dibujamos la puntuación.

La clase WorldGameRenderer

Por último esta clase lo que hace es dibujar todos los objetos del mundo (pájaro, tuberías, fondo) de acuerdo a sus posiciones y estado.

Para comenzar definimos algunas constantes como son su ancho y altura recordando que su valor es de 4.8 y 8 respectivamente ( recordemos que 100 pixeles es igual a 1 metro). El objeto renderBox nos permite dibujar las líneas de 'debug' de los cuerpos de nuestro mundo (Box2D).

La función render() que es llamada desde la clase GameScreen divide los objetos que se van a dibujar por su tipo. Primero dibujamos el fondo, enseguida las tuberías y al final el pájaro.

Es importante recordar que el orden en el que se dibujan las cosas mostrará los objetos unos arriba de otros.

public void render(float delta) {
   … // más código
   spriteBatch.begin();

   spriteBatch.disableBlending();
   drawBackground(delta);
   spriteBatch.enableBlending();
   drawPipe(delta);
   drawBird(delta);

   spriteBatch.end()
   … // más código
}

Fin y conclusiones

Con esto finalizamos el tutorial para crear un flappy bird básico. Como pueden ver es muy sencillo de hacer, tan sencillo que con un poco de experiencia pueden crearlo desde cero en unas cuantas horas. No olviden que pueden descargar el código fuente en github y jugar en tu navegador.

Comparte este artículo