feat(flame): Add helper methods to create frame data on SpriteSheet (#2754)

Add two methods to SpriteSheet to create frame data for SpriteAnimation
This commit is contained in:
Jochum van der Ploeg
2023-09-21 11:34:02 +02:00
committed by GitHub
parent 7313cd5352
commit 477221998a
16 changed files with 160 additions and 39 deletions

View File

@ -331,21 +331,37 @@ a very simple example of how to use it:
```dart
import 'package:flame/sprite.dart';
final spritesheet = SpriteSheet(
final spriteSheet = SpriteSheet(
image: imageInstance,
srcSize: Vector2.all(16.0),
);
final animation = spritesheet.createAnimation(0, stepTime: 0.1);
final animation = spriteSheet.createAnimation(0, stepTime: 0.1);
```
Now you can use the animation directly or use it in an animation component.
You can also get a single frame of the sprite sheet using the `getSprite` method:
You can also create a custom animation by retrieving individual `SpriteAnimationFrameData` using
either `SpriteSheet.createFrameData` or `SpriteSheet.createFrameDataFromId`:
```dart
spritesheet.getSprite(0, 0) // row, column;
final animation = SpriteAnimation.fromFrameData(
imageInstance,
SpriteAnimationData([
spriteSheet.createFrameDataFromId(1, stepTime: 0.1), // by id
spriteSheet.createFrameData(2, 3, stepTime: 0.3), // row, column
spriteSheet.createFrameDataFromId(4, stepTime: 0.1), // by id
]),
);
```
If you don't need any kind of animation and instead only want an instance of a `Sprite` on the
`SpriteSheet` you can use the `getSprite` or `getSpriteById` methods:
```dart
spriteSheet.getSpriteById(2); // by id
spriteSheet.getSprite(0, 0); // row, column
```
You can see a full example of the `SpriteSheet` class
[here](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/sprites/spritesheet_example.dart).
[here](https://github.com/flame-engine/flame/blob/main/examples/lib/stories/sprites/sprite_sheet_example.dart).

View File

@ -308,7 +308,7 @@ it's fully played during the `Particle` lifespan. It's possible to override this
`alignAnimationTime` argument.
```dart
final spritesheet = SpriteSheet(
final spriteSheet = SpriteSheet(
image: yourSpriteSheetImage,
srcSize: Vector2.all(16.0),
);
@ -316,7 +316,7 @@ final spritesheet = SpriteSheet(
game.add(
ParticleSystemComponent(
particle: SpriteAnimationParticle(
animation: spritesheet.createAnimation(0, stepTime: 0.1),
animation: spriteSheet.createAnimation(0, stepTime: 0.1),
);
),
);

View File

@ -261,7 +261,7 @@ generate a `TextElement`.
Flame provides two concrete implementations:
- `TextPaint`: most used, uses Flutter `TextPainter` to render regular text
- `SpriteFontRenderer`: uses a `SpriteFont` (a spritesheet-based font) to render bitmap text
- `SpriteFontRenderer`: uses a `SpriteFont` (a sprite sheet-based font) to render bitmap text
- `DebugTextRenderer`: only intended to be used for Golden Tests
But you can also provide your own if you want to extend to other customized forms of text rendering.
@ -340,7 +340,7 @@ Palette](palette.md) guide.
#### SpriteFontRenderer
The other renderer option provided out of the box is `SpriteFontRenderer`, which allows you to
provide a `SpriteFont` based off of a spritesheet. TODO
provide a `SpriteFont` based off of a sprite sheet. TODO
#### DebugTextRenderer

View File

@ -75,14 +75,14 @@ klondike/
└─pubspec.yaml
```
By the way, this kind of file is called the **spritesheet**: it's just a
By the way, this kind of file is called the **sprite sheet**: it's just a
collection of multiple independent images in a single file. We are using a
spritesheet here for the simple reason that loading a single large image is
sprite sheet here for the simple reason that loading a single large image is
faster than many small images. In addition, rendering sprites that were
extracted from a single source image can be faster too, since Flutter will
optimize multiple such drawing commands into a single `drawAtlas` command.
Here are the contents of my spritesheet:
Here are the contents of my sprite sheet:
- Numerals 2, 3, 4, ..., K, A. In theory, we could have rendered these in the
game as text strings, but then we would need to also include a font as an

View File

@ -39,9 +39,9 @@ have been more difficult to access that image from other classes.
Also note that I am `await`ing the image to finish loading before initializing
anything else in the game. This is for convenience: it means that by the time
all other components are initialized, they can assume the spritesheet is already
all other components are initialized, they can assume the sprite sheet is already
loaded. We can even add a helper function to extract a sprite from the common
spritesheet:
sprite sheet:
```dart
Sprite klondikeSprite(double x, double y, double width, double height) {
@ -193,7 +193,7 @@ not planning to change these values during the game:
Next, we will create a `Stock` component, the `Waste`, four `Foundation`s and
seven `Pile`s, setting their sizes and positions in the world. The positions
are calculated using simple arithmetics. This should all happen inside the
`onLoad` method, after loading the spritesheet:
`onLoad` method, after loading the sprite sheet:
```dart
final stock = Stock()

View File

@ -71,9 +71,9 @@ after the image is loaded into the cache.
```
The last four numbers in the constructor are the coordinates of the sprite
image within the spritesheet `klondike-sprites.png`. If you're wondering how I
image within the sprite sheet `klondike-sprites.png`. If you're wondering how I
obtained these numbers, the answer is that I used a free online service
[spritecow.com] -- it's a handy tool for locating sprites within a spritesheet.
[spritecow.com] -- it's a handy tool for locating sprites within a sprite sheet.
Lastly, I have simple getters to determine the "color" of a suit. This will be
needed later when we need to enforce the rule that cards can only be placed

View File

@ -52,10 +52,10 @@ provide free pixel art that can be used in games, but please check and comply wi
always provide valid creator attribution. For this game though, I am going to take a chance and
make my artwork using an online pixel art tool. If you decide to use this tool, multiple online
tutorials will assist you with the basic operations as well as exporting the assets. Now normally,
most games will utilize spritesheets. These combine many images into one larger image that can be
most games will utilize sprite sheets. These combine many images into one larger image that can be
sectioned and used as individual images. For this tutorial though, I specifically will save the
images individually as I want to demonstrate the Flame engine's caching abilities. Ember and the
water enemy are spritesheets though as they contain multiple images to create animations.
water enemy are sprite sheets though as they contain multiple images to create animations.
Right-click the images below, choose "Save as...", and store them in the `assets/images` folder of the
project. At this point our project's structure looks like this:

View File

Before

Width:  |  Height:  |  Size: 2.1 KiB

After

Width:  |  Height:  |  Size: 2.1 KiB

View File

@ -501,12 +501,12 @@ class ParticlesExample extends FlameGame {
const rows = 8;
const frames = columns * rows;
final spriteImage = images.fromCache('boom.png');
final spritesheet = SpriteSheet.fromColumnsAndRows(
final spriteSheet = SpriteSheet.fromColumnsAndRows(
image: spriteImage,
columns: columns,
rows: rows,
);
final sprites = List<Sprite>.generate(frames, spritesheet.getSpriteById);
final sprites = List<Sprite>.generate(frames, spriteSheet.getSpriteById);
return SpriteAnimation.spriteList(sprites, stepTime: 0.1);
}
}

View File

@ -2,7 +2,7 @@ import 'package:flame/components.dart';
import 'package:flame/game.dart';
import 'package:flame/sprite.dart';
class SpritesheetExample extends FlameGame {
class SpriteSheetExample extends FlameGame {
static const String description = '''
In this example we show how to load images and how to create animations from
sprite sheets.
@ -11,7 +11,7 @@ class SpritesheetExample extends FlameGame {
@override
Future<void> onLoad() async {
final spriteSheet = SpriteSheet(
image: await images.load('spritesheet.png'),
image: await images.load('sprite_sheet.png'),
srcSize: Vector2(16.0, 18.0),
);
@ -28,6 +28,19 @@ class SpritesheetExample extends FlameGame {
stepTimes: [0.1, 0.1, 0.3, 0.3, 0.5, 0.3, 0.1],
);
final customVampireAnimation = SpriteAnimation.fromFrameData(
spriteSheet.image,
SpriteAnimationData([
spriteSheet.createFrameData(0, 0, stepTime: 0.1),
spriteSheet.createFrameData(0, 1, stepTime: 0.1),
spriteSheet.createFrameData(0, 2, stepTime: 0.3),
spriteSheet.createFrameDataFromId(4, stepTime: 0.3),
spriteSheet.createFrameDataFromId(5, stepTime: 0.5),
spriteSheet.createFrameDataFromId(6, stepTime: 0.3),
spriteSheet.createFrameDataFromId(7, stepTime: 0.1),
]),
);
final spriteSize = Vector2(80.0, 90.0);
final vampireComponent = SpriteAnimationComponent(
@ -44,13 +57,20 @@ class SpritesheetExample extends FlameGame {
final ghostAnimationVariableStepTimesComponent = SpriteAnimationComponent(
animation: ghostAnimationVariableStepTimes,
position: Vector2(150, 340),
position: Vector2(250, 220),
size: spriteSize,
);
final customVampireComponent = SpriteAnimationComponent(
animation: customVampireAnimation,
position: Vector2(250, 100),
size: spriteSize,
);
add(vampireComponent);
add(ghostComponent);
add(ghostAnimationVariableStepTimesComponent);
add(customVampireComponent);
// Some plain sprites
final vampireSpriteComponent = SpriteComponent(
@ -65,14 +85,7 @@ class SpritesheetExample extends FlameGame {
position: Vector2(50, 220),
);
final ghostVariableSpriteComponent = SpriteComponent(
sprite: spriteSheet.getSprite(1, 0),
size: spriteSize,
position: Vector2(50, 340),
);
add(vampireSpriteComponent);
add(ghostSpriteComponent);
add(ghostVariableSpriteComponent);
}
}

View File

@ -5,7 +5,7 @@ import 'package:examples/stories/sprites/basic_sprite_example.dart';
import 'package:examples/stories/sprites/sprite_batch_example.dart';
import 'package:examples/stories/sprites/sprite_batch_load_example.dart';
import 'package:examples/stories/sprites/sprite_group_example.dart';
import 'package:examples/stories/sprites/spritesheet_example.dart';
import 'package:examples/stories/sprites/sprite_sheet_example.dart';
import 'package:flame/game.dart';
void addSpritesStories(Dashbook dashbook) {
@ -23,10 +23,10 @@ void addSpritesStories(Dashbook dashbook) {
info: Base64SpriteExample.description,
)
..add(
'Spritesheet',
(_) => GameWidget(game: SpritesheetExample()),
codeLink: baseLink('sprites/spritesheet_example.dart'),
info: SpritesheetExample.description,
'SpriteSheet',
(_) => GameWidget(game: SpriteSheetExample()),
codeLink: baseLink('sprites/sprite_sheet_example.dart'),
info: SpriteSheetExample.description,
)
..add(
'SpriteBatch',

View File

@ -7,4 +7,4 @@ export 'src/sprite.dart';
export 'src/sprite_animation.dart';
export 'src/sprite_animation_ticker.dart';
export 'src/sprite_batch.dart' hide FlippedAtlasStatus;
export 'src/spritesheet.dart';
export 'src/sprite_sheet.dart';

View File

@ -1,7 +1,7 @@
import 'dart:ui';
import 'package:flame/components.dart';
import 'package:flame/src/spritesheet.dart';
import 'package:flame/src/sprite_sheet.dart';
import 'package:meta/meta.dart';
/// This is just a pair of <int, int>.

View File

@ -68,6 +68,34 @@ class SpriteSheet {
return _spriteCache[spriteId] ??= _computeSprite(spriteId);
}
/// Create a [SpriteAnimationFrameData] for the sprite in the position
/// (row, column) on the sprite sheet grid.
SpriteAnimationFrameData createFrameData(
int row,
int column, {
required double stepTime,
}) {
return createFrameDataFromId(row * columns + column, stepTime: stepTime);
}
/// Create a [SpriteAnimationFrameData] for the sprite with id [spriteId]
/// from the grid.
///
/// The ids are defined as starting at 0 on the top left and going
/// sequentially on each row.
SpriteAnimationFrameData createFrameDataFromId(
int spriteId, {
required double stepTime,
}) {
final i = spriteId % columns;
final j = spriteId ~/ columns;
return SpriteAnimationFrameData(
srcPosition: Vector2Extension.fromInts(i, j)..multiply(srcSize),
srcSize: srcSize,
stepTime: stepTime,
);
}
Sprite _computeSprite(int spriteId) {
final i = spriteId % columns;
final j = spriteId ~/ columns;

View File

@ -5,7 +5,7 @@ import 'package:flame/input.dart';
import 'package:flame/src/anchor.dart';
import 'package:flame/src/components/sprite_group_component.dart';
import 'package:flame/src/events/flame_game_mixins/multi_tap_dispatcher.dart';
import 'package:flame/src/spritesheet.dart';
import 'package:flame/src/sprite_sheet.dart';
import 'package:flame_test/flame_test.dart';
import 'package:flutter/material.dart';
import 'package:flutter_test/flutter_test.dart';

View File

@ -54,5 +54,69 @@ void main() {
);
},
);
test('return sprite based on row and column', () {
final spriteSheet = SpriteSheet(
image: image,
srcSize: Vector2(50, 50),
);
expect(
spriteSheet.getSprite(1, 1),
isA<Sprite>().having(
(sprite) => sprite.srcPosition,
'srcPosition',
equals(Vector2(50, 50)),
),
);
});
test('return sprite based on id', () {
final spriteSheet = SpriteSheet(
image: image,
srcSize: Vector2(50, 50),
);
expect(
spriteSheet.getSpriteById(3),
isA<Sprite>().having(
(sprite) => sprite.srcPosition,
'srcPosition',
equals(Vector2(50, 50)),
),
);
});
test('create sprite animation frame data based on row and column', () {
final spriteSheet = SpriteSheet(
image: image,
srcSize: Vector2(50, 50),
);
expect(
spriteSheet.createFrameData(1, 1, stepTime: 0.1),
isA<SpriteAnimationFrameData>().having(
(frame) => frame.srcPosition,
'srcPosition',
equals(Vector2(50, 50)),
),
);
});
test('create sprite animation frame data based on id', () {
final spriteSheet = SpriteSheet(
image: image,
srcSize: Vector2(50, 50),
);
expect(
spriteSheet.createFrameDataFromId(3, stepTime: 0.1),
isA<SpriteAnimationFrameData>().having(
(frame) => frame.srcPosition,
'srcPosition',
equals(Vector2(50, 50)),
),
);
});
});
}