mirror of
https://github.com/flame-engine/flame.git
synced 2025-10-29 07:56:53 +08:00
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:
committed by
GitHub
parent
7313cd5352
commit
477221998a
@ -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).
|
||||
|
||||
@ -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),
|
||||
);
|
||||
),
|
||||
);
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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()
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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:
|
||||
|
||||
|
Before Width: | Height: | Size: 2.1 KiB After Width: | Height: | Size: 2.1 KiB |
@ -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);
|
||||
}
|
||||
}
|
||||
|
||||
@ -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);
|
||||
}
|
||||
}
|
||||
@ -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',
|
||||
|
||||
@ -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';
|
||||
|
||||
@ -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>.
|
||||
|
||||
@ -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;
|
||||
@ -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';
|
||||
|
||||
@ -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)),
|
||||
),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user