Skip to content
Go back

How to use animated sprites and animations with Excalibur and the LDtk level editor

I have been experimenting with game development recently. Excalibur seems like a great 2D game engine for Typescript and LDtk is a very intuitive level editor that is compatible.

Sadly, it does not have support for configuring animated sprites, and it seems like at least more complex player animations are unlikely to be on the roadmap.

I wanted to share the solution I chose, in case it helps someone. With this, you can configure animations directly in LDtk and visually select and see the tiles from the tileset in the level editor (however, sadly not the animation itself).

To do so, we set up an entity and add a custom Array<Tile> field for each animation, with a number of values equal to the animation length.

Custom Array<Tile> field for an entity with animations
Custom Array<Tile> field for an entity with animations

Now, we can configure these animations directly in the editor after placing an entity. If an animation is mission, errors will be shown and selecting sprites for the animation is intuitive because you can see the tile you have selected :).

Characters can be configured with animations based on the custom tile field
Characters can be configured with animations based on the custom tile field

When loading an entity from the exported level, we can create animations based on the information in the fields (but have to manually map the animation names to the field names):

Do you need help with software engineering? I can help and am available for freelance work.

Send me an Email ↗
import type { LdtkEntityInstance } from "@excaliburjs/plugin-ldtk";
import * as ex from "excalibur";

interface LdtkFieldInstance {
  __identifier: string;
  __value?: unknown;
}

interface LdtkTileRect {
  x: number;
  y: number;
  w?: number;
  h?: number;
}

// Helper to create animation from LDTK Tile field
const createAnimation = async (
  imageSource: string,
  entity: LdtkEntityInstance,
  fieldName: string,
  animationFrameDuration: number,
  isIdle: boolean
): Promise<ex.Animation> => {
  const tilesField = entity
    .fieldInstances
    .find((f: LdtkFieldInstance) => f.__identifier === fieldName);

  const tiles = tilesField?.__value as LdtkTileRect[] | null;

  if (tiles && tiles.length > 0) {
    // Create frames from tile data
    const frames: ex.Frame[] = [];
    for (const tile of tiles) {
      const sprite = new ex.Sprite({
        image: imageSource,
        sourceView: {
          x: tile.x,
          y: tile.y,
          width: tile.w || 16,
          height: tile.h || 16,
        },
        destSize: {
          width: tile.w || 16,
          height: tile.h || 16,
        },
      });
      frames.push({
        graphic: sprite,
        duration: animationFrameDuration,
      });
    }

    return new ex.Animation({
      frames,
      strategy: isIdle ? ex.AnimationStrategy.Freeze : ex.AnimationStrategy.Loop,
    });
  }

  // Fallback: use the entity's main tile as a single-frame animation
  const entityTile = entity.__tile;
  if (entityTile) {
    const sprite = new ex.Sprite({
      image: imageSource,
      sourceView: {
        x: entityTile.x,
        y: entityTile.y,
        width: entityTile.w || 16,
        height: entityTile.h || 16,
      },
      destSize: {
        width: entityTile.w || 16,
        height: entityTile.h || 16,
      },
    });

    return new ex.Animation({
      frames: [{
        graphic: sprite,
        duration: animationFrameDuration,
      }],
      strategy: ex.AnimationStrategy.Freeze,
    });
  }
  
  throw new Error(`No animation data found for ${fieldName}`);
};

About Me

I am an indie maker & researcher with a doctorate in computer science, interested in (among others): Software engineering, open data, data science, startups and esports.

See /about for details.

Have feedback, comments? Email me: philip@heltweg.org.

I (very occasionally) send out a newsletter when publishing new articles like this.

Subscribe ↗

Share this post on:

Next Post
Can a domain-specific language improve program structure comprehension of data pipelines? A mixed-methods study