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( final _animationSpriteSheet = SpriteSheet(
image: image, image: image,
columns: 19, srcSize: Vector2.all(96),
rows: 1,
textureWidth: 96,
textureHeight: 96,
); );
_animation = _animationSpriteSheet.createAnimation( _animation = _animationSpriteSheet.createAnimation(
0, row: 0,
stepTime: 0.2, stepTime: 0.2,
to: 19, to: 19,
); );
@ -50,7 +47,7 @@ class MyHomePage extends StatefulWidget {
} }
class _MyHomePageState extends State<MyHomePage> { class _MyHomePageState extends State<MyHomePage> {
Vector2 _position = Vector2(256.0, 256.0); Vector2 _position = Vector2.all(256);
@override @override
void initState() { void initState() {
@ -60,9 +57,7 @@ class _MyHomePageState extends State<MyHomePage> {
void changePosition() async { void changePosition() async {
await Future.delayed(const Duration(seconds: 1)); await Future.delayed(const Duration(seconds: 1));
setState(() { setState(() => _position += Vector2.all(10));
_position = Vector2(10 + _position.x, 10 + _position.y);
});
} }
void _clickFab(GlobalKey<ScaffoldState> key) { 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/components/isometric_tile_map_component.dart';
import 'package:flame/gestures.dart'; import 'package:flame/gestures.dart';
import 'package:flame/sprite.dart'; import 'package:flame/sprite.dart';
import 'package:flame/spritesheet.dart';
import 'package:flutter/gestures.dart'; import 'package:flutter/gestures.dart';
import 'package:flutter/material.dart' hide Image; import 'package:flutter/material.dart' hide Image;
@ -25,7 +26,9 @@ class Selector extends SpriteComponent {
Selector(double s, Image image) Selector(double s, Image image)
: super.fromSprite( : super.fromSprite(
Vector2.all(s), Sprite(image, srcSize: Vector2.all(32.0))); Vector2.all(s),
Sprite(image, srcSize: Vector2.all(32.0)),
);
@override @override
void render(Canvas canvas) { void render(Canvas canvas) {
@ -48,7 +51,7 @@ class MyGame extends BaseGame with MouseMovementDetector {
final selectorImage = await images.load('selector.png'); final selectorImage = await images.load('selector.png');
final tilesetImage = await images.load('tiles.png'); final tilesetImage = await images.load('tiles.png');
final tileset = IsometricTileset(tilesetImage, 32); final tileset = SpriteSheet(image: tilesetImage, srcSize: Vector2.all(32));
final matrix = [ final matrix = [
[3, 1, 1, 1, 0, 0], [3, 1, 1, 1, 0, 0],
[-1, 1, 2, 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], [1, 3, 3, 3, 0, 2],
]; ];
add( add(
base = IsometricTileMapComponent(tileset, matrix, destTileSize: s) base = IsometricTileMapComponent(
tileset,
matrix,
destTileSize: Vector2.all(s.toDouble()),
)
..x = x ..x = x
..y = y, ..y = y,
); );

View File

@ -532,16 +532,14 @@ class MyGame extends BaseGame {
const rows = 8; const rows = 8;
const frames = columns * rows; const frames = columns * rows;
final spriteImage = images.fromCache('boom3.png'); final spriteImage = images.fromCache('boom3.png');
final spritesheet = SpriteSheet( final spritesheet = SpriteSheet.fromColsAndRows(
rows: rows,
columns: columns,
image: spriteImage, image: spriteImage,
textureWidth: spriteImage.width ~/ columns, columns: columns,
textureHeight: spriteImage.height ~/ rows, rows: rows,
); );
final sprites = List<Sprite>.generate( final sprites = List<Sprite>.generate(
frames, frames,
(i) => spritesheet.getSprite(i ~/ rows, i % columns), (i) => spritesheet.getSpriteById(i),
); );
return SpriteAnimation.spriteList(sprites); 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_animation_component.dart';
import 'package:flame/components/sprite_component.dart'; import 'package:flame/components/sprite_component.dart';
import 'package:flame/extensions/vector2.dart';
import 'package:flame/game.dart'; import 'package:flame/game.dart';
import 'package:flame/spritesheet.dart'; import 'package:flame/spritesheet.dart';
import 'package:flutter/material.dart';
void main() async { void main() async {
WidgetsFlutterBinding.ensureInitialized(); WidgetsFlutterBinding.ensureInitialized();
@ -16,16 +16,14 @@ class MyGame extends BaseGame {
Future<void> onLoad() async { Future<void> onLoad() async {
final spriteSheet = SpriteSheet( final spriteSheet = SpriteSheet(
image: await images.load('spritesheet.png'), image: await images.load('spritesheet.png'),
textureWidth: 16, srcSize: Vector2(16.0, 18.0),
textureHeight: 18,
columns: 11,
rows: 2,
); );
final vampireAnimation = final vampireAnimation =
spriteSheet.createAnimation(0, stepTime: 0.1, to: 7); spriteSheet.createAnimation(row: 0, stepTime: 0.1, to: 7);
final ghostAnimation = spriteSheet.createAnimation(1, stepTime: 0.1, to: 7); final ghostAnimation =
final spriteSize = Vector2(80, 90); spriteSheet.createAnimation(row: 1, stepTime: 0.1, to: 7);
final spriteSize = Vector2(80.0, 90.0);
final vampireComponent = final vampireComponent =
SpriteAnimationComponent(spriteSize, vampireAnimation) SpriteAnimationComponent(spriteSize, vampireAnimation)

View File

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

View File

@ -281,10 +281,7 @@ import 'package:flame/spritesheet.dart';
final spritesheet = SpriteSheet( final spritesheet = SpriteSheet(
image: imageInstance, image: imageInstance,
textureWidth: 16, srcSize: Vector2.all(16.0),
textureHeight: 16,
columns: 10,
rows: 2,
); );
final animation = spritesheet.createAnimation(0, stepTime: 0.1); 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 ```dart
final spritesheet = SpriteSheet( final spritesheet = SpriteSheet(
imageName: 'spritesheet.png', imageName: 'spritesheet.png',
textureWidth: 16, srcSize: Vector2.all(16.0),
textureHeight: 16,
columns: 10,
rows: 2
); );
game.add( game.add(

View File

@ -1,56 +1,11 @@
import 'dart:ui'; import 'dart:ui';
import 'package:flame/components/position_component.dart'; import 'position_component.dart';
import '../sprite.dart';
import '../extensions/vector2.dart'; import '../extensions/vector2.dart';
import '../spritesheet.dart';
/// This represents an isometric tileset to be used in a tilemap. /// This is just a pair of <int, int>.
///
/// 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.
/// ///
/// Represents a position in a matrix, or in this case, on the tilemap. /// Represents a position in a matrix, or in this case, on the tilemap.
class Block { class Block {
@ -70,29 +25,29 @@ class Block {
/// property. /// property.
class IsometricTileMapComponent extends PositionComponent { class IsometricTileMapComponent extends PositionComponent {
/// This is the tileset that will be used to render this map. /// 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. /// The positions of each block will be placed respecting this matrix.
List<List<int>> matrix; List<List<int>> matrix;
/// Optionally provide a new tile size to render it scaled. /// Optionally provide a new tile size to render it scaled.
int destTileSize; Vector2 destTileSize;
IsometricTileMapComponent(this.tileset, this.matrix, {this.destTileSize}); IsometricTileMapComponent(this.tileset, this.matrix, {this.destTileSize});
/// This is the size the tiles will be drawn (either original or overwritten). /// 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 @override
void render(Canvas c) { void render(Canvas c) {
super.render(c); super.render(c);
final size = Vector2.all(effectiveTileSize.toDouble()); final size = effectiveTileSize;
for (int i = 0; i < matrix.length; i++) { for (int i = 0; i < matrix.length; i++) {
for (int j = 0; j < matrix[i].length; j++) { for (int j = 0; j < matrix[i].length; j++) {
final element = matrix[i][j]; final element = matrix[i][j];
if (element != -1) { if (element != -1) {
final sprite = tileset.getTile(element); final sprite = tileset.getSpriteById(element);
final p = getBlockPositionInts(j, i); final p = getBlockPositionInts(j, i);
sprite.renderRect(c, p.toPositionedRect(size)); sprite.renderRect(c, p.toPositionedRect(size));
} }
@ -108,8 +63,9 @@ class IsometricTileMapComponent extends PositionComponent {
} }
Vector2 getBlockPositionInts(int i, int j) { Vector2 getBlockPositionInts(int i, int j) {
final s = effectiveTileSize.toDouble() / 2; final pos = Vector2(i.toDouble(), j.toDouble())
return cartToIso(Vector2(i * s, j * s)) - Vector2(s, 0); ..multiply(effectiveTileSize / 2);
return cartToIso(pos) - Vector2(effectiveTileSize.x / 2, 0);
} }
/// Converts a coordinate from the isometric space to the cartesian space. /// 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. /// This can be used to handle clicks or hovers.
Block getBlock(Vector2 p) { Block getBlock(Vector2 p) {
final s = effectiveTileSize.toDouble() / 2;
final cart = isoToCart(p - position); final cart = isoToCart(p - position);
final px = cart.x ~/ s; final px = cart.x ~/ (effectiveTileSize.x / 2);
final py = cart.y ~/ s; final py = cart.y ~/ (effectiveTileSize.y / 2);
return Block(px, py); return Block(px, py);
} }

View File

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

View File

@ -6,66 +6,95 @@ import 'sprite.dart';
import 'sprite_animation.dart'; import 'sprite_animation.dart';
import 'extensions/vector2.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 { class SpriteSheet {
int textureWidth; /// The src image from which each sprite will be generated.
int textureHeight; final Image image;
int columns;
int rows;
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({ SpriteSheet({
@required Image image, @required this.image,
@required this.textureWidth, @required this.srcSize,
@required this.textureHeight, });
@required this.columns,
@required this.rows, SpriteSheet.fromColsAndRows({
}) { @required this.image,
_sprites = List.generate( @required int columns,
rows, @required int rows,
(y) => List.generate( }) : srcSize = Vector2(
columns, image.width / columns,
(x) => _mapImagePath(image, textureWidth, textureHeight, x, y), 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( /// Gets teh sprite with id [spriteId] from the grid.
Image image, ///
int textureWidth, /// The ids are defined as starting at 0 on the top left and going
int textureHeight, /// sequentially on each row.
int x, /// This is lazily computed and cached for your convenience.
int y, Sprite getSpriteById(int spriteId) {
) { return _spriteCache[spriteId] ??= _computeSprite(spriteId);
final size = Vector2(textureWidth.toDouble(), textureHeight.toDouble()); }
Sprite _computeSprite(int spriteId) {
final i = (spriteId % columns).toDouble();
final j = (spriteId ~/ columns).toDouble();
return Sprite( return Sprite(
image, image,
srcPosition: Vector2(x.toDouble(), y.toDouble())..multiply(size), srcPosition: Vector2(i, j)..multiply(srcSize),
srcSize: size, srcSize: srcSize,
); );
} }
Sprite getSprite(int row, int column) { /// Creates a [SpriteAnimation] from this SpriteSheet, using the sequence
final Sprite s = _sprites[row][column]; /// of sprites on a given row.
assert(s != null, 'No sprite found for row $row and column $column');
return s;
}
/// Creates a sprite animation from this SpriteSheet
/// ///
/// An [from] and a [to] parameter can be specified to create an animation from a subset of the columns on the row /// [from] and [to] can be specified to create an animation
SpriteAnimation createAnimation(int row, /// from a subset of the columns on the row
{double stepTime, bool loop = true, int from = 0, int to}) { SpriteAnimation createAnimation({
final spriteRow = _sprites[row]; @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'); final spriteList = List<int>.generate(to - from, (i) => from + i)
.map((e) => getSprite(row, e))
to ??= spriteRow.length; .toList();
final spriteList = spriteRow.sublist(from, to);
return SpriteAnimation.spriteList( return SpriteAnimation.spriteList(
spriteList, spriteList,