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 000000000..9c0bf9b7c Binary files /dev/null and b/doc/examples/spritesheet/flutter_01.png differ 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,