Skip to main content

Animations

FlixelGDX supports two animation sources that share the same playback API:

  • Sparrow atlases — a .png sheet paired with a .xml file that names each region. Used by regular FlixelSprite objects and can be layered onto an Adobe Animate rig.
  • Adobe Animate rigs — the three-file export (spritemap1.png / spritemap1.json / Animation.json) that FlixelAnimateSprite plays 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:

FlixelAnimationController 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:

// 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:

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:

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");

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:

// 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

// 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();

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:

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).

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:

// 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:

anim.onFrameChanged.addListener(animData -> {
if (animData.getAnimationName().equals("attack") && animData.getFrameIndex() == 3) {
spawnHitbox();
}
});

onAnimationFinished fires once when a non-looping clip reaches its last frame:

anim.onAnimationFinished.addListener(animData -> {
if (animData.getAnimationName().equals("death")) {
Flixel.switchState(new 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:

FlixelAnimateSprite character = new FlixelAnimateSprite(x, y);
character.addSpritemapAndAnimation(
"character/spritemap1.png",
"character/spritemap1.json",
"character/Animation.json");
character.animation.playAnimation("Walk");
add(character);
info

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:

// 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:

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:

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.

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.

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.

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:

// 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

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);

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:

@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");
}
}

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.