domingo, 23 de febrero de 2014

Tutorial parte 2: Flappy Bird basico 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í.

La clase WorldGame

Esta es una de las clases mas importantes de nuestro juego por lo que la explicare en partes.

Las primeras constantes WIDTH y HEIGHT almacenan el tamaño de nuestro mundo, como ya lo habíamos comentado antes Box2D trabaja con metros, segundos, etc por lo que en este caso los valores son 4.8 x 8 respectivamente.

Después tenemos los estados que puede tener la clase WorldGame que son dos corriendo o juego terminado.

La siguiente constante TIME_TO_SPAWN_PIPE es el tiempo en segundos que tarda un tubería en aparecer y la variable timeToSpawnPipe acumula el tiempo transcurrido desde que apareció la ultima tubería.

El objeto oWorldBox  es el encargado de manejar todos los cuerpos, simulaciones físicas, memoria, etc.

En la variable score almacenamos la puntuación actual.

El objeto oBird almacena la información.

Luego tenemos un arreglo llamado arrTuberias este arreglo sirve para almacenar cada una de las tuberías que existen en el juego en un instante, es necesario crear un arreglo porque pueden existir mas de una tubería en pantalla.

Por ultimo tenemos un arreglo arrBodies que utilizaremos para almacenar todos los cuerpos del mundo e iterar sobre ellos para poder actualizar la información de nuestros objetos.


En el constructor creamos nuestro objeto oWorlBox, como ya se habia comentado antes tiene una gravedad negativa en Y de -13 m/s^2. Tambien creamos nuestros arreglos. El tiempo acumulado timeToSpawnPipe lo iniciamos en 1.5f para que las primeras tuberías aparezcan en cuanto iniciamos el juego.

Tenemos 3 funciones que explicaremos mas adelante, en pocas palabras crean al pájaro y ponen un limite superior e inferior en nuestro juego para que el pájaro no se salga de la pantalla.

Por ultimo ponemos el estado del juego en corriendo.


La función createBird() como su nombre lo dice sirve para crear el pájaro. Primero es necesario crear el objeto oBird y los parámetros que recibe son las coordenadas en X y Y donde se encuentra. Luego creamos un cuerpo que se encuentra en la misma posición que el objeto oBird y le ponemos una figura circular con radio de .25 metros.

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

Las siguientes dos funciones simplemente ponen un cuerpo en la parte superior y otro en la inferior que actuaran como los limites del juego, el pájaro no puede atravesar estos cuerpos.

La función agregarTuberias() sera llamada cada vez que el tiempo acumulado en la variable timeToSpawnPipe alcance los 1.5 segundos. 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 sera la misma y la posición en Y es la que cambia.

Luego tenemos la función agregarTuberia() aqui es donde se crea un objeto de la clase Tuberia para luego guardarlo en el arreglo de tuberías (arrTuberias). Primero tenemos que saber si es una tubería inferior o una superior y creamos la tubería. Se crea el cuerpo y se le da la velocidad que habíamos asignado en la clase Tuberia que en este caso es de -2 m/s^2. Una vez que se crear el cuerpo y se le asigna su fixtura agregamos al cuerpo la información de la tubería con la función oBody.setUserData(obj) y guardamos en el arreglo la tubería arrTuberias.add(obj).

La función agregarContador() es muy similar a agregarTuberia() la diferencia es que aqui agregamos el objeto contador, como este sera 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. Luego tenemos la función eliminarObjetos() que se explicara a detalle mas adelante por el momento solo diremos que sirve para eliminar los objetos y cuerpos que ya no se encuentran visibles en la pantalla. Las siguientes lineas sirven para ir acumulando el tiempo transcurrido y cuando se a alcanzado el tiempo necesario se agrega una tubería nueva.

En la siguiente parte tenemos un ciclo que iterara entre cada uno de los cuerpos del mundo, analizamos la información que tiene el cuerpo y la actualizamos conforme a su tipo.

Por ultimo revisamos si el estado del pajaro es Bird.STATE_MUERTO de ser verdadero ponemos el estado del mundo en STATE_GAMEOVER.

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 si el estado del pájaro es STATE_NORMAL si 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 updateTuberias() y updateContador() 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 esta 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 esta fuera de la pantalla y removerlo después para que ya no ocupe mas espacio en la memoria.

Cuando una tubería o contador están fuera de la pantalla es necesario eliminarlos para liberar memoria y no saturar nuestro dispositivo, para eso tenemos la función eliminarObjetos() que simplemente itera entre cada cuerpo del mundo y revisa si su estado es STATE_DESTRUIR en este caso destruye el cuerpo del mundo y lo elimina de nuestro arreglo de ser necesario.

La clase interna Colisiones sirve para detectar cuando dos cuerpos hacen contacto entre si, la función beginContact se llama automaticamente cuando dos cuerpos inician contacto y aqui 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 beginContactBirdOtraCosa() 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 beginContactBirdOtraCosa() revisamos contra que se colisionó si fue el contador revisamos que el estado de este sea igual a STATE_NORMAL incrementamos la puntuación y podemos al contador en STATE_DESTRUIR 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 STATE_NORMAL y llamamos la función oBird.getHurt() con lo que el estado cambiara a STATE_MUERTO y en la siguiente actualización el estado del juego se cambiara a STATE_GAMEOVER.

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 tendra su propio metodo update() donde se realizaran tareas especificas.


La primera parte consiste en la declaración de las algunas constantes que son los estados del juego, un objeto de la clase WorldGame, un objeto WorldGameRenderer que analizaremos mas adelante, así como 3 imágenes llamadas getReady, tap y gameOver. El constructor inicializa las nuestros objetos asi como a las imágenes, a estas ultimas tambien les da una posición y las agrega getReady y tap al stage.


A continuación tenemos las funciones para actualizar cada uno de los estados: updateReady(), updateRunning() updateGameOver(). Dependiendo de en que estado nos encontremos se llamara a la función correspondiente. Hay que recordar que la función update() se llama automaticamente aproximadamente 60 veces por segundo (60 fps).


Para la función updateReady() 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  en un lapso de .3 segundos ademas de cambiar al estado STATE_RUNNING.



En función updateRunning() creamos una variable boleana jump que sera verdadera si se toca la pantalla,  actualizamos el mundo (oWorld) pasándole como parámetros el tiempo y si se toco la pantalla o no. Después revisamos si el estado del mundo es igual a STATE_GAMEOVER de que sea así ponemos el estado de la pantalla GameScreen en STATE_GAME_OVER y agregamos la gameOver imagen al stage.

La función updateGameOver() es muy parecida a updateReady() pero aqui 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 automaticamente. Lo primero que se hace aqui 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 ultimo 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 lineas 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 necesario actualizar la cámara oCam.update() así como la matriz de proyección del SpriteBatch batcher.setProjectionMatrix(oCam.combined).



Este fragmento de código no tiene ciencia alguna solamente dibujamos el fondo iniciando en las coordenadas (0,0) con un tamaño de (4.8, 8) lo que llena completamente la pantalla con la imagen del fondo.


Después vamos a dibujar las tuberías, tendremos que iterar en nuestro arreglo de tuberías arrTuberias que se encuentra en nuestra objeto de la clase WorldGame. Revisamos que tipo de tubería es (superior o inferior) y dibujamos. Es muy muy importante tener en cuenta que el ancho y alto que seleccionemos para dibujar no tiene que ser el mismo ancho y alto que el cuerpo.

En esta linea batcher.draw(Assets.tuberiaDown, obj.position.x - .5f,obj.position.y - 2f, 1f, 4);  observamos que estamos dibujando con un ancho de 1 y altura de 4, e iniciamos en las coordenadas x-.5 y y-2, esta resta es necesaria para centrar la imagen en el cuerpo.


Para finalizar dibujamos el pájaro, primero vemos en que estado se encuentra para saber si repetimos la animación de aleteo o no. Y procedemos a dibujar.


--
Con esta segunda parte concluimos con el tutorial para crear un flappy bird basico. Pueden descargar el código fuente aqui.

Tratare de contestar todas sus preguntas que hagan así que no duden en preguntar.


6 comentarios:

  1. Muy buen tuto. Enhorabuena y gran trabajo!

    ResponderEliminar
    Respuestas
    1. Muchas gracias, ya puedes descargar el código fuente https://github.com/Tiarsoft/Tutorial---Flappy-Bird-Basico

      Eliminar
  2. hey muchas gracias aprendi mucho de esta pagina

    ResponderEliminar
    Respuestas
    1. Muchas gracias, seguire publicando tutoriales y si tienes dudas no olvides preguntar

      Eliminar
  3. pues tengo una duda jaja , gerardo uno como hace para guardar puntaje y niveles en el juego , pues ya lo he hecho y me funciono perfecto el juego ejecutado desde el escritorio , pero cuando lo exporto en el android y lo corro no guarda nada , saludos .

    ResponderEliminar
  4. Por favor debes hacer tambien videos haciendolo, asi nosotros aprendemos mas, yo mismo estoy algo liado ya que no se tanta programacion como ustedes

    ResponderEliminar