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.
SpriteImagedraws one still image.SpriteAnimationplays a run of frames.SpriteGrouplays severalSprites together, end to end.SpriteMapnames 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)]);
SpriteSheet cannot hold animations that wrap across multiple rows. A single animation must be specified contiguously on one row.
Lineage
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'));
Playing an animation#
final fire = SpriteNode(
sprite: SpriteAnimation(
'assets/sheets/bonfire.png',
BONFIRE_SIZE,
fps: 16,
),
);
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,
),
);
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);
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'));
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));
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
);
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));
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
);
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);
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');
}
});
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));
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,
),
);
});
