Improve IsometricTileMap and Spritesheet classes

This commit is contained in:
Luan Nico
2020-10-14 01:55:03 -04:00
parent cb3ca5518a
commit 1e285464f1
12 changed files with 166 additions and 151 deletions

View File

@ -19,13 +19,10 @@ void main() async {
final _animationSpriteSheet = SpriteSheet(
image: image,
columns: 19,
rows: 1,
textureWidth: 96,
textureHeight: 96,
srcSize: Vector2.all(96),
);
_animation = _animationSpriteSheet.createAnimation(
0,
row: 0,
stepTime: 0.2,
to: 19,
);
@ -50,7 +47,7 @@ class MyHomePage extends StatefulWidget {
}
class _MyHomePageState extends State<MyHomePage> {
Vector2 _position = Vector2(256.0, 256.0);
Vector2 _position = Vector2.all(256);
@override
void initState() {
@ -60,9 +57,7 @@ class _MyHomePageState extends State<MyHomePage> {
void changePosition() async {
await Future.delayed(const Duration(seconds: 1));
setState(() {
_position = Vector2(10 + _position.x, 10 + _position.y);
});
setState(() => _position += Vector2.all(10));
}
void _clickFab(GlobalKey<ScaffoldState> key) {

41
doc/examples/isometric/.gitignore vendored Normal file
View File

@ -0,0 +1,41 @@
# Miscellaneous
*.class
*.log
*.pyc
*.swp
.DS_Store
.atom/
.buildlog/
.history
.svn/
# IntelliJ related
*.iml
*.ipr
*.iws
.idea/
# The .vscode folder contains launch configuration and tasks you configure in
# VS Code which you may wish to be included in version control, so this line
# is commented out by default.
#.vscode/
# Flutter/Dart/Pub related
**/doc/api/
**/ios/Flutter/.last_build_id
.dart_tool/
.flutter-plugins
.flutter-plugins-dependencies
.packages
.pub-cache/
.pub/
/build/
# Web related
lib/generated_plugin_registrant.dart
# Symbolication related
app.*.symbols
# Obfuscation related
app.*.map.json

View File

@ -4,6 +4,7 @@ import 'package:flame/game.dart';
import 'package:flame/components/isometric_tile_map_component.dart';
import 'package:flame/gestures.dart';
import 'package:flame/sprite.dart';
import 'package:flame/spritesheet.dart';
import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide Image;
@ -25,7 +26,9 @@ class Selector extends SpriteComponent {
Selector(double s, Image image)
: super.fromSprite(
Vector2.all(s), Sprite(image, srcSize: Vector2.all(32.0)));
Vector2.all(s),
Sprite(image, srcSize: Vector2.all(32.0)),
);
@override
void render(Canvas canvas) {
@ -48,7 +51,7 @@ class MyGame extends BaseGame with MouseMovementDetector {
final selectorImage = await images.load('selector.png');
final tilesetImage = await images.load('tiles.png');
final tileset = IsometricTileset(tilesetImage, 32);
final tileset = SpriteSheet(image: tilesetImage, srcSize: Vector2.all(32));
final matrix = [
[3, 1, 1, 1, 0, 0],
[-1, 1, 2, 1, 0, 0],
@ -58,7 +61,11 @@ class MyGame extends BaseGame with MouseMovementDetector {
[1, 3, 3, 3, 0, 2],
];
add(
base = IsometricTileMapComponent(tileset, matrix, destTileSize: s)
base = IsometricTileMapComponent(
tileset,
matrix,
destTileSize: Vector2.all(s.toDouble()),
)
..x = x
..y = y,
);

View File

@ -532,16 +532,14 @@ class MyGame extends BaseGame {
const rows = 8;
const frames = columns * rows;
final spriteImage = images.fromCache('boom3.png');
final spritesheet = SpriteSheet(
rows: rows,
columns: columns,
final spritesheet = SpriteSheet.fromColsAndRows(
image: spriteImage,
textureWidth: spriteImage.width ~/ columns,
textureHeight: spriteImage.height ~/ rows,
columns: columns,
rows: rows,
);
final sprites = List<Sprite>.generate(
frames,
(i) => spritesheet.getSprite(i ~/ rows, i % columns),
(i) => spritesheet.getSpriteById(i),
);
return SpriteAnimation.spriteList(sprites);

Binary file not shown.

After

Width:  |  Height:  |  Size: 37 KiB

View File

@ -1,9 +1,9 @@
import 'package:flame/extensions/vector2.dart';
import 'package:flutter/material.dart';
import 'package:flame/components/sprite_animation_component.dart';
import 'package:flame/components/sprite_component.dart';
import 'package:flame/extensions/vector2.dart';
import 'package:flame/game.dart';
import 'package:flame/spritesheet.dart';
import 'package:flutter/material.dart';
void main() async {
WidgetsFlutterBinding.ensureInitialized();
@ -16,16 +16,14 @@ class MyGame extends BaseGame {
Future<void> onLoad() async {
final spriteSheet = SpriteSheet(
image: await images.load('spritesheet.png'),
textureWidth: 16,
textureHeight: 18,
columns: 11,
rows: 2,
srcSize: Vector2(16.0, 18.0),
);
final vampireAnimation =
spriteSheet.createAnimation(0, stepTime: 0.1, to: 7);
final ghostAnimation = spriteSheet.createAnimation(1, stepTime: 0.1, to: 7);
final spriteSize = Vector2(80, 90);
spriteSheet.createAnimation(row: 0, stepTime: 0.1, to: 7);
final ghostAnimation =
spriteSheet.createAnimation(row: 1, stepTime: 0.1, to: 7);
final spriteSize = Vector2(80.0, 90.0);
final vampireComponent =
SpriteAnimationComponent(spriteSize, vampireAnimation)

View File

@ -1,3 +1,4 @@
import 'package:flame/extensions/vector2.dart';
import 'package:flutter/material.dart' hide Animation;
import 'package:flame/flame.dart';
import 'package:flame/sprite.dart';
@ -64,10 +65,7 @@ void main() async {
final buttonsImage = await Flame.images.load('buttons.png');
final _buttons = SpriteSheet(
image: buttonsImage,
textureHeight: 20,
textureWidth: 60,
columns: 1,
rows: 2,
srcSize: Vector2(60, 20),
);
dashbook.storiesOf('SpriteButton').decorator(CenterDecorator()).add(
'default',
@ -119,13 +117,10 @@ void main() async {
final pteroImage = await Flame.images.load('bomb_ptero.png');
final _animationSpriteSheet = SpriteSheet(
image: pteroImage,
textureHeight: 32,
textureWidth: 48,
columns: 4,
rows: 1,
srcSize: Vector2(48, 32),
);
final _animation = _animationSpriteSheet.createAnimation(
0,
row: 0,
stepTime: 0.2,
to: 3,
loop: true,

View File

@ -281,10 +281,7 @@ import 'package:flame/spritesheet.dart';
final spritesheet = SpriteSheet(
image: imageInstance,
textureWidth: 16,
textureHeight: 16,
columns: 10,
rows: 2,
srcSize: Vector2.all(16.0),
);
final animation = spritesheet.createAnimation(0, stepTime: 0.1);

View File

@ -259,10 +259,7 @@ A `Particle` which embeds a Flame `Animation`. By default, aligns `Animation`s `
```dart
final spritesheet = SpriteSheet(
imageName: 'spritesheet.png',
textureWidth: 16,
textureHeight: 16,
columns: 10,
rows: 2
srcSize: Vector2.all(16.0),
);
game.add(

View File

@ -1,56 +1,11 @@
import 'dart:ui';
import 'package:flame/components/position_component.dart';
import 'position_component.dart';
import '../sprite.dart';
import '../extensions/vector2.dart';
import '../spritesheet.dart';
/// This represents an isometric tileset to be used in a tilemap.
///
/// It's basically a grid of squares, each square has a tile, in order.
/// The block ids are calculated going row per row, left to right, top to
/// bottom.
///
/// This class will cache the usage of sprites to improve performance.
class IsometricTileset {
/// The image for this tileset.
final Image tileset;
/// The size of each square block within the image.
///
/// The image width and height must be multiples of this number.
final int size;
final Map<int, Sprite> _spriteCache = {};
IsometricTileset(this.tileset, this.size);
/// Compute the number of columns the image has
/// by using the image width and tile size.
int get columns => tileset.width ~/ size;
/// Compute the number of rows the image has
/// by using the image height and tile size.
int get rows => tileset.height ~/ size;
/// Get a sprite to render one specific tile given its id.
///
/// The ids are assigned left to right, top to bottom, row per row.
/// The returned sprite will be cached, so don't modify it!
Sprite getTile(int tileId) {
return _spriteCache[tileId] ??= _computeTile(tileId);
}
Sprite _computeTile(int tileId) {
final i = tileId % columns;
final j = tileId ~/ columns;
final s = size.toDouble();
return Sprite(tileset,
srcPosition: Vector2(s * i, s * j), srcSize: Vector2.all(s));
}
}
/// This is just a pair of int, int.
/// This is just a pair of <int, int>.
///
/// Represents a position in a matrix, or in this case, on the tilemap.
class Block {
@ -70,29 +25,29 @@ class Block {
/// property.
class IsometricTileMapComponent extends PositionComponent {
/// This is the tileset that will be used to render this map.
IsometricTileset tileset;
SpriteSheet tileset;
/// The positions of each block will be placed respecting this matrix.
List<List<int>> matrix;
/// Optionally provide a new tile size to render it scaled.
int destTileSize;
Vector2 destTileSize;
IsometricTileMapComponent(this.tileset, this.matrix, {this.destTileSize});
/// This is the size the tiles will be drawn (either original or overwritten).
int get effectiveTileSize => destTileSize ?? tileset.size;
Vector2 get effectiveTileSize => destTileSize ?? tileset.srcSize;
@override
void render(Canvas c) {
super.render(c);
final size = Vector2.all(effectiveTileSize.toDouble());
final size = effectiveTileSize;
for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) {
final element = matrix[i][j];
if (element != -1) {
final sprite = tileset.getTile(element);
final sprite = tileset.getSpriteById(element);
final p = getBlockPositionInts(j, i);
sprite.renderRect(c, p.toPositionedRect(size));
}
@ -108,8 +63,9 @@ class IsometricTileMapComponent extends PositionComponent {
}
Vector2 getBlockPositionInts(int i, int j) {
final s = effectiveTileSize.toDouble() / 2;
return cartToIso(Vector2(i * s, j * s)) - Vector2(s, 0);
final pos = Vector2(i.toDouble(), j.toDouble())
..multiply(effectiveTileSize / 2);
return cartToIso(pos) - Vector2(effectiveTileSize.x / 2, 0);
}
/// Converts a coordinate from the isometric space to the cartesian space.
@ -130,10 +86,9 @@ class IsometricTileMapComponent extends PositionComponent {
///
/// This can be used to handle clicks or hovers.
Block getBlock(Vector2 p) {
final s = effectiveTileSize.toDouble() / 2;
final cart = isoToCart(p - position);
final px = cart.x ~/ s;
final py = cart.y ~/ s;
final px = cart.x ~/ (effectiveTileSize.x / 2);
final py = cart.y ~/ (effectiveTileSize.y / 2);
return Block(px, py);
}

View File

@ -48,8 +48,11 @@ class SpriteAnimation {
/// Creates an animation based on the parameters.
///
/// All frames have the same [stepTime].
SpriteAnimation.spriteList(List<Sprite> sprites,
{double stepTime, this.loop = true}) {
SpriteAnimation.spriteList(
List<Sprite> sprites, {
double stepTime,
this.loop = true,
}) {
if (sprites.isEmpty) {
throw Exception('You must have at least one frame!');
}

View File

@ -6,66 +6,95 @@ import 'sprite.dart';
import 'sprite_animation.dart';
import 'extensions/vector2.dart';
/// Utility class to help extract animations and sprites from a spritesheet image
/// Utility class to help extract animations and sprites from a sprite sheet image.
///
/// A sprite sheet is a single image in which several regions can be defined as individual sprites.
/// For the purposes of this class, all of these regions must be identically sized rectangles.
/// You can use the [Sprite] class directly if you want to have varying shapes.
///
/// Each sprite in this sheet can be identified either by it's (row, col) pair or
/// by it's "id", which is basically it's sequenced index if the image is put in a
/// single line. The sprites can be used to compose an animation easily if they
/// all the frames happen to be sequentially on the same row.
/// Sprites are lazily generated but cached.
class SpriteSheet {
int textureWidth;
int textureHeight;
int columns;
int rows;
/// The src image from which each sprite will be generated.
final Image image;
List<List<Sprite>> _sprites;
/// The size of each rectangle within the image that define each sprite.
///
/// For example, if this sprite sheet is a tile map, this would be the tile size.
/// If it's an animation sheet, this would be the frame size.
final Vector2 srcSize;
final Map<int, Sprite> _spriteCache = {};
/// Creates a sprite sheet given the image and the tile size.
SpriteSheet({
@required Image image,
@required this.textureWidth,
@required this.textureHeight,
@required this.columns,
@required this.rows,
}) {
_sprites = List.generate(
rows,
(y) => List.generate(
columns,
(x) => _mapImagePath(image, textureWidth, textureHeight, x, y),
),
);
@required this.image,
@required this.srcSize,
});
SpriteSheet.fromColsAndRows({
@required this.image,
@required int columns,
@required int rows,
}) : srcSize = Vector2(
image.width / columns,
image.height / rows,
);
/// Compute the number of columns the image has
/// by using the image width and tile size.
int get columns => image.width ~/ srcSize.x;
/// Compute the number of rows the image has
/// by using the image height and tile size.
int get rows => image.height ~/ srcSize.y;
/// Gets the sprite in the position (row, column) on the sprite sheet grid.
///
/// This is lazily computed and cached for your convenience.
Sprite getSprite(int row, int column) {
return getSpriteById(row * columns + column);
}
Sprite _mapImagePath(
Image image,
int textureWidth,
int textureHeight,
int x,
int y,
) {
final size = Vector2(textureWidth.toDouble(), textureHeight.toDouble());
/// Gets teh 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.
/// This is lazily computed and cached for your convenience.
Sprite getSpriteById(int spriteId) {
return _spriteCache[spriteId] ??= _computeSprite(spriteId);
}
Sprite _computeSprite(int spriteId) {
final i = (spriteId % columns).toDouble();
final j = (spriteId ~/ columns).toDouble();
return Sprite(
image,
srcPosition: Vector2(x.toDouble(), y.toDouble())..multiply(size),
srcSize: size,
srcPosition: Vector2(i, j)..multiply(srcSize),
srcSize: srcSize,
);
}
Sprite getSprite(int row, int column) {
final Sprite s = _sprites[row][column];
assert(s != null, 'No sprite found for row $row and column $column');
return s;
}
/// Creates a sprite animation from this SpriteSheet
/// Creates a [SpriteAnimation] from this SpriteSheet, using the sequence
/// of sprites on a given row.
///
/// An [from] and a [to] parameter can be specified to create an animation from a subset of the columns on the row
SpriteAnimation createAnimation(int row,
{double stepTime, bool loop = true, int from = 0, int to}) {
final spriteRow = _sprites[row];
/// [from] and [to] can be specified to create an animation
/// from a subset of the columns on the row
SpriteAnimation createAnimation({
@required int row,
@required double stepTime,
bool loop = true,
int from = 0,
int to,
}) {
to ??= columns;
assert(spriteRow != null, 'There is no row for $row index');
to ??= spriteRow.length;
final spriteList = spriteRow.sublist(from, to);
final spriteList = List<int>.generate(to - from, (i) => from + i)
.map((e) => getSprite(row, e))
.toList();
return SpriteAnimation.spriteList(
spriteList,