Animaciones cuadro por cuadro en LibGDX

Introducción

A la hora de animar los elementos de nuestros videojuegos, uno de los métodos con los que contamos son las animaciones cuadro por cuadro (frame by frame). Es el método clásico de animación, y consiste en dibujar una serie de imágenes de forma que, al mostrarlas de forma secuencial, dan la sensación de que el elemento realiza alguna acción.

runnerPor ejemplo, si queremos mostrar un personaje corriendo, deberíamos tener una serie de imágenes, cada una con pequeño avance sobre la imagen anterior; de forma que al mostrarlas una detrás de otra, podamos ver como el personaje se mueve:

charset

 
 

Animaciones cuadro por cuadro en LibGDX

La clase Animation

La clase encargada de gestionar nuestras animaciones cuadro por cuadro es Animation. El trabajo de esta clase consiste en calcular el frame (cuadro) que debe pintarse en cada momento, siendo nosotros los encargados finales de dibujar el resultado.

Básicamente esta clase necesita conocer estos cuatro elementos:

  • Imágenes que componen la animación. Cada una de estas imágenes es un cuadro de nuestra animación.
  • Tiempo que debe transcurrir entre un cuadro y el siguiente.
  • Modo de animación. Están definidos en el enumerado Animation.PlayMode, e indica a nuestro objeto Animation qué orden deben seguir las imágenes para conseguir la animación deseada. Los modos posibles son:
    • NORMAL: Los cuadros se dibujan del primero al último. Sin repetición. Es decir, una vez se alcance el último cuadro, siempre se dibujará este último cuadro.
    • REVERSED: Los cuadros se dibujan del último al primero. Sin repetición.
    • LOOP: Los cuadros se dibujan del primero al último. Una vez llegado al último cuadro, se vuelve a empezar por el primero y así sucesivamente.
    • LOOP_REVERSED: Los cuadros se dibujan del último al primero. Una vez alcanzado el primer cuadro, se vuelve a comenzar por el último y así sucesivamente.
    • LOOP_PINGPONG: Los cuadros se dibujan del primero al último. Una vez llegado al último cuadro, se comienza a dibujar hacía atrás, del último al primero. Al llegar otra vez al primero, se vuelve a dibujar hacía delante, y así sucesivamente.
    • LOOP_RANDOM: Los cuadros se dibujan aleatoriamente, sin un orden definido.
  • Tiempo transcurrido desde el comienzo de la animación. Cada vez que dibujamos en pantalla, indicaremos a nuestra Animation el tiempo que ha transcurrido desde que comenzó la animación. En función del tiempo y el modo de animación configurado, nos devolverá el cuadro que debe ser dibujado.

 
 

Ejemplo

Normalmente, en un videojuego, cada personaje tendrá varias animaciones y se dibujará una animación u otra dependiendo del estado del personaje (andando, corriendo, saltando, golpeando, etc.). Para hacer este ejemplo lo más claro posible, voy a olvidarme de los estados y sólo mostraré un único elemento con una sola animación en pantalla.

 

Imágenes

Para este ejemplo utilizaré un archivo (charset.png) que contiene todas las imágenes que componen la animación:

charsetAdemás crearé el archivo charset.atlas, sabiendo que cada cuadro de la animación ocupa 80 píxeles de ancho por 120 de alto. En el atlas, utilizaré el mismo nombre para todos los cuadros, asignándoles un índice a cada uno de ellos. Quedará algo así:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
charset.png
format: RGBA8888
filter: Linear,Linear
repeat: none
running
  rotate: false
  xy: 0, 0
  size: 80, 120
  orig: 80, 120
  offset: 0, 0
  index: 0
running
  rotate: false
  xy: 80, 0
  size: 80, 120
  orig: 80, 120
  offset: 0, 0
  index: 1
running
  rotate: false
  xy: 160, 0
  size: 80, 120
  orig: 80, 120
  offset: 0, 0
  index: 2
(continúa)

De esta forma, podemos obtener fácilmente todos los cuadros que componen la animación:

public class GdxAnimationExample extends ApplicationAdapter {
    ...
    // Atlas con todos los cuadros definidos en "charset.atlas"
    private TextureAtlas charset;
    ...
 
    @Override
    public void create () {
        ...
        // Carga de todas las imagenes definidas en "charset.atlas"
        charset = new TextureAtlas( Gdx.files.internal("charset.atlas") );
 
        // Obtencion de las imagenes de la animacion "running"
        Array<AtlasRegion> runningFrames = charset.findRegions("running");
        ...
    }
    ...
}

 

Construir la animación

Una vez tenemos cargadas las imágenes que componen la animación, podemos construirla. Para este ejemplo repetiremos la animación de indefinidamente, así que usamos el modo PlayMode.LOOP:

public class GdxAnimationExample extends ApplicationAdapter {
    ...
    // Tiempo que permanece visible cada cuadro de la animacion
    private static float FRAME_DURATION = .05f;
 
    // Animacion del personaje corriendo
    private Animation runningAnimation;
    ...
 
    @Override
    public void create () {
        ...
        // Construcción de la animacion
        runningAnimation = new Animation(FRAME_DURATION, runningFrames, PlayMode.LOOP);
        ...
    }
    ...
}

 

Dibujar cada cuadro

A la hora de dibujar, el objeto runningAnimation nos devolerá el cuadro que debe ser dibujado en función del tiempo que haya transcurrido. Por ello, tenemos la variable local elapsed_time donde vamos acumulando el tiempo transcurrido desde que la animación comenzó:

public class GdxAnimationExample extends ApplicationAdapter {
    ...
    // Cuadro que debe ser dibujado en cada momento
    private TextureRegion currentFrame;
 
    // Acumulador del tiempo que lleva representada una animacion
    private float elapsed_time = 0f;
 
    // Variables auxiliares para saber donde pintar la imagen para que quede centrada
    private float origin_x, origin_y;
    ...
 
    @Override
    public void render () {
        ...
        // Se acumula el tiempo transcurrido
        elapsed_time += Gdx.graphics.getDeltaTime();
 
        // Se obtiene el cuadro que debe ser dibujado
        currentFrame = runningAnimation.getKeyFrame(elapsed_time);
 
        // Dibujo en pantalla
        batch.begin();
        batch.draw(currentFrame, origin_x, origin_y);
        batch.end();
        ...
    }
    ...
}

 

Ejemplo completo

Así queda la clase cuando unimos todo lo anterior:

14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
/**
 * Ejemplo de animacion frame-by-frame (cuadro por cuadro) con LibGDX:
 * 
 * http://www.pixnbgames.com/blog/libgdx/animaciones-cuadro-por-cuadro-en-libgdx
 * 
 * @author angel
 */
public class GdxAnimationExample extends ApplicationAdapter {
 
    // Tiempo que permanece visible cada cuadro de la animacion
    private static float FRAME_DURATION = .05f;
 
    // Para dibujar en pantalla
    private SpriteBatch batch;
 
    // Atlas con todos los cuadros definidos en "charset.atlas"
    private TextureAtlas charset;
 
    // Cuadro que debe ser dibujado en cada momento
    private TextureRegion currentFrame;
 
    // Animacion del personaje corriendo
    private Animation runningAnimation;
 
    // Acumulador del tiempo que lleva representada una animacion
    private float elapsed_time = 0f;
 
    // Variables auxiliares para saber donde pintar la imagen para que quede centrada
    private float origin_x, origin_y;
 
 
    @Override
        public void create () {
        batch = new SpriteBatch();
 
        // Carga de los cuadros definidos en "charset.atlas"
        charset = new TextureAtlas( Gdx.files.internal("charset.atlas") );
 
        // Se obtienen todas las imagenes para la animacion del personaje corriendo. Para hacer esto
        // lo mas facil posible, hemos definido todos los cuadros con el mismo nombre, "running", y
        // hemos asignado un indice (propiedad "index") a cada cuadro (ver "charset.atlas")
        Array<AtlasRegion> runningFrames = charset.findRegions("running");
 
        // Construcción de la animacion
        runningAnimation = new Animation(FRAME_DURATION, runningFrames, PlayMode.LOOP);
 
        // Calculo de la posicion en la que dibujar para que quede centrado en pantalla
        TextureRegion firstTexture = runningFrames.first();
        origin_x = (Gdx.graphics.getWidth()  - firstTexture.getRegionWidth())  / 2;
        origin_y = (Gdx.graphics.getHeight() - firstTexture.getRegionHeight()) / 2;
    }
 
 
    @Override
    public void render () {
        // Se limpia la pantalla aplicando un color naranja
        Gdx.gl.glClearColor(1.0f, .8f, .667f, 1);
        Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
 
        // Se acumula el tiempo que ha pasado desde la ultima vez que dibujamos. Con el tiempo total
        // transcurrido es como el objeto Animation sabe el cuadro que toca dibujar ahora
        elapsed_time += Gdx.graphics.getDeltaTime();
 
        // Obtencion del cuadro que debe ser dibujado
        currentFrame = runningAnimation.getKeyFrame(elapsed_time);
 
        // Dibujo del cuadro
        batch.begin();
        batch.draw(currentFrame, origin_x, origin_y);
        batch.end();
    }
 
 
    @Override
    public void dispose() {
        // Liberación de recursos
        charset.dispose();
        super.dispose();
    }
}

 

 

Código fuente

Puedes descargar el proyecto de ejemplo de mi repositorio de GitHub.

 

Saludos,

3 Replies to “Animaciones cuadro por cuadro en LibGDX”

  1. Muy buen artículo. En cuestión de rendimiento para la animación de un actor (movimiento en diagonal, horizontal o vertical), cual crees que es la mejor opción respecto al png, crear un png con todas las imágenes? O crear un png para cada tipo de movimiento?

    Muchas gracias!!

    • Hola Pablo,

      En general es preferible tener todas las imágenes dentro de un mismo PNG; es lo que se conoce como una sprite sheet. Cada imagen PNG será un textura que OpenGL deberá cargar en la GPU para ser renderizada. Al tener todas las imágenes dentro del mismo PNG, es posible hacer varias operaciones de renderizado sobre una misma textura de una sola vez. Mientras que tenerlas en archivos distintos obliga a cambiar constantemente las texturas cargadas en la GPU, y esto es una operación costosa.

      Existen otras razones para agrupar las imágenes dentro del mismo archivo. Por ejemplo, más archivos PNG probablemente implique que nuestro ejecutable final ocupe más espacio en disco. Esto puede no ser un problema para los ordenadores actuales, pero sí se debería tener en cuenta si programamos nuestro juego para dispositivos móviles.

      La última razón que se me ocurre, aunque esto es cuestión de gustos, es por comodidad. Seguramente tanto para programadores como para diseñadores, será más fácil tener todas las poses de cada personaje dentro del mismo archivo. Pensemos, por ejemplo, que decidimos cambiar el color a la ropa del personaje; será más cómodo hacerlo una vez sobre la imagen global, a tener que editar tantos archivos como animaciones existan.

      Espero haber aclarado tu duda.

      Gracias por comentar y un saludo!

Leave a Reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code> <del datetime=""> <em> <i> <q cite=""> <s> <strike> <strong>

*