Arquitectura y ciclo de vida de Libgdx

libgdx projectsLibgdx 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).

proyectos libgdx android y coreSi 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.

logcat life cycle

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.