Skip to content

Commit

Permalink
fix!: Change Tiled batched rendering to batched rendering per layer (#…
Browse files Browse the repository at this point in the history
…1317)

This fix introduces a list of SpriteBatch maps corresponding to the visible TileLayers. The BatchMaps are then rendered in order of the layers.
  • Loading branch information
lfraker authored Jan 16, 2022
1 parent d77e5ef commit 30fce39
Show file tree
Hide file tree
Showing 6 changed files with 170 additions and 21 deletions.
58 changes: 41 additions & 17 deletions packages/flame_tiled/lib/src/renderable_tile_map.dart
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import 'dart:math' as math;

import 'package:collection/collection.dart';
import 'package:flame/extensions.dart';
import 'package:flame/flame.dart';
import 'package:flame/sprite.dart';
Expand All @@ -17,21 +18,29 @@ class RenderableTiledMap {
/// [TiledMap] instance for this map.
final TiledMap map;

/// Cached [SpriteBatch]es of this map.
final Map<String, SpriteBatch> batches;

/// The size of tile to be rendered on the game.
final Vector2 destTileSize;

/// Cached list of [SpriteBatch]es, ordered by layer.
final List<Map<String, SpriteBatch>> batchesByLayer;

/// {@macro _renderable_tiled_map}
RenderableTiledMap(
this.map,
this.batches,
this.batchesByLayer,
this.destTileSize,
) {
_fillBatches();
}

/// Cached [SpriteBatch]es of this map.
@Deprecated(
'If you take a direct dependency on batches, use batchesByLayer instead',
)
Map<String, SpriteBatch> get batches => batchesByLayer.isNotEmpty
? batchesByLayer.first
: <String, SpriteBatch>{};

/// Parses a file returning a [RenderableTiledMap].
///
/// NOTE: this method looks for files under the path "assets/tiles/".
Expand All @@ -49,9 +58,19 @@ class RenderableTiledMap {
Vector2 destTileSize,
) async {
final map = await _loadMap(contents);
final batches = await _loadImages(map);
final batchesByLayer = await Future.wait(
_renderableTileLayers(map).map((e) => _loadImages(map)),
);

return RenderableTiledMap(
map,
batchesByLayer,
destTileSize,
);
}

return RenderableTiledMap(map, batches, destTileSize);
static Iterable<TileLayer> _renderableTileLayers(TiledMap map) {
return map.layers.where((layer) => layer.visible).whereType<TileLayer>();
}

static Future<TiledMap> _loadMap(String contents) async {
Expand Down Expand Up @@ -80,23 +99,26 @@ class RenderableTiledMap {
result[src] = await SpriteBatch.load(src);
}
});

return result;
}

void _fillBatches() {
for (final batch in batches.keys) {
batches[batch]!.clear();
}
batchesByLayer.forEach(
(batchMap) => batchMap.values.forEach((batch) => batch.clear),
);

map.layers
.where((layer) => layer.visible)
.whereType<TileLayer>()
_renderableTileLayers(map)
.map((e) => e.tileData)
.whereType<List<List<Gid>>>()
.forEach(_renderLayer);
.forEachIndexed(_renderLayer);
}

void _renderLayer(List<List<Gid>> tileData) {
void _renderLayer(
int mapIndex,
List<List<Gid>> tileData,
) {
final batchMap = batchesByLayer.elementAt(mapIndex);
tileData.asMap().forEach((ty, tileRow) {
tileRow.asMap().forEach((tx, tile) {
if (tile.tile == 0) {
Expand All @@ -106,7 +128,7 @@ class RenderableTiledMap {
final ts = map.tilesetByTileGId(tile.tile);
final img = t.image ?? ts.image;
if (img != null) {
final batch = batches[img.source];
final batch = batchMap[img.source];
final src = ts.computeDrawRect(t).toRect();
final flips = SimpleFlips.fromFlips(tile.flips);
final size = destTileSize;
Expand All @@ -123,9 +145,11 @@ class RenderableTiledMap {
});
}

/// Render all [batches] that compose this tile map.
/// Render [batchesByLayer] that compose this tile map.
void render(Canvas c) {
batches.forEach((_, batch) => batch.render(c));
batchesByLayer.forEach((batchMap) {
batchMap.forEach((_, batch) => batch.render(c));
});
}

/// This returns an object group fetch by name from a given layer.
Expand Down
1 change: 1 addition & 0 deletions packages/flame_tiled/pubspec.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ dependencies:
tiled: ^0.7.2
xml: ^5.3.0
meta: ^1.7.0
collection: ^1.15.0
flutter:
sdk: flutter

Expand Down
19 changes: 19 additions & 0 deletions packages/flame_tiled/test/assets/2_tiles-green_on_red.tmx
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
<?xml version="1.0" encoding="UTF-8"?>
<map version="1.5" tiledversion="1.7.2" orientation="orthogonal" renderorder="right-down" width="2" height="1" tilewidth="16" tileheight="16" infinite="0" nextlayerid="3" nextobjectid="1">
<tileset firstgid="1" name="green_tile" tilewidth="16" tileheight="16" tilecount="1" columns="1">
<image source="green_sprite.png" width="16" height="16"/>
</tileset>
<tileset firstgid="2" name="red_tile-base" tilewidth="16" tileheight="16" tilecount="1" columns="1">
<image source="red_sprite.png" width="16" height="16"/>
</tileset>
<layer id="2" name="red_tile-base" width="2" height="1">
<data encoding="base64" compression="zlib">
eJxjYmBgYAJiAAAgAAU=
</data>
</layer>
<layer id="1" name="green_tile-top" width="2" height="1">
<data encoding="base64" compression="zlib">
eJxjZIAAAAAQAAI=
</data>
</layer>
</map>
Binary file added packages/flame_tiled/test/assets/green_sprite.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Binary file added packages/flame_tiled/test/assets/red_sprite.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
113 changes: 109 additions & 4 deletions packages/flame_tiled/test/tiled_test.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
import 'dart:async';
import 'dart:io';
import 'dart:typed_data';
import 'dart:ui';

import 'package:flame/extensions.dart';
import 'package:flame/flame.dart';
Expand All @@ -10,22 +11,126 @@ import 'package:test/test.dart';

void main() {
test('correct loads the file', () async {
Flame.bundle = TestAssetBundle();
Flame.bundle = TestAssetBundle(
imageNames: ['map-level1.png'],
mapPath: 'test/assets/map.tmx',
);
final tiled = await TiledComponent.load('x', Vector2.all(16));
expect(tiled.tileMap.batches, isNotEmpty);
expect(tiled.tileMap.batchesByLayer.length == 1, true);
});

group('Layered tiles render correctly with layered sprite batch', () {
late Uint8List canvasPixelData;
late RenderableTiledMap overlapMap;
setUp(() async {
Flame.bundle = TestAssetBundle(
imageNames: [
'green_sprite.png',
'red_sprite.png',
],
mapPath: 'test/assets/2_tiles-green_on_red.tmx',
);
overlapMap = await RenderableTiledMap.fromFile(
'2_tiles-green_on_red.tmx',
Vector2.all(16),
);
final canvasRecorder = PictureRecorder();
final canvas = Canvas(canvasRecorder);
overlapMap.render(canvas);
final picture = canvasRecorder.endRecording();

final image = await picture.toImage(32, 16);
final bytes = await image.toByteData();
canvasPixelData = bytes!.buffer.asUint8List();
});

test(
'Correctly loads batches list',
() => expect(overlapMap.batchesByLayer.length == 2, true),
);

test(
'Canvas pixel dimensions match',
() => expect(
canvasPixelData.length == 16 * 32 * 4,
true,
),
);

test('Base test - right tile pixel is red', () {
expect(
canvasPixelData[16 * 4] == 255 &&
canvasPixelData[(16 * 4) + 1] == 0 &&
canvasPixelData[(16 * 4) + 2] == 0 &&
canvasPixelData[(16 * 4) + 3] == 255,
true,
);
final rightTilePixels = <int>[];
for (var ind = 16 * 4; ind < 16 * 32 * 4; ind += 32 * 4) {
rightTilePixels.addAll(canvasPixelData.getRange(ind, ind + (16 * 4)));
}

var allRed = true;
for (var indRed = 0; indRed < rightTilePixels.length; indRed += 4) {
allRed &= rightTilePixels[indRed] == 255 &&
rightTilePixels[indRed + 1] == 0 &&
rightTilePixels[indRed + 2] == 0 &&
rightTilePixels[indRed + 3] == 255;
}
expect(allRed, true);
});

test('Left tile pixel is green', () {
expect(
canvasPixelData[15 * 4] == 0 &&
canvasPixelData[(15 * 4) + 1] == 255 &&
canvasPixelData[(15 * 4) + 2] == 0 &&
canvasPixelData[(15 * 4) + 3] == 255,
true,
);

final leftTilePixels = <int>[];
for (var ind = 0; ind < 15 * 32 * 4; ind += 32 * 4) {
leftTilePixels.addAll(canvasPixelData.getRange(ind, ind + (16 * 4)));
}

var allGreen = true;
for (var indGreen = 0; indGreen < leftTilePixels.length; indGreen += 4) {
allGreen &= leftTilePixels[indGreen] == 0 &&
leftTilePixels[indGreen + 1] == 255 &&
leftTilePixels[indGreen + 2] == 0 &&
leftTilePixels[indGreen + 3] == 255;
}
expect(allGreen, true);
});
});
}

class TestAssetBundle extends CachingAssetBundle {
TestAssetBundle({
required this.imageNames,
required this.mapPath,
});

final List<String> imageNames;
final String mapPath;

@override
Future<ByteData> load(String key) async {
return File('test/assets/map-level1.png')
final split = key.split('/');
final imgName = split.isNotEmpty ? split.last : key;

var toLoadName = key.split('/').last;
if (!imageNames.contains(imgName) && imageNames.isNotEmpty) {
toLoadName = imageNames.first;
}
return File('test/assets/$toLoadName')
.readAsBytes()
.then((bytes) => ByteData.view(Uint8List.fromList(bytes).buffer));
}

@override
Future<String> loadString(String key, {bool cache = true}) {
return File('test/assets/map.tmx').readAsString();
return File(mapPath).readAsString();
}
}

0 comments on commit 30fce39

Please sign in to comment.