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:
- Use Texture Atlases: Combine multiple animations into single texture atlases
- Pool Animations: Reuse animation instances for better performance
- State Management: Use state machines for complex character animations
- Event System: Implement frame events for sound effects and gameplay triggers
- 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.