LogoIgnis

Sprites

Cutting art into frames, and playing them.

Loading the scene…

A sprite is an image or a sequence of images in a game. It refers to a single visual entity on the screen.

In Ignis, SpriteNode draws Sprites. A Sprite states what the art decides: its frames, how fast they play, and whether they loop. SpriteNode acts as a controller, selecting the appropriate frame on demand. The process is the same whether you want to render an image or play an animation: preload assets, create a Sprite, then pass it to a SpriteNode.

There are several kinds of Sprite, depending on what you need to draw.

  • SpriteImage draws one still image.
  • SpriteAnimation plays a run of frames.
  • SpriteGroup lays several Sprites together, end to end.
  • SpriteMap names them.

These Sprite implementations automatically work with the local asset bundle, allowing SpriteNode to reload images in live scenes as they change on disk.

Sprite itself is abstract, so it is straightforward to implement one for images stored in complex ways, such as when packed optimally by an art application or sprite packer.

Sprite Sheets#

Most games don't store sprites in individual images for performance reasons. Instead, they use sprite sheets, which encode multiple logical sprites into the same physical file.

Ignis provides a SpriteSheet to produce Sprites using regions of a single image file. Note that a SpriteSheet itself is not a Sprite, but rather a means of acquiring one (or more) with minimal bookkeeping.

// Measure a grid of 56x56 frames over the image.
final sheet = SpriteSheet('assets/slime.png', Vector2.all(56));

// One cell of the grid, drawn still.
final tile = sheet.image(row: 0, column: 3);

// One row of the grid, or a span of one, played.
final jump = sheet.animation(row: 1, end: 30, fps: 24);

// Every row at once, end to end, each with its own parameters.
final slime = sheet.animations(fps: 16, rows: [.new(start: 6), .new(end: 30)]);
Warning

SpriteSheet cannot hold animations that wrap across multiple rows. A single animation must be specified contiguously on one row.

FlameLineage

The APIs for SpriteSheet, SpriteAnimation and SpriteGroup are heavily inspired by their corresponding classes in Flame.

Examples#

A single image#

final fire = SpriteNode(sprite: SpriteImage('assets/images/bonfire.png'));
sprites.dart
Loading the scene…

Playing an animation#

final fire = SpriteNode(
  sprite: SpriteAnimation(
    'assets/sheets/bonfire.png',
    BONFIRE_SIZE,
    fps: 16,
  ),
);
sprites.dart
Loading the scene…

Combining sprites#

final smoke = SpriteNode(
  sprite: SpriteAnimation(
    'assets/sheets/bonfire_smoke.png',
    BONFIRE_SIZE,
    fps: 10,
  ),
);

final flame = SpriteNode(
  sprite: SpriteAnimation(
    'assets/sheets/bonfire_flame.png',
    BONFIRE_SIZE,
    fps: 16,
  ),
);

final wood = SpriteNode(
  sprite: SpriteAnimation(
    'assets/sheets/bonfire_wood.png',
    BONFIRE_SIZE,
    fps: 6,
  ),
);
sprites.dart
Loading the scene…

Packing a sheet#

final sheet = SpriteSheet('assets/sheets/slime.png', SLIME_SIZE);

final slime = SpriteNode(
  sprite: sheet.animations(
    fps: 16,
    rows: [
      .new(end: 14), // idle
      .new(end: 30), // jump
      .new(end: 25), // jump_forward
      .new(end: 17), // spit
      .new(end: 30), // spike
      .new(end: 12), // injured1
      .new(end: 13), // injured2
      .new(end: 13), // injured3
      .new(end: 45), // splat_wall
      .new(end: 27), // recover
      .new(end: 49), // death
    ],
  ),
);

final taps = TapInput(shape: .rectangle(DEMO_SIZE));
taps.onTap(slime.playNext);
sprites.dart
Loading the scene…
Try tapping!

Naming sprites#

final sheet = SpriteSheet('assets/sheets/slime.png', SLIME_SIZE);

final slime = SpriteNode(
  sprite: SpriteMap({
    'idle': sheet.animation(row: 0, end: 14, fps: 16),
    'jump': sheet.animation(row: 1, end: 30, fps: 16),
  }),
);

final taps = TapInput(shape: .rectangle(DEMO_SIZE));
taps.onTap(() => slime.play(slime.current.key == 'idle' ? 'jump' : 'idle'));
sprites.dart
Loading the scene…
Try tapping!

Setting a rate per animation#

final sheet = SpriteSheet('assets/sheets/slime.png', SLIME_SIZE);

final slow = SpriteNode(sprite: sheet.animation(row: 1, end: 30, fps: 8));
final fast = SpriteNode(sprite: sheet.animation(row: 1, end: 30, fps: 24));
sprites.dart
Loading the scene…

Playing part of a row#

final sheet = SpriteSheet('assets/sheets/slime.png', SLIME_SIZE);

final slime = SpriteNode(
  sprite: sheet.animation(row: 0, start: 6, end: 12, fps: 12), // idle
);
sprites.dart
Loading the scene…

Working with tiles#

final sheet = SpriteSheet('assets/sheets/slime.png', SLIME_SIZE);

final crouch = SpriteNode(sprite: sheet.image(row: 1, column: 0));
final launch = SpriteNode(sprite: sheet.image(row: 1, column: 9));
final peak = SpriteNode(sprite: sheet.image(row: 1, column: 18));
final land = SpriteNode(sprite: sheet.image(row: 1, column: 27));
sprites.dart
Loading the scene…

Timing frames by hand#

final sheet = SpriteSheet('assets/sheets/slime.png', SLIME_SIZE);

final slime = SpriteNode(
  sprite: sheet.timed([0.8, 0.06, 0.06, 0.06, 0.06, 0.06]), // idle
);
sprites.dart
Loading the scene…

Scaling the rate#

final bonfire = SpriteAnimation(
  'assets/sheets/bonfire.png',
  BONFIRE_SIZE,
  fps: 16,
);

final fire = SpriteNode(sprite: bonfire);
final embers = SpriteNode(sprite: bonfire, speed: 0.25);
sprites.dart
Loading the scene…

Mixing stills and animations#

final creature = SpriteNode(
  sprite: SpriteMap({
    'fire': SpriteImage('assets/images/bonfire.png'),
    'slime': SpriteAnimation(
      'assets/sheets/slime_jump.png',
      SLIME_SIZE,
      fps: 16,
    ),
  }),
);

final taps = TapInput(shape: .rectangle(DEMO_SIZE));

taps.onTap(() {
  switch (creature.current.key) {
    case 'fire':
      creature.play('slime');

    case 'slime':
      creature.play('fire');
  }
});
sprites.dart
Loading the scene…
Try tapping!

Reporting progress#

final slime = SpriteNode(
  sprite: SpriteAnimation(
    'assets/sheets/slime_jump.png',
    SLIME_SIZE,
    fps: 16,
  ),
);

var count = 0;

slime.onLoop(() => log('onLoop ${count += 1}', ORANGE));
sprites.dart
Loading the scene…

Playing once#

final taps = TapInput(shape: .rectangle(DEMO_SIZE));
taps.onTapDown((event) {
  add(
    SpriteNode(
      sprite: SpriteAnimation(
        'assets/sheets/explosion.png',
        EXPLOSION_SIZE,
        fps: 20,
        loop: false,
      ),
      cleanup: true,
      position: event.scene,
      anchor: .bottomCenter,
    ),
  );
});
sprites.dart
Loading the scene…
Try tapping!