libGDX: Texture packer y sprites: caminar, saltar, caer, etc

En este tutorial vamos a utilizar la herramienta llamada Texture Packer para juntar todas las imágenes independientes en una sola gran imagen (llamado sprite sheet), después la vamos a cargar en nuestro programa y vamos a crear un personaje con animaciones.

Antes de comenzar recuerda que puedes descargar el código completo en Github y si lo prefieres puedes ver el videotutorial de este articulo.

También puedes correr la aplicación de este tutorial en tu navegador.

En las siguientes imagenes podemos ver al personaje con animaciones de caminar, saltar, etc y como quedarian los sprites empaquetados en un gran sprite sheet:

¿Por qué queremos empaquetar las imágenes?

En OpenGL cada vez que queremos dibujar una escena en la pantalla tenemos que cargar una imagen después se dibuja, después cargamos otra imagen y dibujamos, después cargamos otra imagen y dibujamos. Todo este proceso de cargar imágenes es muy costoso así que lo mejor es empaquetar todas las imágenes en una sola, carga 1 sola imagen y solo dibujar fragmentos de esta gran imagen, el resultado es que solo vamos a cargar la imagen una vez.

Algunas ventajas son:

  • Reduce el uso de espacio al comprimir las imágenes
  • Incrementa los frames por segundo ya que no hay que cargar múltiples veces las imágenes
  • Animaciones más fluidas ya que al comprimir permite el uso de más sprites para mejorar las animaciones

Si quieres saber más te invito a ver estos 2 videos que explican muy bien las ventajas de empaquetar las imágenes (Videos en inglés):

Texture Packer GUI

Para comenzar descarga la herramienta de Texture Packer GUI y también vamos a utilizar algunos sprites que podemos descargar aquí. Una vez que tenemos todo abrimos el texture packer y seleccionamos las imágenes.

Para este tutorial solo vamos a utilizar un personaje y las animaciones de: caminar, saltar, caer, agacharse, y estar sin movimiento.

La siguiente imagen contiene las configuraciones que seleccione para empaquetar las imágenes y crear el sprite sheet.

Al crear el sprite sheet se creará 2 archivos que vamos a copiar a nuestra carpeta de Assets:

Cargar las sprites con libGDX

Vamos a crear una clase llamada AssetsLearn8 para cargar los sprites y utilizarlos más adelante:

public class AssetsLearn8 {  
  
	static Sprite duck;  
	static Sprite fall;  
	static Sprite idle;  
	static Sprite jump;  
  
	static Animation<Sprite> walk;  
	static TextureAtlas atlas;  
  
	public static void load() {  
		atlas = new TextureAtlas(Gdx.files.internal("data/learn8/learn8.txt"));  
	  
		duck = atlas.createSprite("character_robot_duck");  
		fall = atlas.createSprite("character_robot_fall");  
		idle = atlas.createSprite("character_robot_idle");  
		jump = atlas.createSprite("character_robot_jump");  
		
		walk = new Animation<>(  
			Robot.WALK_FRAME_DURATION,  
			atlas.createSprite("character_robot_walk0"),  
			atlas.createSprite("character_robot_walk1"),  
			atlas.createSprite("character_robot_walk2"),  
			atlas.createSprite("character_robot_walk3"),  
			atlas.createSprite("character_robot_walk4"),  
			atlas.createSprite("character_robot_walk5"),  
			atlas.createSprite("character_robot_walk6"),  
			atlas.createSprite("character_robot_walk7"));  
	}  
  
	public static void dispose() {  
		atlas.dispose();  
	}  
}

Como pueden ver estamos utilizando la clase Sprite y Animation<Sprite> para cargar las imágenes. La clase Sprite tiene la ventaja que mantiene algunas de las propiedades que se crearon en el Texture Packer. Por ejemplo si el Texture Packer roto la imagen o eliminar los píxeles transparentes la clase Sprite la va mostrar correctamente al momento de dibujar en pantalla.

Crear la clase Robot

Esta clase mantiene todas las propiedades del personaje y su estado actual. Esta clase nos va a ayudar a saber si el personaje está saltando, caminando, etc. También va a contener algunas constantes como el ancho, la altura, la velocidad de movimiento, etc.

public class Robot {
	static final float WIDTH = .45f;
	static final float HEIGHT = .6f;
	
	// El ancho y alto de objeto de Box2d puede ser diferente al ancho y alto de dibujo
	static final float DRAW_WIDTH = 1.3f;
	static final float DRAW_HEIGHT = 1.7f;
	
	// Duración de cada frame de la animación de caminar
	static final float WALK_FRAME_DURATION = 0.05f;
	static final float WALK_SPEED = 3; // Velocidad cuando el robot caminan en X
	static final float JUMP_SPEED = 8; // Velocidad cuando el robot salta en Y
	
	boolean isJumping; // Nos dice si el robot está saltando
	boolean isFalling; // Nos dice si el robot está cayendo
	boolean isWalking; // Nos dice si el robot está caminando
	boolean isDucking; // Nos dice si el robot está agachado

	float stateTime = 0;

	Vector2 position;
	Vector2 velocity;

	// Nos dice si el robot tiene que saltar en la siguiente actualización
	private boolean didJump = false;

	// Nos dice si el robot se tiene que agachar en la siguiente actualización
	private boolean didDuck = false;

	public Robot(float x, float y) {
		position = new Vector2(x, y);
	}

	public void update(Body body, float delta, float accelX) {
		position.x = body.getPosition().x;
		position.y = body.getPosition().y;

		velocity = body.getLinearVelocity();

		isDucking = false;

		if (didDuck) {
			isDucking = true;
			didDuck = false;
			stateTime = 0;
		}

		if (didJump) {
			didJump = false;
			isJumping = true;
			stateTime = 0;
			velocity.y = JUMP_SPEED;
		}

		if (accelX == -1) {
			velocity.x = -WALK_SPEED;
			// Si está saltando o cayendo no puede caminar
			isWalking = !isJumping && !isFalling;
		} else if (accelX == 1) {
			velocity.x = WALK_SPEED;
			// Si está saltando o cayendo no puede caminar
			isWalking = !isJumping && !isFalling;
		} else {
			velocity.x = 0;
			isWalking = false;
		}

		if (isJumping) {
			// Si está saltando y la velocidad en y es igual a cero o menos significa
			// que está apunto de caer y hay que cambiar las propiedades
			if (velocity.y <= 0) {
				isJumping = false;
				isFalling = true;
				stateTime = 0;
			}
		} else if (isFalling) {
				if (velocity.y >= 0) {
					isFalling = false;
					stateTime = 0;
				}
		}
	
		body.setLinearVelocity(velocity);
		stateTime += delta;
	}
	
	public void jump() {
		if (!isJumping && !isFalling) {
			didJump = true;
		}
	}

	public void duck() {
		if (!isJumping && !isFalling && !isWalking) {
			didDuck = true;
		}
	}
}

Clase Learn8

Esta clase es muy parecida a los tutoriales anteriores. Vamos a tener una referencia a la clase World Robot, a los Cuerpos (Body). Para este tutorial lo importante van a ser las funciones de update y draw.

La función update tiene el trabajo de revisar si el usuario esta presionando alguna tecla para hacer caminar al personaje o hacerlo saltar:

public void update(float delta) {

   float accelX = 0;
   
   if (Gdx.input.isKeyPressed(Input.Keys.LEFT))
   	accelX = -1;
   else if (Gdx.input.isKeyPressed(Input.Keys.RIGHT))
   	accelX = 1;
   
   if (Gdx.input.isKeyPressed(Input.Keys.DOWN)) {
   	robot.duck();
   }

   if (Gdx.input.justTouched() || Gdx.input.isKeyPressed(Input.Keys.SPACE)) {
   	robot.jump();
   }

   oWorld.step(delta, 8, 6);
   oWorld.getBodies(arrBodies);

   for (Body body : arrBodies) {
   	if (body.getUserData() instanceof Robot) {
   		Robot obj = (Robot) body.getUserData();
   		obj.update(body, delta, accelX);
   	}
   }
}

La función draw va llamar a una función llamada drawRobot para dibujar al personaje dependiendo de las propiedades y estado:

private void drawRobot() {
	
	Sprite keyframe = AssetsLearn8.idle;
	
	if (robot.isJumping) {
		keyframe = AssetsLearn8.jump;
	} else if (robot.isFalling) {
		keyframe = AssetsLearn8.fall;
	} else if (robot.isWalking) {
		keyframe = AssetsLearn8.walk.getKeyFrame(robot.stateTime, true);
	} else if (robot.isDucking) {
		keyframe = AssetsLearn8.duck;
	}
 
	// Si la velocidad es negativa el sprite debe mirar hacia la izquierda
	if (robot.velocity.x < 0) {
		keyframe.setPosition(robot.position.x + Robot.DRAW_WIDTH / 2,
		robot.position.y - Robot.DRAW_HEIGHT / 2 + .25f);
		keyframe.setSize(-Robot.DRAW_WIDTH, Robot.DRAW_HEIGHT);
	} else {
		keyframe.setPosition(robot.position.x - Robot.DRAW_WIDTH / 2,
		robot.position.y - Robot.DRAW_HEIGHT / 2 + .25f);
		keyframe.setSize(Robot.DRAW_WIDTH, Robot.DRAW_HEIGHT);
	}
	
	keyframe.draw(spriteBatch);
}
Comparte este artículo