viernes, 14 de mayo de 2021

libGDX: Parte 2 - Flappy Bird con Box2D

Este tutorial es la continuación de como hacer un Flappy Bird básico con Box2d y libGDX. Pueden encontrar la primera parte aquí.


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 video tutorial para ver paso a paso la creación del juego



Implementando el juego

Vamos a continuar con la implementación de las clases del juego.

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 (ver tutorial 1). 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

}


Como se puede ver primero se dibuja el fondo (background) después las tuberías (pipes) y por último el pájaro (bird). En este orden el fondo va ser dibujado primero y sobre el los demás objetos.

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.

0 comments:

Publicar un comentario

Entradas populares