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.
Por 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:
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 objetoAnimationqué 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:
Ademá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,

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!
Muchas gracias!!! me ha quedado claro 😀