Sprite Animation in LibGDX: Complete Guide

LibGDX provides powerful tools for creating smooth and efficient sprite animations. This comprehensive guide covers everything from basic animation to advanced techniques.

Project Setup

Maven Configuration

<!-- pom.xml -->
<properties>
<gdx.version>1.12.1</gdx.version>
</properties>
<dependencies>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx</artifactId>
<version>${gdx.version}</version>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-backend-lwjgl3</artifactId>
<version>${gdx.version}</version>
</dependency>
<dependency>
<groupId>com.badlogicgames.gdx</groupId>
<artifactId>gdx-platform</artifactId>
<version>${gdx.version}</version>
<classifier>natives-desktop</classifier>
</dependency>
</dependencies>

Basic Sprite Animation

Texture Atlas Animation

package com.game.animation;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.utils.Array;
public class BasicSpriteAnimation {
private Animation<TextureRegion> animation;
private float stateTime;
private boolean looping;
public BasicSpriteAnimation(String atlasPath, String regionPrefix, float frameDuration, PlayMode playMode) {
TextureAtlas atlas = new TextureAtlas(Gdx.files.internal(atlasPath));
// Get all regions with the specified prefix
Array<TextureAtlas.AtlasRegion> regions = atlas.findRegions(regionPrefix);
Array<TextureRegion> frames = new Array<>();
// Convert to TextureRegion array
for (TextureAtlas.AtlasRegion region : regions) {
frames.add(region);
}
this.animation = new Animation<>(frameDuration, frames);
this.animation.setPlayMode(playMode);
this.looping = (playMode == Animation.PlayMode.LOOP || playMode == Animation.PlayMode.LOOP_PINGPONG);
this.stateTime = 0f;
}
public void update(float delta) {
stateTime += delta;
}
public void render(SpriteBatch batch, float x, float y) {
TextureRegion currentFrame = animation.getKeyFrame(stateTime, looping);
batch.draw(currentFrame, x, y);
}
public void render(SpriteBatch batch, float x, float y, float width, float height) {
TextureRegion currentFrame = animation.getKeyFrame(stateTime, looping);
batch.draw(currentFrame, x, y, width, height);
}
public boolean isAnimationFinished() {
return animation.isAnimationFinished(stateTime);
}
public void reset() {
stateTime = 0f;
}
public void dispose() {
// Note: TextureAtlas should be managed separately
}
}

Sprite Sheet Animation

package com.game.animation;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.Texture;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.utils.Array;
public class SpriteSheetAnimation {
private Texture spriteSheet;
private Animation<TextureRegion> animation;
private float stateTime;
private int frameCols, frameRows;
public SpriteSheetAnimation(String texturePath, int frameCols, int frameRows, float frameDuration, Animation.PlayMode playMode) {
this.spriteSheet = new Texture(Gdx.files.internal(texturePath));
this.frameCols = frameCols;
this.frameRows = frameRows;
// Split texture into frames
TextureRegion[][] tmp = TextureRegion.split(
spriteSheet,
spriteSheet.getWidth() / frameCols,
spriteSheet.getHeight() / frameRows
);
// Convert 2D array to 1D array
Array<TextureRegion> frames = new Array<>();
for (int row = 0; row < frameRows; row++) {
for (int col = 0; col < frameCols; col++) {
frames.add(tmp[row][col]);
}
}
this.animation = new Animation<>(frameDuration, frames);
this.animation.setPlayMode(playMode);
this.stateTime = 0f;
}
public void update(float delta) {
stateTime += delta;
}
public void render(SpriteBatch batch, float x, float y) {
TextureRegion currentFrame = animation.getKeyFrame(stateTime, true);
batch.draw(currentFrame, x, y);
}
public void render(SpriteBatch batch, float x, float y, float width, float height) {
TextureRegion currentFrame = animation.getKeyFrame(stateTime, true);
batch.draw(currentFrame, x, y, width, height);
}
public void render(SpriteBatch batch, float x, float y, float originX, float originY, 
float width, float height, float scaleX, float scaleY, float rotation) {
TextureRegion currentFrame = animation.getKeyFrame(stateTime, true);
batch.draw(currentFrame, x, y, originX, originY, 
width, height, scaleX, scaleY, rotation);
}
public int getCurrentFrameIndex() {
return animation.getKeyFrameIndex(stateTime);
}
public boolean isAnimationFinished() {
return animation.isAnimationFinished(stateTime);
}
public void reset() {
stateTime = 0f;
}
public void dispose() {
spriteSheet.dispose();
}
}

Character Animation System

Animated Sprite Class

package com.game.animation;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Vector2;
public class AnimatedSprite {
private Vector2 position;
private Vector2 velocity;
private float stateTime;
private boolean flipped;
private float scale;
private Animation<TextureRegion> currentAnimation;
private String currentAnimationName;
// Animation states
public static final String IDLE = "idle";
public static final String WALK = "walk";
public static final String RUN = "run";
public static final String JUMP = "jump";
public static final String ATTACK = "attack";
public static final String HURT = "hurt";
public static final String DEATH = "death";
public AnimatedSprite(float x, float y) {
this.position = new Vector2(x, y);
this.velocity = new Vector2();
this.stateTime = 0f;
this.flipped = false;
this.scale = 1f;
}
public void update(float delta) {
// Update position based on velocity
position.x += velocity.x * delta;
position.y += velocity.y * delta;
// Update animation state time
stateTime += delta;
}
public void render(SpriteBatch batch) {
if (currentAnimation == null) return;
TextureRegion currentFrame = currentAnimation.getKeyFrame(stateTime, true);
float width = currentFrame.getRegionWidth() * scale;
float height = currentFrame.getRegionHeight() * scale;
// Handle flipping
if (!flipped) {
batch.draw(currentFrame, position.x, position.y, width, height);
} else {
batch.draw(currentFrame, 
position.x + width, position.y, 
-width, height);
}
}
public void setAnimation(Animation<TextureRegion> animation, String name) {
if (this.currentAnimation != animation) {
this.currentAnimation = animation;
this.currentAnimationName = name;
this.stateTime = 0f; // Reset animation when changing
}
}
public void setPosition(float x, float y) {
position.set(x, y);
}
public void setVelocity(float x, float y) {
velocity.set(x, y);
}
public void setFlipped(boolean flipped) {
this.flipped = flipped;
}
public void setScale(float scale) {
this.scale = scale;
}
public Vector2 getPosition() {
return position;
}
public Vector2 getVelocity() {
return velocity;
}
public String getCurrentAnimationName() {
return currentAnimationName;
}
public boolean isAnimationFinished() {
return currentAnimation != null && currentAnimation.isAnimationFinished(stateTime);
}
public TextureRegion getCurrentFrame() {
return currentAnimation != null ? currentAnimation.getKeyFrame(stateTime, true) : null;
}
public float getWidth() {
TextureRegion frame = getCurrentFrame();
return frame != null ? frame.getRegionWidth() * scale : 0;
}
public float getHeight() {
TextureRegion frame = getCurrentFrame();
return frame != null ? frame.getRegionHeight() * scale : 0;
}
}

Animation Manager

package com.game.animation;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.TextureAtlas;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ObjectMap;
public class AnimationManager {
private ObjectMap<String, Animation<TextureRegion>> animations;
private TextureAtlas atlas;
public AnimationManager(String atlasPath) {
this.animations = new ObjectMap<>();
this.atlas = new TextureAtlas(atlasPath);
}
public void createAnimation(String name, String regionPrefix, float frameDuration, Animation.PlayMode playMode) {
Array<TextureAtlas.AtlasRegion> regions = atlas.findRegions(regionPrefix);
Array<TextureRegion> frames = new Array<>();
for (TextureAtlas.AtlasRegion region : regions) {
frames.add(region);
}
Animation<TextureRegion> animation = new Animation<>(frameDuration, frames);
animation.setPlayMode(playMode);
animations.put(name, animation);
}
public void createDirectionalAnimation(String baseName, String[] directions, float frameDuration, Animation.PlayMode playMode) {
for (String direction : directions) {
String animationName = baseName + "_" + direction;
String regionPrefix = baseName + "/" + direction;
createAnimation(animationName, regionPrefix, frameDuration, playMode);
}
}
public Animation<TextureRegion> getAnimation(String name) {
return animations.get(name);
}
public boolean hasAnimation(String name) {
return animations.containsKey(name);
}
public void dispose() {
animations.clear();
if (atlas != null) {
atlas.dispose();
}
}
}

Advanced Animation Systems

State-Based Animation Controller

package com.game.animation;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
public class AnimationStateMachine {
public enum CharacterState {
IDLE, WALKING, RUNNING, JUMPING, FALLING, ATTACKING, HURT, DEAD
}
public enum CharacterDirection {
LEFT, RIGHT, UP, DOWN
}
private CharacterState currentState;
private CharacterDirection currentDirection;
private AnimationManager animationManager;
private AnimatedSprite sprite;
private String characterType;
// State timers
private float stateTimer;
private float attackCooldown;
private float hurtTimer;
public AnimationStateMachine(String characterType, AnimationManager animationManager, float startX, float startY) {
this.characterType = characterType;
this.animationManager = animationManager;
this.sprite = new AnimatedSprite(startX, startY);
this.currentState = CharacterState.IDLE;
this.currentDirection = CharacterDirection.RIGHT;
this.stateTimer = 0f;
this.attackCooldown = 0f;
this.hurtTimer = 0f;
updateAnimation();
}
public void update(float delta) {
// Update timers
stateTimer += delta;
if (attackCooldown > 0) attackCooldown -= delta;
if (hurtTimer > 0) hurtTimer -= delta;
// State transitions
handleStateTransitions();
// Update sprite
sprite.update(delta);
}
private void handleStateTransitions() {
// Handle automatic state transitions
switch (currentState) {
case HURT:
if (hurtTimer <= 0) {
setState(CharacterState.IDLE);
}
break;
case ATTACKING:
if (sprite.isAnimationFinished()) {
setState(CharacterState.IDLE);
}
break;
case DEAD:
// Stay in dead state
break;
}
}
public void setState(CharacterState newState) {
if (currentState == newState) return;
// Special cases for state transitions
if (newState == CharacterState.ATTACKING && attackCooldown > 0) {
return; // Can't attack while on cooldown
}
if (newState == CharacterState.HURT && currentState == CharacterState.DEAD) {
return; // Can't be hurt when dead
}
this.currentState = newState;
this.stateTimer = 0f;
// Set state-specific properties
switch (newState) {
case ATTACKING:
attackCooldown = 0.5f; // 500ms cooldown
break;
case HURT:
hurtTimer = 0.3f; // 300ms hurt duration
break;
case DEAD:
sprite.setVelocity(0, 0);
break;
}
updateAnimation();
}
public void setDirection(CharacterDirection direction) {
if (currentDirection == direction) return;
this.currentDirection = direction;
updateAnimation();
}
public void setMovement(float velocityX, float velocityY) {
if (currentState == CharacterState.DEAD) return;
sprite.setVelocity(velocityX, velocityY);
// Update state based on movement
if (currentState != CharacterState.ATTACKING && currentState != CharacterState.HURT) {
if (velocityX != 0 || velocityY != 0) {
setState(CharacterState.WALKING);
// Update direction based on movement
if (Math.abs(velocityX) > Math.abs(velocityY)) {
setDirection(velocityX > 0 ? CharacterDirection.RIGHT : CharacterDirection.LEFT);
} else if (velocityY != 0) {
setDirection(velocityY > 0 ? CharacterDirection.UP : CharacterDirection.DOWN);
}
} else {
setState(CharacterState.IDLE);
}
}
}
private void updateAnimation() {
String animationName = buildAnimationName();
Animation<TextureRegion> animation = animationManager.getAnimation(animationName);
if (animation != null) {
sprite.setAnimation(animation, animationName);
// Handle flipping for left/right directions
sprite.setFlipped(currentDirection == CharacterDirection.LEFT);
}
}
private String buildAnimationName() {
String stateName = currentState.name().toLowerCase();
String directionName = currentDirection.name().toLowerCase();
return characterType + "_" + stateName + "_" + directionName;
}
public void render(SpriteBatch batch) {
sprite.render(batch);
}
public CharacterState getCurrentState() {
return currentState;
}
public CharacterDirection getCurrentDirection() {
return currentDirection;
}
public AnimatedSprite getSprite() {
return sprite;
}
public void dispose() {
// Sprite disposal handled externally
}
}

Frame-Based Event System

package com.game.animation;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.utils.Array;
public class EventDrivenAnimation extends Animation<TextureRegion> {
public interface AnimationEventListener {
void onFrameEvent(int frameIndex, String eventName);
void onAnimationStart();
void onAnimationEnd();
void onAnimationLoop();
}
private Array<AnimationEventListener> listeners;
private ObjectMap<Integer, Array<String>> frameEvents;
private float previousStateTime;
private boolean wasFinished;
public EventDrivenAnimation(float frameDuration, Array<? extends TextureRegion> keyFrames, PlayMode playMode) {
super(frameDuration, keyFrames, playMode);
this.listeners = new Array<>();
this.frameEvents = new ObjectMap<>();
this.previousStateTime = -1f;
this.wasFinished = false;
}
public void addFrameEvent(int frameIndex, String eventName) {
Array<String> events = frameEvents.get(frameIndex);
if (events == null) {
events = new Array<>();
frameEvents.put(frameIndex, events);
}
events.add(eventName);
}
public void addListener(AnimationEventListener listener) {
listeners.add(listener);
}
public void removeListener(AnimationEventListener listener) {
listeners.removeValue(listener, true);
}
public void update(float stateTime) {
int currentFrameIndex = getKeyFrameIndex(stateTime);
int previousFrameIndex = getKeyFrameIndex(previousStateTime);
// Check for frame events
if (currentFrameIndex != previousFrameIndex) {
triggerFrameEvents(currentFrameIndex);
}
// Check for animation start
if (previousStateTime < 0 && stateTime >= 0) {
triggerAnimationStart();
}
// Check for animation end
boolean isFinished = isAnimationFinished(stateTime);
if (!wasFinished && isFinished) {
triggerAnimationEnd();
}
// Check for loop completion
if (getPlayMode() == PlayMode.LOOP || getPlayMode() == PlayMode.LOOP_PINGPONG) {
if (stateTime < previousStateTime) { // Animation looped
triggerAnimationLoop();
}
}
previousStateTime = stateTime;
wasFinished = isFinished;
}
private void triggerFrameEvents(int frameIndex) {
Array<String> events = frameEvents.get(frameIndex);
if (events != null) {
for (AnimationEventListener listener : listeners) {
for (String event : events) {
listener.onFrameEvent(frameIndex, event);
}
}
}
}
private void triggerAnimationStart() {
for (AnimationEventListener listener : listeners) {
listener.onAnimationStart();
}
}
private void triggerAnimationEnd() {
for (AnimationEventListener listener : listeners) {
listener.onAnimationEnd();
}
}
private void triggerAnimationLoop() {
for (AnimationEventListener listener : listeners) {
listener.onAnimationLoop();
}
}
}

Particle and Effect Animations

Animated Particle System

package com.game.animation;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.math.Vector2;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.Pool;
public class AnimatedParticleSystem {
public static class AnimatedParticle implements Pool.Poolable {
public Vector2 position;
public Vector2 velocity;
public Vector2 acceleration;
public float rotation;
public float rotationSpeed;
public float scale;
public float scaleSpeed;
public float alpha;
public float alphaSpeed;
public float life;
public float maxLife;
public Animation<TextureRegion> animation;
public float stateTime;
public AnimatedParticle() {
this.position = new Vector2();
this.velocity = new Vector2();
this.acceleration = new Vector2();
reset();
}
public void init(float x, float y, Animation<TextureRegion> animation, float life) {
this.position.set(x, y);
this.animation = animation;
this.life = life;
this.maxLife = life;
this.stateTime = 0f;
this.scale = 1f;
this.alpha = 1f;
}
public boolean update(float delta) {
// Update physics
velocity.x += acceleration.x * delta;
velocity.y += acceleration.y * delta;
position.x += velocity.x * delta;
position.y += velocity.y * delta;
// Update properties
rotation += rotationSpeed * delta;
scale += scaleSpeed * delta;
alpha += alphaSpeed * delta;
alpha = Math.max(0, Math.min(1, alpha)); // Clamp alpha
// Update animation
stateTime += delta;
// Update life
life -= delta;
return life > 0;
}
public void render(SpriteBatch batch) {
if (animation == null || alpha <= 0) return;
TextureRegion frame = animation.getKeyFrame(stateTime, true);
batch.setColor(1, 1, 1, alpha);
float width = frame.getRegionWidth() * scale;
float height = frame.getRegionHeight() * scale;
batch.draw(frame, 
position.x - width / 2, position.y - height / 2,
width / 2, height / 2,
width, height,
1, 1, rotation);
batch.setColor(1, 1, 1, 1); // Reset color
}
@Override
public void reset() {
position.set(0, 0);
velocity.set(0, 0);
acceleration.set(0, 0);
rotation = 0;
rotationSpeed = 0;
scale = 1f;
scaleSpeed = 0;
alpha = 1f;
alphaSpeed = 0;
life = 0;
maxLife = 0;
animation = null;
stateTime = 0;
}
}
private Pool<AnimatedParticle> particlePool;
private Array<AnimatedParticle> activeParticles;
private AnimationManager animationManager;
public AnimatedParticleSystem(AnimationManager animationManager, int initialCapacity) {
this.animationManager = animationManager;
this.particlePool = new Pool<AnimatedParticle>(initialCapacity, initialCapacity * 2) {
@Override
protected AnimatedParticle newObject() {
return new AnimatedParticle();
}
};
this.activeParticles = new Array<>();
}
public void createParticle(float x, float y, String animationName, 
float life, Vector2 velocity, Vector2 acceleration) {
Animation<TextureRegion> animation = animationManager.getAnimation(animationName);
if (animation == null) return;
AnimatedParticle particle = particlePool.obtain();
particle.init(x, y, animation, life);
particle.velocity.set(velocity);
particle.acceleration.set(acceleration);
activeParticles.add(particle);
}
public void createExplosion(float x, float y, int count, float spread, float life) {
for (int i = 0; i < count; i++) {
Vector2 velocity = new Vector2(
(float) (Math.random() - 0.5) * spread,
(float) (Math.random() - 0.5) * spread
);
Vector2 acceleration = new Vector2(0, -50f); // Gravity
String[] explosionAnims = {"explosion_small", "explosion_medium", "explosion_large"};
String animName = explosionAnims[(int) (Math.random() * explosionAnims.length)];
createParticle(x, y, animName, life, velocity, acceleration);
}
}
public void update(float delta) {
for (int i = activeParticles.size - 1; i >= 0; i--) {
AnimatedParticle particle = activeParticles.get(i);
if (!particle.update(delta)) {
activeParticles.removeIndex(i);
particlePool.free(particle);
}
}
}
public void render(SpriteBatch batch) {
for (AnimatedParticle particle : activeParticles) {
particle.render(batch);
}
}
public void clear() {
particlePool.freeAll(activeParticles);
activeParticles.clear();
}
public int getActiveParticleCount() {
return activeParticles.size;
}
}

UI and HUD Animations

Animated UI Elements

package com.game.animation;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.Batch;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.scenes.scene2d.Actor;
import com.badlogic.gdx.scenes.scene2d.ui.Image;
import com.badlogic.gdx.scenes.scene2d.utils.TextureRegionDrawable;
public class AnimatedImage extends Image {
private Animation<TextureRegion> animation;
private float stateTime;
private boolean playing;
private boolean looping;
public AnimatedImage(Animation<TextureRegion> animation) {
super(animation.getKeyFrame(0));
this.animation = animation;
this.stateTime = 0f;
this.playing = true;
this.looping = true;
}
@Override
public void act(float delta) {
super.act(delta);
if (playing) {
stateTime += delta;
TextureRegion frame = animation.getKeyFrame(stateTime, looping);
((TextureRegionDrawable) getDrawable()).setRegion(frame);
}
}
public void play() {
playing = true;
}
public void pause() {
playing = false;
}
public void stop() {
playing = false;
stateTime = 0f;
TextureRegion frame = animation.getKeyFrame(0);
((TextureRegionDrawable) getDrawable()).setRegion(frame);
}
public void setLooping(boolean looping) {
this.looping = looping;
}
public boolean isAnimationFinished() {
return animation.isAnimationFinished(stateTime);
}
public void setAnimation(Animation<TextureRegion> animation) {
this.animation = animation;
this.stateTime = 0f;
TextureRegion frame = animation.getKeyFrame(0);
((TextureRegionDrawable) getDrawable()).setRegion(frame);
}
}
// Animated Button with hover/click animations
public class AnimatedButton extends Actor {
private Animation<TextureRegion> normalAnimation;
private Animation<TextureRegion> hoverAnimation;
private Animation<TextureRegion> clickAnimation;
private Animation<TextureRegion> currentAnimation;
private float stateTime;
private boolean isHovered;
private boolean isClicked;
public AnimatedButton(Animation<TextureRegion> normal, 
Animation<TextureRegion> hover, 
Animation<TextureRegion> click) {
this.normalAnimation = normal;
this.hoverAnimation = hover;
this.clickAnimation = click;
this.currentAnimation = normal;
this.stateTime = 0f;
// Set size based on first frame
TextureRegion firstFrame = normal.getKeyFrame(0);
setSize(firstFrame.getRegionWidth(), firstFrame.getRegionHeight());
}
@Override
public void act(float delta) {
super.act(delta);
stateTime += delta;
// Update animation based on state
if (isClicked) {
currentAnimation = clickAnimation;
} else if (isHovered) {
currentAnimation = hoverAnimation;
} else {
currentAnimation = normalAnimation;
}
}
@Override
public void draw(Batch batch, float parentAlpha) {
TextureRegion frame = currentAnimation.getKeyFrame(stateTime, true);
batch.draw(frame, getX(), getY(), getWidth(), getHeight());
}
public void setHovered(boolean hovered) {
if (this.isHovered != hovered) {
this.isHovered = hovered;
if (!hovered) {
stateTime = 0f; // Reset animation when exiting hover
}
}
}
public void setClicked(boolean clicked) {
this.isClicked = clicked;
if (!clicked) {
stateTime = 0f; // Reset animation when releasing click
}
}
public boolean isAnimationFinished() {
return currentAnimation.isAnimationFinished(stateTime);
}
}

Complete Game Example

Main Game Class with Animation

package com.game;
import com.badlogic.gdx.ApplicationAdapter;
import com.badlogic.gdx.Gdx;
import com.badlogic.gdx.graphics.GL20;
import com.badlogic.gdx.graphics.OrthographicCamera;
import com.badlogic.gdx.graphics.g2d.SpriteBatch;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.game.animation.AnimationManager;
import com.game.animation.AnimationStateMachine;
import com.game.animation.AnimatedParticleSystem;
public class AnimationGame extends ApplicationAdapter {
private OrthographicCamera camera;
private SpriteBatch batch;
private AnimationManager animationManager;
private AnimationStateMachine player;
private AnimatedParticleSystem particleSystem;
private float gameTime;
@Override
public void create() {
// Setup camera
float aspectRatio = (float) Gdx.graphics.getWidth() / Gdx.graphics.getHeight();
camera = new OrthographicCamera(800, 800 / aspectRatio);
camera.position.set(camera.viewportWidth / 2, camera.viewportHeight / 2, 0);
camera.update();
batch = new SpriteBatch();
// Initialize animation system
animationManager = new AnimationManager("animations/characters.atlas");
setupAnimations();
// Create player
player = new AnimationStateMachine("knight", animationManager, 100, 100);
// Create particle system
particleSystem = new AnimatedParticleSystem(animationManager, 100);
gameTime = 0f;
}
private void setupAnimations() {
// Setup knight animations
String[] directions = {"left", "right", "up", "down"};
// Idle animations
for (String dir : directions) {
animationManager.createAnimation("knight_idle_" + dir, 
"knight/idle/" + dir, 0.2f, Animation.PlayMode.LOOP);
}
// Walk animations
for (String dir : directions) {
animationManager.createAnimation("knight_walk_" + dir, 
"knight/walk/" + dir, 0.1f, Animation.PlayMode.LOOP);
}
// Attack animations
for (String dir : directions) {
animationManager.createAnimation("knight_attack_" + dir, 
"knight/attack/" + dir, 0.05f, Animation.PlayMode.NORMAL);
}
// Setup particle animations
animationManager.createAnimation("explosion_small", "effects/explosion_small", 0.05f, Animation.PlayMode.NORMAL);
animationManager.createAnimation("explosion_medium", "effects/explosion_medium", 0.05f, Animation.PlayMode.NORMAL);
animationManager.createAnimation("explosion_large", "effects/explosion_large", 0.05f, Animation.PlayMode.NORMAL);
}
@Override
public void render() {
// Clear screen
Gdx.gl.glClearColor(0.2f, 0.2f, 0.3f, 1);
Gdx.gl.glClear(GL20.GL_COLOR_BUFFER_BIT);
float delta = Gdx.graphics.getDeltaTime();
gameTime += delta;
// Handle input
handleInput();
// Update game objects
player.update(delta);
particleSystem.update(delta);
// Auto-spawn some particles for demo
if (gameTime % 2.0f < delta) {
particleSystem.createExplosion(
(float) (Math.random() * 600 + 100),
(float) (Math.random() * 400 + 100),
10, 100, 2.0f
);
}
// Render
batch.setProjectionMatrix(camera.combined);
batch.begin();
particleSystem.render(batch);
player.render(batch);
batch.end();
}
private void handleInput() {
float speed = 100f; // pixels per second
// Simple movement input
float moveX = 0, moveY = 0;
if (Gdx.input.isKeyPressed(com.badlogic.gdx.Input.Keys.LEFT)) {
moveX = -speed;
}
if (Gdx.input.isKeyPressed(com.badlogic.gdx.Input.Keys.RIGHT)) {
moveX = speed;
}
if (Gdx.input.isKeyPressed(com.badlogic.gdx.Input.Keys.UP)) {
moveY = speed;
}
if (Gdx.input.isKeyPressed(com.badlogic.gdx.Input.Keys.DOWN)) {
moveY = -speed;
}
player.setMovement(moveX, moveY);
// Attack input
if (Gdx.input.isKeyJustPressed(com.badlogic.gdx.Input.Keys.SPACE)) {
player.setState(AnimationStateMachine.CharacterState.ATTACKING);
}
}
@Override
public void resize(int width, int height) {
camera.viewportWidth = 800;
camera.viewportHeight = 800 * height / width;
camera.update();
}
@Override
public void dispose() {
batch.dispose();
animationManager.dispose();
particleSystem.clear();
}
}

Performance Optimization

Animation Pooling System

package com.game.animation;
import com.badlogic.gdx.graphics.g2d.Animation;
import com.badlogic.gdx.graphics.g2d.TextureRegion;
import com.badlogic.gdx.utils.Array;
import com.badlogic.gdx.utils.ObjectMap;
import com.badlogic.gdx.utils.Pool;
public class AnimationPool {
private static class AnimationInstance implements Pool.Poolable {
Animation<TextureRegion> animation;
float stateTime;
boolean inUse;
@Override
public void reset() {
stateTime = 0f;
inUse = false;
}
}
private ObjectMap<String, Pool<AnimationInstance>> animationPools;
private ObjectMap<String, Animation<TextureRegion>> templateAnimations;
private Array<AnimationInstance> activeInstances;
public AnimationPool() {
animationPools = new ObjectMap<>();
templateAnimations = new ObjectMap<>();
activeInstances = new Array<>();
}
public void registerAnimation(String name, Animation<TextureRegion> template, int poolSize) {
templateAnimations.put(name, template);
Pool<AnimationInstance> pool = new Pool<AnimationInstance>(poolSize, poolSize * 2) {
@Override
protected AnimationInstance newObject() {
AnimationInstance instance = new AnimationInstance();
instance.animation = new Animation<>(
template.getFrameDuration(),
template.getKeyFrames(),
template.getPlayMode()
);
return instance;
}
};
// Pre-fill pool
Array<AnimationInstance> prefill = new Array<>();
for (int i = 0; i < poolSize; i++) {
prefill.add(pool.obtain());
}
pool.freeAll(prefill);
animationPools.put(name, pool);
}
public AnimationInstance obtainAnimation(String name) {
Pool<AnimationInstance> pool = animationPools.get(name);
if (pool == null) return null;
AnimationInstance instance = pool.obtain();
instance.inUse = true;
instance.reset();
activeInstances.add(instance);
return instance;
}
public void freeAnimation(AnimationInstance instance) {
if (instance.inUse) {
instance.inUse = false;
activeInstances.removeValue(instance, true);
// Find the right pool and free the instance
for (ObjectMap.Entry<String, Pool<AnimationInstance>> entry : animationPools) {
if (entry.value.getFree() > 0) { // This is a hack - better approach needed
entry.value.free(instance);
break;
}
}
}
}
public void update(float delta) {
for (int i = activeInstances.size - 1; i >= 0; i--) {
AnimationInstance instance = activeInstances.get(i);
instance.stateTime += delta;
// Auto-free finished non-looping animations
if (instance.animation.getPlayMode() == Animation.PlayMode.NORMAL && 
instance.animation.isAnimationFinished(instance.stateTime)) {
freeAnimation(instance);
}
}
}
public void dispose() {
for (Pool<AnimationInstance> pool : animationPools.values()) {
pool.clear();
}
animationPools.clear();
templateAnimations.clear();
activeInstances.clear();
}
}

Conclusion

This comprehensive LibGDX sprite animation system provides:

Key Features:

  • Basic sprite animation with texture atlases and sprite sheets
  • Character animation system with state machines
  • Event-driven animations for frame-specific actions
  • Particle systems with animated effects
  • UI animations for interactive elements
  • Performance optimization with pooling systems

Best Practices:

  1. Use Texture Atlases: Combine multiple animations into single texture atlases
  2. Pool Animations: Reuse animation instances for better performance
  3. State Management: Use state machines for complex character animations
  4. Event System: Implement frame events for sound effects and gameplay triggers
  5. Memory Management: Properly dispose of textures and animations

Performance Tips:

  • Use texture atlases to minimize texture binds
  • Implement object pooling for frequently used animations
  • Batch similar animations together
  • Use appropriate frame rates (15-30 FPS for most game animations)
  • Consider animation LOD (Level of Detail) for distant objects

This system is scalable and can be extended for complex 2D games with rich animation requirements.

Leave a Reply

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


Macro Nepal Helper