mirror of
https://github.com/flame-engine/flame.git
synced 2025-10-30 00:17:20 +08:00
TileStack (#1910)
Support searching for, and collecting stacks of tiles.
This commit is contained in:
@ -965,14 +965,46 @@ void main() {
|
||||
|
||||
## TiledComponent
|
||||
|
||||
Currently we have a very basic implementation of a Tiled component. This API uses the lib
|
||||
[tiled.dart](https://github.com/flame-engine/tiled.dart) to parse map files and render visible
|
||||
layers.
|
||||
Tiled is a free and open source, full-featured level and map editor for your platformer or
|
||||
RPG game. Currently we have an "in progress" implementation of a Tiled component. This API
|
||||
uses the lib [tiled.dart](https://github.com/flame-engine/tiled.dart) to parse map files and
|
||||
render visible layers using the performant `SpriteBatch` for each layer.
|
||||
|
||||
Supported map types include: Orthogonal, Isometric, Hexagonal, and Staggered.
|
||||
|
||||
Orthogonal | Hexagonal | Isomorphic
|
||||
:--:|:-------------------------:|:-------------------------:
|
||||
| | 
|
||||
|
||||
An example of how to use the API can be found
|
||||
[here](https://github.com/flame-engine/flame_tiled/tree/main/example).
|
||||
|
||||
|
||||
### TileStack
|
||||
|
||||
Once a `TiledComponent` is loaded, you can select any column of (x,y) tiles in a `tileStack` to
|
||||
then add animation. Removing the stack will not remove the tiles from the map.
|
||||
|
||||
> **Note**: This currently only supports position based effects.
|
||||
|
||||
```dart
|
||||
final stack = map.tileMap.tileStack(4, 0, named: {'floor_under'});
|
||||
stack.add(
|
||||
SequenceEffect(
|
||||
[
|
||||
MoveEffect.by(
|
||||
Vector2(5, 0),
|
||||
NoiseEffectController(duration: 1, frequency: 20),
|
||||
),
|
||||
MoveEffect.by(Vector2.zero(), LinearEffectController(2)),
|
||||
],
|
||||
repeatCount: 3,
|
||||
)..onComplete = () => stack.removeFromParent(),
|
||||
);
|
||||
map.add(stack);
|
||||
```
|
||||
|
||||
|
||||
## IsometricTileMapComponent
|
||||
|
||||
This component allows you to render an isometric map based on a cartesian matrix of blocks and an
|
||||
|
||||
BIN
doc/images/orthogonal.png
Normal file
BIN
doc/images/orthogonal.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
BIN
doc/images/pointy_hex_even.png
Normal file
BIN
doc/images/pointy_hex_even.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 45 KiB |
BIN
doc/images/tile_stack_single_move.png
Normal file
BIN
doc/images/tile_stack_single_move.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
67
packages/flame_tiled/lib/src/mutable_transform.dart
Normal file
67
packages/flame_tiled/lib/src/mutable_transform.dart
Normal file
@ -0,0 +1,67 @@
|
||||
import 'dart:typed_data';
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/effects.dart';
|
||||
|
||||
/// A mutable version of [RSTransform] for custom batch manipulation.
|
||||
class MutableRSTransform implements RSTransform, PositionProvider {
|
||||
final _values = Float32List(4);
|
||||
|
||||
/// This is a cache of `-scos * anchorX + ssin * anchorY`
|
||||
final double _anchorX;
|
||||
|
||||
/// This is a cache of `-ssin * anchorX - scos * anchorY`
|
||||
final double _anchorY;
|
||||
|
||||
final Vector2 _position;
|
||||
|
||||
MutableRSTransform(
|
||||
double scos,
|
||||
double ssin,
|
||||
double tx,
|
||||
double ty,
|
||||
this._anchorX,
|
||||
this._anchorY,
|
||||
) : _position = Vector2(tx, ty) {
|
||||
_values[0] = scos;
|
||||
_values[1] = ssin;
|
||||
_values[2] = tx + _anchorX;
|
||||
_values[3] = ty + _anchorY;
|
||||
}
|
||||
|
||||
/// The cosine of the rotation multiplied by the scale factor.
|
||||
@override
|
||||
double get scos => _values[0];
|
||||
set scos(double scos) => _values[0] = scos;
|
||||
|
||||
/// The sine of the rotation multiplied by that same scale factor.
|
||||
@override
|
||||
double get ssin => _values[1];
|
||||
set ssin(double ssin) => _values[1] = ssin;
|
||||
|
||||
/// The x coordinate of the translation, minus [scos] multiplied by the
|
||||
/// x-coordinate of the rotation point, plus [ssin] multiplied by the
|
||||
/// y-coordinate of the rotation point.
|
||||
@override
|
||||
double get tx => _values[2];
|
||||
set tx(double tx) => _values[2] = tx;
|
||||
|
||||
/// The y coordinate of the translation, minus [ssin] multiplied by the
|
||||
/// x-coordinate of the rotation point, minus [scos] multiplied by the
|
||||
/// y-coordinate of the rotation point.
|
||||
@override
|
||||
double get ty => _values[3];
|
||||
set ty(double ty) => _values[3] = ty;
|
||||
|
||||
@override
|
||||
set position(Vector2 value) {
|
||||
_values[2] = value.x + _anchorX;
|
||||
_values[3] = value.y + _anchorY;
|
||||
_position.x = value.x;
|
||||
_position.y = value.y;
|
||||
}
|
||||
|
||||
@override
|
||||
Vector2 get position => _position;
|
||||
}
|
||||
@ -2,6 +2,7 @@ import 'package:flame/extensions.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame/sprite.dart';
|
||||
import 'package:flame_tiled/flame_tiled.dart';
|
||||
import 'package:flame_tiled/src/mutable_transform.dart';
|
||||
import 'package:flame_tiled/src/renderable_layers/group_layer.dart';
|
||||
import 'package:flame_tiled/src/renderable_layers/renderable_layer.dart';
|
||||
import 'package:flame_tiled/src/tile_transform.dart';
|
||||
@ -13,6 +14,7 @@ import 'package:tiled/tiled.dart' as tiled;
|
||||
class TileLayer extends RenderableLayer<tiled.TileLayer> {
|
||||
late final _layerPaint = Paint();
|
||||
late final Map<String, SpriteBatch> _cachedSpriteBatches;
|
||||
late List<List<MutableRSTransform?>> indexes;
|
||||
|
||||
TileLayer(
|
||||
super.layer,
|
||||
@ -26,6 +28,11 @@ class TileLayer extends RenderableLayer<tiled.TileLayer> {
|
||||
|
||||
@override
|
||||
void refreshCache() {
|
||||
indexes = List.generate(
|
||||
layer.width,
|
||||
(index) => List.filled(layer.height, null),
|
||||
);
|
||||
|
||||
_cacheLayerTiles();
|
||||
}
|
||||
|
||||
@ -95,14 +102,18 @@ class TileLayer extends RenderableLayer<tiled.TileLayer> {
|
||||
final scos = flips.cos * scale;
|
||||
final ssin = flips.sin * scale;
|
||||
|
||||
indexes[tx][ty] = MutableRSTransform(
|
||||
scos,
|
||||
ssin,
|
||||
offsetX,
|
||||
offsetY,
|
||||
-scos * anchorX + ssin * anchorY,
|
||||
-ssin * anchorX - scos * anchorY,
|
||||
);
|
||||
|
||||
batch.addTransform(
|
||||
source: src,
|
||||
transform: RSTransform(
|
||||
scos,
|
||||
ssin,
|
||||
offsetX + -scos * anchorX + ssin * anchorY,
|
||||
offsetY + -ssin * anchorX - scos * anchorY,
|
||||
),
|
||||
transform: indexes[tx][ty],
|
||||
flip: flips.flip,
|
||||
);
|
||||
}
|
||||
@ -152,14 +163,18 @@ class TileLayer extends RenderableLayer<tiled.TileLayer> {
|
||||
final scos = flips.cos * scale;
|
||||
final ssin = flips.sin * scale;
|
||||
|
||||
indexes[tx][ty] = MutableRSTransform(
|
||||
scos,
|
||||
ssin,
|
||||
offsetX,
|
||||
offsetY,
|
||||
-scos * anchorX + ssin * anchorY,
|
||||
-ssin * anchorX - scos * anchorY,
|
||||
);
|
||||
|
||||
batch.addTransform(
|
||||
source: src,
|
||||
transform: RSTransform(
|
||||
scos,
|
||||
ssin,
|
||||
offsetX + -scos * anchorX + ssin * anchorY,
|
||||
offsetY + -ssin * anchorX - scos * anchorY,
|
||||
),
|
||||
transform: indexes[tx][ty],
|
||||
flip: flips.flip,
|
||||
);
|
||||
}
|
||||
@ -257,11 +272,13 @@ class TileLayer extends RenderableLayer<tiled.TileLayer> {
|
||||
final scos = flips.cos * scale;
|
||||
final ssin = flips.sin * scale;
|
||||
|
||||
final transform = RSTransform(
|
||||
final transform = indexes[tx][ty] = MutableRSTransform(
|
||||
scos,
|
||||
ssin,
|
||||
offsetX + -scos * anchorX + ssin * anchorY,
|
||||
offsetY + -ssin * anchorX - scos * anchorY,
|
||||
offsetX,
|
||||
offsetY,
|
||||
-scos * anchorX + ssin * anchorY,
|
||||
-ssin * anchorX - scos * anchorY,
|
||||
);
|
||||
|
||||
// A second pass is only needed in the case of staggery.
|
||||
@ -377,13 +394,14 @@ class TileLayer extends RenderableLayer<tiled.TileLayer> {
|
||||
final scos = flips.cos * scale;
|
||||
final ssin = flips.sin * scale;
|
||||
|
||||
final transform = RSTransform(
|
||||
final transform = indexes[tx][ty] = MutableRSTransform(
|
||||
scos,
|
||||
ssin,
|
||||
offsetX + -scos * anchorX + ssin * anchorY,
|
||||
offsetY + -ssin * anchorX - scos * anchorY,
|
||||
offsetX,
|
||||
offsetY,
|
||||
-scos * anchorX + ssin * anchorY,
|
||||
-ssin * anchorX - scos * anchorY,
|
||||
);
|
||||
|
||||
// A second pass is only needed in the case of staggery.
|
||||
if (map.staggerAxis == tiled.StaggerAxis.x && staggerY > 0) {
|
||||
xSecondPass.add(TileTransform(src, transform, flips.flip, batch));
|
||||
|
||||
@ -5,11 +5,13 @@ import 'package:flame/extensions.dart';
|
||||
import 'package:flame/flame.dart';
|
||||
import 'package:flame/game.dart';
|
||||
import 'package:flame_tiled/src/flame_tsx_provider.dart';
|
||||
import 'package:flame_tiled/src/mutable_transform.dart';
|
||||
import 'package:flame_tiled/src/renderable_layers/group_layer.dart';
|
||||
import 'package:flame_tiled/src/renderable_layers/image_layer.dart';
|
||||
import 'package:flame_tiled/src/renderable_layers/object_layer.dart';
|
||||
import 'package:flame_tiled/src/renderable_layers/renderable_layer.dart';
|
||||
import 'package:flame_tiled/src/renderable_layers/tile_layer.dart';
|
||||
import 'package:flame_tiled/src/tile_stack.dart';
|
||||
import 'package:flutter/painting.dart';
|
||||
import 'package:tiled/tiled.dart' as tiled;
|
||||
|
||||
@ -115,6 +117,74 @@ class RenderableTiledMap {
|
||||
return null;
|
||||
}
|
||||
|
||||
/// Select a group of tiles from the coordinates [x] and [y].
|
||||
///
|
||||
/// If [all] is set to true, every renderable tile from the map is collected.
|
||||
///
|
||||
/// If the [named] or [ids] sets are not empty, any layer with matching
|
||||
/// name or id will have their renderable tiles collected. If the matching
|
||||
/// layer is a group layer, all layers in the group will have their tiles
|
||||
/// collected.
|
||||
TileStack tileStack(
|
||||
int x,
|
||||
int y, {
|
||||
Set<String> named = const <String>{},
|
||||
Set<int> ids = const <int>{},
|
||||
bool all = false,
|
||||
}) {
|
||||
return TileStack(
|
||||
_tileStack(
|
||||
renderableLayers,
|
||||
x,
|
||||
y,
|
||||
named: named,
|
||||
ids: ids,
|
||||
all: all,
|
||||
),
|
||||
);
|
||||
}
|
||||
|
||||
/// Recursive support for [tileStack]
|
||||
List<MutableRSTransform> _tileStack(
|
||||
List<RenderableLayer> layers,
|
||||
int x,
|
||||
int y, {
|
||||
Set<String> named = const <String>{},
|
||||
Set<int> ids = const <int>{},
|
||||
bool all = false,
|
||||
}) {
|
||||
final tiles = <MutableRSTransform>[];
|
||||
for (final layer in layers) {
|
||||
if (layer is GroupLayer) {
|
||||
// if the group matches named or ids; grab every child.
|
||||
// else descend and ask for named children.
|
||||
tiles.addAll(
|
||||
_tileStack(
|
||||
layer.children,
|
||||
x,
|
||||
y,
|
||||
named: named,
|
||||
ids: ids,
|
||||
all: all ||
|
||||
named.contains(layer.layer.name) ||
|
||||
ids.contains(layer.layer.id),
|
||||
),
|
||||
);
|
||||
} else if (layer is TileLayer) {
|
||||
if (!(all ||
|
||||
named.contains(layer.layer.name) ||
|
||||
ids.contains(layer.layer.id))) {
|
||||
continue;
|
||||
}
|
||||
|
||||
if (layer.indexes[x][y] != null) {
|
||||
tiles.add(layer.indexes[x][y]!);
|
||||
}
|
||||
}
|
||||
}
|
||||
return tiles;
|
||||
}
|
||||
|
||||
/// Parses a file returning a [RenderableTiledMap].
|
||||
///
|
||||
/// NOTE: this method looks for files under the path "assets/tiles/".
|
||||
|
||||
27
packages/flame_tiled/lib/src/tile_stack.dart
Normal file
27
packages/flame_tiled/lib/src/tile_stack.dart
Normal file
@ -0,0 +1,27 @@
|
||||
import 'package:flame/components.dart';
|
||||
import 'package:flame/effects.dart';
|
||||
import 'package:flame_tiled/src/mutable_transform.dart';
|
||||
|
||||
/// A select group of tiles from RenderableTiledMap that can be animated.
|
||||
///
|
||||
/// Tiles are nothing more than an x/y coordinate in each layer. TileStack lets
|
||||
/// you collect a certain group of tiles out of all the layers, and then
|
||||
/// set their positions. This is typically done by using Flame's effects.
|
||||
class TileStack extends Component implements PositionProvider {
|
||||
final List<MutableRSTransform> _tiles;
|
||||
|
||||
/// The number of tiles in this stack.
|
||||
int get length => _tiles.length;
|
||||
|
||||
TileStack(this._tiles);
|
||||
|
||||
@override
|
||||
Vector2 get position => _tiles.first.position;
|
||||
|
||||
@override
|
||||
set position(Vector2 position) {
|
||||
for (final tile in _tiles) {
|
||||
tile.position = position;
|
||||
}
|
||||
}
|
||||
}
|
||||
@ -1,13 +1,14 @@
|
||||
import 'dart:ui';
|
||||
|
||||
import 'package:flame/sprite.dart';
|
||||
import 'package:flame_tiled/src/mutable_transform.dart';
|
||||
import 'package:meta/meta.dart';
|
||||
|
||||
/// Caches transforms for staggered maps as the row/col are switched.
|
||||
@internal
|
||||
class TileTransform {
|
||||
final Rect source;
|
||||
final RSTransform transform;
|
||||
final MutableRSTransform transform;
|
||||
final bool flip;
|
||||
final SpriteBatch batch;
|
||||
|
||||
|
||||
@ -1,5 +1,5 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<map version="1.9" tiledversion="1.9.1" orientation="isometric" renderorder="right-down" width="5" height="5" tilewidth="128" tileheight="64" infinite="0" nextlayerid="4" nextobjectid="1">
|
||||
<map version="1.9" tiledversion="1.9.1" orientation="isometric" renderorder="right-down" width="5" height="5" tilewidth="128" tileheight="64" infinite="0" nextlayerid="5" nextobjectid="1">
|
||||
<tileset firstgid="1" name="isometric_spritesheet" tilewidth="128" tileheight="256" tilecount="4" columns="1">
|
||||
<image source="isometric_spritesheet.png" width="128" height="1024"/>
|
||||
</tileset>
|
||||
@ -8,9 +8,14 @@
|
||||
AgAAAAIAAAACAAAAAgAAAAIAAAACAAAAAgAAAAQAAAACAAAAAwAAAAMAAAACAAAAAwAAAAIAAAADAAAAAgAAAAIAAAAEAACAAgAAAAMAAAACAAAAAwAAAAIAAAACAAAAAgAAAA==
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="2" name="item" width="5" height="5">
|
||||
<layer id="2" name="item" class="items" width="5" height="5">
|
||||
<data encoding="base64">
|
||||
AQAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAABAACAAAAAAAAAAAAAAAAAAAAAAA==
|
||||
</data>
|
||||
</layer>
|
||||
<layer id="4" name="emty" width="5" height="5">
|
||||
<data encoding="base64">
|
||||
AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA==
|
||||
</data>
|
||||
</layer>
|
||||
</map>
|
||||
|
||||
BIN
packages/flame_tiled/test/goldens/tile_stack_all_move.png
Normal file
BIN
packages/flame_tiled/test/goldens/tile_stack_all_move.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 102 KiB |
BIN
packages/flame_tiled/test/goldens/tile_stack_single_move.png
Normal file
BIN
packages/flame_tiled/test/goldens/tile_stack_single_move.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 104 KiB |
@ -310,8 +310,8 @@ void main() {
|
||||
|
||||
Future<Uint8List> renderMapToPng(
|
||||
TiledComponent component,
|
||||
int width,
|
||||
int height,
|
||||
num width,
|
||||
num height,
|
||||
) async {
|
||||
final canvasRecorder = PictureRecorder();
|
||||
final canvas = Canvas(canvasRecorder);
|
||||
@ -320,7 +320,7 @@ void main() {
|
||||
|
||||
// Map size is now 320 wide, but it has 1 extra tile of height becusae
|
||||
// its actually double-height tiles.
|
||||
final image = await picture.toImageSafe(width, height);
|
||||
final image = await picture.toImageSafe(width.toInt(), height.toInt());
|
||||
return (await image.toByteData(format: ImageByteFormat.png))!
|
||||
.buffer
|
||||
.asUint8List();
|
||||
@ -624,6 +624,65 @@ void main() {
|
||||
);
|
||||
});
|
||||
});
|
||||
|
||||
group('TileStack', () {
|
||||
late TiledComponent component;
|
||||
final size = Vector2(256 / 2, 128 / 2);
|
||||
|
||||
setUp(() async {
|
||||
Flame.bundle = TestAssetBundle(
|
||||
imageNames: [
|
||||
'isometric_spritesheet.png',
|
||||
],
|
||||
mapPath: 'test/assets/test_isometric.tmx',
|
||||
);
|
||||
component = await TiledComponent.load('test_isometric.tmx', size);
|
||||
});
|
||||
test('from all layers', () {
|
||||
var stack = component.tileMap.tileStack(0, 0, all: true);
|
||||
expect(stack.length, 2);
|
||||
|
||||
stack = component.tileMap.tileStack(1, 0, all: true);
|
||||
expect(stack.length, 1);
|
||||
});
|
||||
|
||||
test('from some layers', () {
|
||||
var stack = component.tileMap.tileStack(0, 0, named: {'empty'});
|
||||
expect(stack.length, 0);
|
||||
|
||||
stack = component.tileMap.tileStack(0, 0, named: {'item'});
|
||||
expect(stack.length, 1);
|
||||
|
||||
stack = component.tileMap.tileStack(0, 0, ids: {1});
|
||||
expect(stack.length, 1);
|
||||
|
||||
stack = component.tileMap.tileStack(0, 0, ids: {1, 2});
|
||||
expect(stack.length, 2);
|
||||
});
|
||||
|
||||
test('can be positioned together', () async {
|
||||
final stack = component.tileMap.tileStack(0, 0, all: true);
|
||||
stack.position = stack.position + Vector2.all(20);
|
||||
|
||||
final pngData =
|
||||
await renderMapToPng(component, size.x * 5, size.y * 5 + size.y / 2);
|
||||
expect(
|
||||
pngData,
|
||||
matchesGoldenFile('goldens/tile_stack_all_move.png'),
|
||||
);
|
||||
});
|
||||
|
||||
test('can be positioned singularly', () async {
|
||||
final stack = component.tileMap.tileStack(0, 0, named: {'item'});
|
||||
stack.position = stack.position + Vector2(-20, 20);
|
||||
|
||||
final pngData = await renderMapToPng(component, size.x * 5, size.y * 5);
|
||||
expect(
|
||||
pngData,
|
||||
matchesGoldenFile('goldens/tile_stack_single_move.png'),
|
||||
);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
class TestAssetBundle extends CachingAssetBundle {
|
||||
|
||||
Reference in New Issue
Block a user