From 1e285464f19cca70180a32c267af68a48dcc38f5 Mon Sep 17 00:00:00 2001 From: Luan Nico Date: Wed, 14 Oct 2020 01:55:03 -0400 Subject: [PATCH] Improve IsometricTileMap and Spritesheet classes --- doc/examples/animation_widget/lib/main.dart | 13 +- doc/examples/isometric/.gitignore | 41 ++++++ doc/examples/isometric/lib/main.dart | 13 +- doc/examples/particles/lib/main.dart | 10 +- doc/examples/spritesheet/flutter_01.png | Bin 0 -> 37903 bytes doc/examples/spritesheet/lib/main.dart | 16 +-- doc/examples/widgets/lib/main.dart | 13 +- doc/images.md | 5 +- doc/particles.md | 5 +- .../isometric_tile_map_component.dart | 71 ++-------- lib/sprite_animation.dart | 7 +- lib/spritesheet.dart | 123 +++++++++++------- 12 files changed, 166 insertions(+), 151 deletions(-) create mode 100644 doc/examples/isometric/.gitignore create mode 100644 doc/examples/spritesheet/flutter_01.png diff --git a/doc/examples/animation_widget/lib/main.dart b/doc/examples/animation_widget/lib/main.dart index 82182360a..94c4290b8 100644 --- a/doc/examples/animation_widget/lib/main.dart +++ b/doc/examples/animation_widget/lib/main.dart @@ -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 { - Vector2 _position = Vector2(256.0, 256.0); + Vector2 _position = Vector2.all(256); @override void initState() { @@ -60,9 +57,7 @@ class _MyHomePageState extends State { 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 key) { diff --git a/doc/examples/isometric/.gitignore b/doc/examples/isometric/.gitignore new file mode 100644 index 000000000..9d532b18a --- /dev/null +++ b/doc/examples/isometric/.gitignore @@ -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 diff --git a/doc/examples/isometric/lib/main.dart b/doc/examples/isometric/lib/main.dart index 1a3fbc35a..4fd7caba4 100644 --- a/doc/examples/isometric/lib/main.dart +++ b/doc/examples/isometric/lib/main.dart @@ -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, ); diff --git a/doc/examples/particles/lib/main.dart b/doc/examples/particles/lib/main.dart index cf2022037..db8d41115 100644 --- a/doc/examples/particles/lib/main.dart +++ b/doc/examples/particles/lib/main.dart @@ -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.generate( frames, - (i) => spritesheet.getSprite(i ~/ rows, i % columns), + (i) => spritesheet.getSpriteById(i), ); return SpriteAnimation.spriteList(sprites); diff --git a/doc/examples/spritesheet/flutter_01.png b/doc/examples/spritesheet/flutter_01.png new file mode 100644 index 0000000000000000000000000000000000000000..9c0bf9b7ca248b7a6e3dfe2cc50a50a3f97d35d4 GIT binary patch literal 37903 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 literal 0 HcmV?d00001 diff --git a/doc/examples/spritesheet/lib/main.dart b/doc/examples/spritesheet/lib/main.dart index 571a8355c..194f7232a 100644 --- a/doc/examples/spritesheet/lib/main.dart +++ b/doc/examples/spritesheet/lib/main.dart @@ -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 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) diff --git a/doc/examples/widgets/lib/main.dart b/doc/examples/widgets/lib/main.dart index ea2ac004a..70d240676 100644 --- a/doc/examples/widgets/lib/main.dart +++ b/doc/examples/widgets/lib/main.dart @@ -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, diff --git a/doc/images.md b/doc/images.md index b6feabcbb..c378a9ac7 100644 --- a/doc/images.md +++ b/doc/images.md @@ -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); diff --git a/doc/particles.md b/doc/particles.md index 98c9df58b..43f623e68 100644 --- a/doc/particles.md +++ b/doc/particles.md @@ -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( diff --git a/lib/components/isometric_tile_map_component.dart b/lib/components/isometric_tile_map_component.dart index f95f6758d..a372000f3 100644 --- a/lib/components/isometric_tile_map_component.dart +++ b/lib/components/isometric_tile_map_component.dart @@ -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 _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 . /// /// 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> 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); } diff --git a/lib/sprite_animation.dart b/lib/sprite_animation.dart index ccfc6d832..8e7a476a8 100644 --- a/lib/sprite_animation.dart +++ b/lib/sprite_animation.dart @@ -48,8 +48,11 @@ class SpriteAnimation { /// Creates an animation based on the parameters. /// /// All frames have the same [stepTime]. - SpriteAnimation.spriteList(List sprites, - {double stepTime, this.loop = true}) { + SpriteAnimation.spriteList( + List sprites, { + double stepTime, + this.loop = true, + }) { if (sprites.isEmpty) { throw Exception('You must have at least one frame!'); } diff --git a/lib/spritesheet.dart b/lib/spritesheet.dart index ead49b04b..15111fee5 100644 --- a/lib/spritesheet.dart +++ b/lib/spritesheet.dart @@ -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> _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 _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.generate(to - from, (i) => from + i) + .map((e) => getSprite(row, e)) + .toList(); return SpriteAnimation.spriteList( spriteList,