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 píxeles 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:
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.
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:
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 casos 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 de 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 solo 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.
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 píxeles 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.