Animations
FlixelGDX supports two animation sources that share the same playback API:
- Sparrow atlases — a
.pngsheet paired with a.xmlfile that names each region. Used by regularFlixelSpriteobjects and can be layered onto an Adobe Animate rig. - Adobe Animate rigs — the three-file export (
spritemap1.png/spritemap1.json/Animation.json) thatFlixelAnimateSpriteplays as a full nested-symbol timeline. The framework also includes out-of-the-box support for the Better Texture Atlas extension.
Both are driven through FlixelAnimationController. You call playAnimation, pause, reverse, and
listen to signals the same way regardless of which source loaded the frames.
Regular sprite animations
The animation controller
FlixelSprite does not create an animation controller until you need one. Call
ensureAnimation() to get it, creating it lazily if it does not exist yet. Once you have the
controller you load atlases, register clips, and play them:
- Java
- Kotlin
FlixelAnimationController anim = sprite.ensureAnimation();
anim.addSparrowAtlas("images/player"); // Assuming it's from the assets folder!
anim.addAnimationByPrefix("idle", "idle", 24, true);
anim.playAnimation("idle");
val anim = sprite.ensureAnimation()
anim.addSparrowAtlas("images/player") // Assuming it's from the assets folder!
anim.addAnimationByPrefix("idle", "idle", 24, true)
anim.playAnimation("idle")
After the first load you can also reach the controller through the nullable sprite.animation
field. ensureAnimation() is the safe entry point whenever you are not certain the controller
exists yet.
Loading a Sparrow atlas
addSparrowAtlas(path) takes a base path without any file extension. It loads
<path>.png as the texture and <path>.xml as the frame data:
- Java
- Kotlin
// Loads assets/images/player.png + assets/images/player.xml
sprite.ensureAnimation().addSparrowAtlas("images/player");
// Loads assets/images/player.png + assets/images/player.xml
sprite.ensureAnimation().addSparrowAtlas("images/player")
If the PNG and XML live at different paths, pass them explicitly:
- Java
- Kotlin
sprite.ensureAnimation().addSparrowAtlas(
"textures/player_sheet.png",
"data/player_atlas.xml");
sprite.ensureAnimation().addSparrowAtlas(
"textures/player_sheet.png",
"data/player_atlas.xml")
Merging multiple Sparrow atlases
A single sprite can pull its frames from more than one atlas sheet. Call addSparrowAtlas again
with each additional sheet and the controller merges the new frames into the existing pool without
disturbing any clips already registered. After the merge, addAnimationByPrefix can see frames
from all loaded sheets at once:
- Java
- Kotlin
FlixelAnimationController anim = sprite.ensureAnimation();
// First call: installs the primary atlas and sizes the sprite to the first frame.
anim.addSparrowAtlas("images/player_base");
// Second call: appends the extra sheet's frames without resetting existing clips.
anim.addSparrowAtlas("images/player_extras");
// Clips from both sheets are registered on the same controller.
anim.addAnimationByPrefix("idle", "idle", 24, true);
anim.addAnimationByPrefix("attack", "attack", 24, false); // Frames live in player_extras.
anim.addAnimationByPrefix("hurt", "hurt", 24, false);
anim.playAnimation("idle");
val anim = sprite.ensureAnimation()
// First call: installs the primary atlas and sizes the sprite to the first frame.
anim.addSparrowAtlas("assets/images/player_base")
// Second call: appends the extra sheet's frames without resetting existing clips.
anim.addSparrowAtlas("assets/images/player_extras")
// Clips from both sheets are registered on the same controller.
anim.addAnimationByPrefix("idle", "idle", 24, true)
anim.addAnimationByPrefix("attack", "attack", 24, false) // Frames live in player_extras.
anim.addAnimationByPrefix("hurt", "hurt", 24, false)
anim.playAnimation("idle")
This pattern is useful when a character's animations are too large to fit on a single texture or are split across separately exported sheets. You add as many sheets as you need and register clips across all of them — the controller treats the merged frame pool as one contiguous source.
Registering clips with addAnimationByPrefix
addAnimationByPrefix scans the frame pool for every frame whose name starts with the given
prefix, sorts them alphabetically, and registers that sorted sequence as a clip:
- Java
- Kotlin
// A Sparrow XML that contains "run0001", "run0002", "run0003", ...
anim.addAnimationByPrefix("run", "run", 24, true);
// A one-shot clip, which plays once and parks on the last frame.
anim.addAnimationByPrefix("death", "death", 24, false);
// A Sparrow XML that contains "run0001", "run0002", "run0003", ...
anim.addAnimationByPrefix("run", "run", 24, true)
// A one-shot clip, which plays once and parks on the last frame.
anim.addAnimationByPrefix("death", "death", 24, false)
Playing and controlling clips
- Java
- Kotlin
// Loop by default.
anim.playAnimation("run");
// Play once (parks on the last frame when done).
anim.playAnimation("attack", false);
// Play once and force a restart even if the clip is already playing.
anim.playAnimation("attack", false, true);
// Pause and resume.
anim.pause();
anim.resume();
// Check whether a non-looping clip has reached its last frame.
if (anim.isAnimationFinished()) { ... }
// Which clip is currently playing?
String current = anim.getCurrentAnim();
// Reverse the playback direction on the current clip.
anim.reverse();
// Loop by default.
anim.playAnimation("run")
// Play once (parks on the last frame when done).
anim.playAnimation("attack", false)
// Play once and force a restart even if the clip is already playing.
anim.playAnimation("attack", false, true)
// Pause and resume.
anim.pause()
anim.resume()
// Check whether a non-looping clip has reached its last frame.
if (anim.isAnimationFinished()) { ... }
// Which clip is currently playing?
val current = anim.getCurrentAnim()
// Reverse the playback direction on the current clip.
anim.reverse()
Per-animation offsets
Sparrow frames are trimmed: each frame is cropped to its tight bounding box and the original
canvas size is stored separately. When different clips are exported on differently sized canvases
the sprite's visual anchor can drift between clips. addOffset lets you register a per-clip
pixel nudge that is applied automatically every time that clip starts:
- Java
- Kotlin
anim.addAnimationByPrefix("idle", "BF idle dance", 24, true);
anim.addAnimationByPrefix("singLEFT", "BF NOTE LEFT", 24, false);
// Nudge this clip 12 pixels to the left and 6 pixels up to align with idle.
anim.addOffset("singLEFT", -12, 6);
anim.playAnimation("idle"); // Offset snaps to (0, 0).
anim.addAnimationByPrefix("idle", "BF idle dance", 24, true)
anim.addAnimationByPrefix("singLEFT", "BF NOTE LEFT", 24, false)
// Nudge this clip 12 pixels to the left and 6 pixels up to align with idle.
anim.addOffset("singLEFT", -12f, 6f)
anim.playAnimation("idle") // Offset snaps to (0, 0).
Once the first addOffset call is made the controller owns the sprite's offset. Playing a clip
with no registered offset resets it to (0, 0) so a previous clip's nudge does not carry
forward. Avoid mixing sprite.setOffset(...) with the addOffset API after the first offset is
registered.
centerOffsets() is a one-shot alternative that centers the hitbox inside the current frame's
untrimmed source box without touching the offset registry:
- Java
- Kotlin
// Center the hitbox inside the current frame's untrimmed canvas.
anim.centerOffsets();
// Also shift the sprite's position so its visual center stays put.
anim.centerOffsets(true);
// Center the hitbox inside the current frame's untrimmed canvas.
anim.centerOffsets()
// Also shift the sprite's position so its visual center stays put.
anim.centerOffsets(true)
Animation signals
Two signals fire on the controller during normal playback.
onFrameChanged fires every time the visible keyframe index changes. The dispatched
FlixelAnimationFrameSignalData carries the clip name, the frame index, and the frame itself:
- Java
- Kotlin
anim.onFrameChanged.addListener(animData -> {
if (animData.getAnimationName().equals("attack") && animData.getFrameIndex() == 3) {
spawnHitbox();
}
});
anim.onFrameChanged.addListener { animData ->
if (animData.animationName == "attack" && animData.frameIndex == 3) {
spawnHitbox()
}
}
onAnimationFinished fires once when a non-looping clip reaches its last frame:
- Java
- Kotlin
anim.onAnimationFinished.addListener(animData -> {
if (animData.getAnimationName().equals("death")) {
Flixel.switchState(new GameOverState());
}
});
anim.onAnimationFinished.addListener { animData ->
if (animData.animationName == "death") {
Flixel.switchState(GameOverState())
}
}
Adobe Animate rigs
FlixelAnimateSprite plays animations exported from Adobe Animate as texture atlas sets. It is a
drop-in replacement for FlixelSprite — position, velocity, physics, flipping, scaling, color
tint, and all other standard sprite properties work identically.
Loading a rig
An Adobe Animate export produces three files: a PNG atlas, a JSON spritemap layout, and an
Animation.json that holds clip and keyframe data. Pass all three to
addSpritemapAndAnimation:
- Java
- Kotlin
FlixelAnimateSprite character = new FlixelAnimateSprite(x, y);
character.addSpritemapAndAnimation(
"character/spritemap1.png",
"character/spritemap1.json",
"character/Animation.json");
character.animation.playAnimation("Walk");
add(character);
val character = FlixelAnimateSprite(x, y).apply {
addSpritemapAndAnimation(
"character/spritemap1.png",
"character/spritemap1.json",
"character/Animation.json")
animation?.playAnimation("Walk")
}
add(character)
When you call addSpritemapAndAnimation, it automatically creates an animation controller
on its own, so there's no need to call ensureAnimation or manually assign it!
Directory shorthand
If all three files live in the same folder with the conventional names (spritemap1.png,
spritemap1.json, Animation.json), pass the directory path instead:
- Java
- Kotlin
// Automatically loads all three required files from the provided folder.
character.addSpritemapAndAnimation("characters/my-character");
// Automatically loads all three required files from the provided folder.
character.addSpritemapAndAnimation("characters/my-character")
If your export uses different file names, override the static defaults once before you start loading:
- Java
- Kotlin
FlixelAnimateSprite.defaultSpritemapName = "atlas"; // Loads atlas.png + atlas.json
FlixelAnimateSprite.defaultAnimationName = "timeline"; // Loads timeline.json
FlixelAnimateSprite.defaultSpritemapName = "atlas" // Loads atlas.png + atlas.json
FlixelAnimateSprite.defaultAnimationName = "timeline" // Loads timeline.json
Merging multiple atlases
When a character's animation data spans more than one Animate export (for example, a base export
plus a separately authored DLC pack), call addSpritemapAndAnimation again with the next triple.
The first call builds the rig; every subsequent call appends its frames to the shared atlas and
bakes its clips into the existing anchor space. All clip names from every loaded sheet are
available on the same playAnimation call after the merge:
- Java
- Kotlin
FlixelAnimateSprite character = new FlixelAnimateSprite(x, y);
// First call: builds the rig from the base export.
character.addSpritemapAndAnimation(
"character/base/spritemap1.png",
"character/base/spritemap1.json",
"character/base/Animation.json");
// Second call: merges the extra export into the same rig.
character.addSpritemapAndAnimation(
"character/dlc/spritemap1.png",
"character/dlc/spritemap1.json",
"character/dlc/Animation.json");
// Clips from both exports are now reachable on the same controller.
character.animation.playAnimation("Walk"); // From base.
character.animation.playAnimation("DanceAlt"); // From DLC.
val character = FlixelAnimateSprite(x, y)
// First call: builds the rig from the base export.
character.addSpritemapAndAnimation(
"character/base/spritemap1.png",
"character/base/spritemap1.json",
"character/base/Animation.json")
// Second call: merges the extra export into the same rig.
character.addSpritemapAndAnimation(
"character/dlc/spritemap1.png",
"character/dlc/spritemap1.json",
"character/dlc/Animation.json")
// Clips from both exports are now reachable on the same controller.
character.animation?.playAnimation("Walk") // From base.
character.animation?.playAnimation("DanceAlt") // From DLC.
The body stays pinned to the anchor established by the first load. Names from a later sheet override earlier registrations if the same clip name appears in both exports.
Mixing in Sparrow atlases
A FlixelAnimateSprite can also carry Sparrow XML clips on the same body. Call
animation.addSparrowAtlas(...) after the rig is loaded. The sprite picks the right renderer
per clip automatically: rig clips draw through the baked-part pipeline; Sparrow clips draw
through the standard frame path.
- Java
- Kotlin
FlixelAnimateSprite character = new FlixelAnimateSprite(x, y);
// Rig clips.
character.addSpritemapAndAnimation("characters/my-character");
// Sparrow clips on the same body (possibly a simpler idle exported as a spritesheet).
character.animation.addSparrowAtlas("character/idle_loop");
character.animation.addAnimationByPrefix("idleLoop", "idle loop", 12, true);
// The sprite switches renderers per clip:
character.animation.playAnimation("Walk"); // Uses the rig.
character.animation.playAnimation("idleLoop"); // Uses the Sparrow frame.
val character = FlixelAnimateSprite(x, y)
// Rig clips.
character.addSpritemapAndAnimation("characters/my-character")
// Sparrow clips on the same body (possibly a simpler idle exported as a spritesheet).
character.animation?.addSparrowAtlas("character/idle_loop")
character.animation?.addAnimationByPrefix("idleLoop", "idle loop", 12, true)
// The sprite switches renderers per clip:
character.animation?.playAnimation("Walk") // Uses the rig.
character.animation?.playAnimation("idleLoop") // Uses the Sparrow frame.
Sizing and hitbox
The hitbox is derived from the anchor clip's bounding box and must be updated after loading and after any scale change:
- Java
- Kotlin
// Scale the rig and update the hitbox.
character.setScale(0.65f);
character.updateHitbox();
character.screenCenter();
// ...or set an explicit pixel size directly. The scale is computed automatically.
character.setGraphicSize(200, 300);
// Scale the rig and update the hitbox.
character.setScale(0.65f)
character.updateHitbox()
character.screenCenter()
// ...or set an explicit pixel size directly. The scale is computed automatically.
character.setGraphicSize(200, 300)
Animation state machine
When a character has more than a few clips, scattering playAnimation calls throughout your
update logic becomes hard to follow. FlixelAnimationStateMachine gathers the rules in one
place — which clips are legal from which states, what auto-plays when a one-shot clip ends, and
what side effects run on entry or exit.
Defining states
- Java
- Kotlin
FlixelAnimationStateMachine fsm = new FlixelAnimationStateMachine(player);
// allowTo(...) sets which other animation states (like idle) is allowed to transition to.
fsm.addState("idle", "idle").allowTo("run", "attack");
fsm.addState("run", "run").allowTo("idle", "attack");
fsm.addState("attack", "attack")
.autoAdvanceTo("idle") // Returns to idle automatically when the clip ends.
.onEnter(() -> sword.swing());
fsm.setState("idle");
// Link the machine so player.update(elapsed) ticks both automatically.
player.ensureAnimation().setStateMachine(fsm);
val fsm = FlixelAnimationStateMachine(player)
// allowTo(...) sets which other animation states (like idle) is allowed to transition to.
fsm.addState("idle", "idle").allowTo("run", "attack")
fsm.addState("run", "run").allowTo("idle", "attack")
fsm.addState("attack", "attack")
.autoAdvanceTo("idle") // Returns to idle automatically when the clip ends.
.onEnter { sword.swing() }
fsm.state = "idle"
// Link the machine so player.update(elapsed) ticks both automatically.
player.ensureAnimation().setStateMachine(fsm)
Driving the machine each frame
With the machine linked to the controller, player.update(elapsed) ticks both. Your gameplay
code only needs to describe the situation — calling setState with the already-active state is
a no-op, so you can call it unconditionally:
- Java
- Kotlin
@Override
public void update(float elapsed) {
super.update(elapsed);
if (attackPressed) {
fsm.setState("attack");
} else if (Math.abs(player.getVelocityX()) > 0.1f) {
fsm.setState("run");
} else {
fsm.setState("idle");
}
}
override fun update(elapsed: Float) {
super.update(elapsed)
when {
attackPressed -> fsm.state = "attack"
Math.abs(player.velocityX) > 0.1f -> fsm.state = "run"
else -> fsm.state = "idle"
}
}
The machine enforces the allowTo guards you defined, so calling fsm.setState("attack") while
the current state does not list "attack" as a legal target is silently rejected. This prevents
interrupting a death animation with an idle transition, for example, without needing any extra
guard logic in your update code.