Arquitectura y ciclo de vida de Libgdx
Libgdx es un framework en el cual podemos programar en un projecto Java llamado "core" y compilar para Windows, Linux, Android, iOS y Html5 en diferentes proyectos llamados "backends".
Esto permite ganar velocidad en el desarrollo, probando frecuentemente en desktop y ocacionalmente en los demas backends que son más lentos para compilar y ejecutar.
Este tutorial requiere conociminetos de Java y desarrollo de juegos. Les recomiendo "Video tutoriales de programación Java" y "Programación de juegos para principiantes".
Proyecto core y backends en Libgdx
En el proyecto core se programa la lógica del juego independientemente de la plataforma o sistema operativo. Esto se logra usando una interfaz o "API libgdx genérica" para acceder a la pantalla, sonido, archivos, red, etc. Libgdx provee una implementación de esta "API libgdx genérica" para cada plataforma soportada (desktop, Android, iOS y Html5).
El proyecto core no se puede ejecutar directamente ya que esta interfaz no está implementada en el proyecto. Para ejecutar el código de core se utilizan los proyectos backend (desktop, Android, iOS y Html5).
Si ejecutamos el proyecto backend desktop este usará la implamentacion específica del API libgdx para que el código de core funcione en una máquina de escritorio. Si ejecutamos el backend Android se usará una implementación basada en las librerias Android.
Si observamos las dependencias de los proyectos Android y core veremos que los dos tienen el API gdx-1.3.1.jar pero sólo el proyecto android tiene la implementación gdx-backend-android-1.3.1.jar.
gdx-backend-android es la implementación de gdx especifica para Android que depende de android.jar
Interfaz ApplicationListener y ciclo de vida
En el proyecto core debe existir una clase que implementa la interfaz ApplicationListener. Los objetos que manejan la aplicación en cada plataforma llaman a los métodos de la interfaz ApplicationListener para informar de eventos.
public interface ApplicationListener { public void create (); public void resize (int width, int height); public void render (); public void pause (); public void resume (); public void dispose (); }
Cada método corresponde a un evento que la aplicación usa informar y permitir a core ejecutar la lógica del Juego. Para entender estos eventos veamos cuando es llamado cada método.
Método | Description |
---|---|
create () | Se llama cuando la aplicación es creada (antes de iniciar el game loop). Es el lugar para realizar las inicializaciones de los objetos que usaremos en el juego. |
resize(int width, int height) | Se llama cuando las dimensiones de la pantalla cambian (si agrandamos la pantalla en desktop o cuando rotamos el telefono y cambia la orientación de la pantalla). Tambien se llama después de create(). Los parámetros indican el nuevo ancho y alto. |
render () | Se llama dentro del game loop. Aquí realizaremos el update y el render. |
pause () | Se llama antes de que la aplicación pierda el foco. Es el lugar para salvar el estado de la aplicación ya que en Android no esta garantizado la llamada a dispose(). |
resume () | En Android es llamado cuando se devuelve el control a la aplicación despues de pause(). En desktop no se utiliza. |
dispose () | Se llama cuando se destruye la aplicación. Cuidado: Android suele matar procesos de aplicación sin llamarlo. |
Estos métodos reflejan lo que se conoce como ciclo de vida de la aplicación. Este es un concepto tomado prestado de Android. Libgdx oculta el game loop que será manejado por cada implementación de la aplicación pero nos da el metodo render() que es todo lo que necesitamos.
Libgdx Launchers
Cada proyecto backend tiene una clase "Launcher" encargada de iniciar la ejecución. En cada clase lancher se crea una instancia del ApplicationListener del proyecto core. Todos los proyectos backend estan configurados para depender de core y asi tener acceso al ApplicationListener.
package com.mygdx.game.desktop; import com.badlogic.gdx.backends.lwjgl.LwjglApplication; import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration; import com.mygdx.game.MyGdxGame; public class DesktopLauncher { public static void main (String[] arg) { LwjglApplicationConfiguration config = new LwjglApplicationConfiguration(); new LwjglApplication(new MyGdxGame(), config); } }
Arriba se ve como en DesktopLauncher se crea una instancia de la clase MyGdxGame que implementa ApplicationListener. En el código fuente generado MyGdxGame extiende ApplicationAdapter que es quién implementa ApplicationListener.
Cuando se crea LwjglApplication que es la implementación de la aplicación para desktop se le entrega la instancia de ApplicationListener para que pueda informarle a core los eventos que ocurran en la aplicación.
Abajo podemos ver como se crea una instancia de MyGdxGame para AndroidLauncher.
package com.mygdx.game.android; import android.os.Bundle; import com.badlogic.gdx.backends.android.AndroidApplication; import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration; import com.mygdx.game.MyGdxGame; public class AndroidLauncher extends AndroidApplication { @Override protected void onCreate (Bundle savedInstanceState) { super.onCreate(savedInstanceState); AndroidApplicationConfiguration config = new AndroidApplicationConfiguration(); initialize(new MyGdxGame(), config); } } }
Testeando el ciclo de vida de libgdx
Para ver como funciona el ciclo de vida he creado un ApplicationListener llamado Tester que no imprime nada por pantalla (se ve una pantalla negra). Tester imprime mensajes en la consola cuando la aplicación llama a los metodos de ApplicationListener.
El método render es llamado muchas veces por segundo. Si imprimimos cada vez que es llamado nos llenaría la consola. Para evitar esto he agregado un código para que imprima solo cada 5 segundos.
Uso la función Gdx.graphics.getDeltaTime() del módulo grafico que nos da el tiempo desde la última llamada a render. En el proximo tutorial veremos los módulos de libgdx. Tambien veremos como reemplazar los poco profesionales System.out.println() usando las capacidades de logging de libgdx.
package com.mygdx.game; import com.badlogic.gdx.ApplicationListener; import com.badlogic.gdx.Gdx; public class Tester implements ApplicationListener{ private float timeFromLast ; private int counter ; @Override public void create() { System.out.println("call create()"); } @Override public void resize(int width, int height) { System.out.println("call resize()"); } @Override public void render() { float delta = Gdx.graphics.getDeltaTime(); timeFromLast = timeFromLast + delta; counter++; if (timeFromLast>5) { System.out.println(counter+" render() calls in "+timeFromLast+" seconds"); timeFromLast = 0; } } @Override public void pause() { System.out.println("call pause()"); } @Override public void resume() { System.out.println("call resume()"); } @Override public void dispose() { System.out.println("call dispose()"); } }
Probaremos a Tester usando desktop y Android con un disposito real. Si tienen dudas como usar un dispositivo Android para pruebas ver "Depurar en un teléfono o tablet Android usando Eclipse". Modificamos los launchers como vemos abajo.
package com.mygdx.game.desktop; import com.badlogic.gdx.backends.lwjgl.LwjglApplication; import com.badlogic.gdx.backends.lwjgl.LwjglApplicationConfiguration; import com.mygdx.game.Tester; public class DesktopLauncher { public static void main (String[] arg) { LwjglApplicationConfiguration config = new LwjglApplicationConfiguration(); new LwjglApplication(new Tester(), config); } }
package com.mygdx.game.android; import android.os.Bundle; import com.badlogic.gdx.backends.android.AndroidApplication; import com.badlogic.gdx.backends.android.AndroidApplicationConfiguration; import com.mygdx.game.Tester; public class AndroidLauncher extends AndroidApplication { @Override protected void onCreate (Bundle savedInstanceState) { super.onCreate(savedInstanceState); AndroidApplicationConfiguration config = new AndroidApplicationConfiguration(); initialize(new Tester(), config); } }
Si ejecutamos desktop podremos ver en la consola como se llama a create(), resize() y luego a render(). Si hacemos que nuestra aplicación pierda el foco y lo recupere podemos ver como se llama a pause() y resume(). Cuando cerramos la ventana se ve la llamada a dispose(). Es curioso que en desktop se sigue llamando a render() cuand estamos en pause().
call create() call resize() 220 render() calls in 5.0004935 seconds call pause() 520 render() calls in 5.0002027 seconds call resume() 820 render() calls in 5.0002413 seconds call pause() call dispose()
Para probar en Android conectamos el teléfono o tablet por USB y abrimos la vista Logcat en menú Windows - Show View - Other y en la ventana elegir Android - Logcat. Depuramos el proyecto Android (en el proyecto Android Debug As - Android Application).
En el dispositivo hacemos touch en Home, luego traemos de nuevo la aplicación al frente y después nos salimos con back. En Logcat (si lo tiene activado en el dispositivo) podrán ver la salida por consola.
Primero se llama a create(), resize() y render(). Después de Home se llama a pause() y cuando volvemos se llama a resize(), resume() y resize(). Cuando salimos con back se llama a dispose().
Es curioso notar que si salimos con Home y luego quitamos de memoria nuestra aplicación, dispose() no es llamado nunca.