Support searching for, and collecting stacks of tiles.
This commit is contained in:
John McDole
2022-09-19 23:25:34 -07:00
committed by GitHub
parent 6021471796
commit f4e8af2c90
13 changed files with 307 additions and 28 deletions

View File

@ -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 an orthogonal map](../images/orthogonal.png)|![An example of hexagonal map](../images/pointy_hex_even.png) | ![An example of isomorphic map](../images/tile_stack_single_move.png)
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

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 45 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

View 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;
}

View File

@ -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));

View File

@ -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/".

View 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;
}
}
}

View File

@ -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;

View File

@ -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>

Binary file not shown.

After

Width:  |  Height:  |  Size: 102 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

View File

@ -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 {