mirror of
https://github.com/flame-engine/flame.git
synced 2025-11-01 10:38:17 +08:00
Improve IsometricTileMap and Spritesheet classes
This commit is contained in:
@ -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
41
doc/examples/isometric/.gitignore
vendored
Normal 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
|
||||
@ -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,
|
||||
);
|
||||
|
||||
@ -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);
|
||||
|
||||
BIN
doc/examples/spritesheet/flutter_01.png
Normal file
BIN
doc/examples/spritesheet/flutter_01.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 37 KiB |
@ -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)
|
||||
|
||||
@ -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,
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -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(
|
||||
|
||||
@ -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);
|
||||
}
|
||||
|
||||
|
||||
@ -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!');
|
||||
}
|
||||
|
||||
@ -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,
|
||||
|
||||
Reference in New Issue
Block a user