onLoad() async {
+ await super.onLoad();
+
+ // Add our renderable tile map
await add(tileMap);
}
diff --git a/packages/flame_tiled/pubspec.yaml b/packages/flame_tiled/pubspec.yaml
index 8738043db87..ea94b8b19e7 100644
--- a/packages/flame_tiled/pubspec.yaml
+++ b/packages/flame_tiled/pubspec.yaml
@@ -27,6 +27,7 @@ dependencies:
dev_dependencies:
dartdoc: ^8.0.8
flame_lint: ^1.2.1
+ flame_test: ^1.17.2
flutter_test:
sdk: flutter
test: any
diff --git a/packages/flame_tiled/test/assets/images/map-level1.png b/packages/flame_tiled/test/assets/images/map-level1.png
new file mode 100644
index 0000000000000000000000000000000000000000..541b07a59a88b16dc03efcf872fd201a90347ea6
GIT binary patch
literal 3059
zcma);XHXN`630U|C?G{3G-*<#H!%V?2!`IIODB}jdza9qN)zdV;VFVNK|nx4?;QlB
zBhAojD3UclI}Xc6R==3D0#@scy5}1^@t5>S|DZ0Dy?*<{kkg
zzOh=t|M~_1+-XvWDjEjm?-#UrvZ^xnaZm-$0Es75JjjTjSn)pYtSs{#5Qw>}_Hlyz
z%aOk~%qig>b=sN*8%-c>8lPZywV&WvNAjZkZKbCX0DWd4%_+xLq9cTAb7nLRwP=aC
zjN}yN;BZv!L|`{&Mt5nq?!`^Men6<*HCQ&c4n3ce`Gvqsv$~b9#m3ik)Xy4uuh(tz
z3zkAGH(`?ufMhPSiOjAt8}(}W*{#~Dx?YhL#T)mCZ$PNv8cbp~{b=qn6%=$tF+JR`
zPwd)d+z)plrzMASeT})nogeE>TUh3iMiKF?hRq!76(_!nW?KJgTqg2V+;vn<1oQAm
z()|}X(M)!^Brm?W6p%)l`ex{t=KVNZX{Kp#DW9j{%<6HP9_Q7ndYA}
zLMcoPACMt7TKvppmp~E8B{vQ0yaA>_W*gg%UaN8^4fE|v^woQ^&{ppdr9_{7CkknCcpE25
zOgd?__Q)<3cA6k&E|cWj!dnfgph5lv*9?9zvb&N
zXA?46|EEDmJRtgWmQ_~x$yi?qo}HWJ;81j30qdm*
z46Ne*&iZz{d7)9Aj`(5?8f9VTUbaN@>vvbo@se2bJ9EOWqi#N$^lEMuy%2a)h2FMP
zSXLI?wptE9*cPrgtxo)bIC)&Q!)vI1opR)L(bT941+W#^HM6%Tw7e}AkRoMq@w=<1
zgbb1m;%2%$WfvgQi&SwcumO4GVJurCTBJK?F-IBU_m3Qlyew_bNsjHc`{z^pha8jN
z{OKLl_?A4aE#&()6noD8SYvyqXx@1m;m>htCCN#Vv3|Ol>XqweKi3bf6!bWy{uw$x
z4u>ZPn|qI=J9fE#TL+Vt7x~kw%$Hk;!L#eH4++<}KW$+;CwPI_IF2IayC^oK0?IpF
zs|m5k*RlF^p~hiP*E_MVaiyUs?Z{2+1yvU~7-diKC%A(Zs_^R~_qsYwl@t1IT!h?&j@i2n9m9*1NFP6N
z(pKKw?qRiCBPq(#
z!!2gJEirTnL^oif)asZ+2^LYvLH_KTw|XD7ecTU>D2!;Kh6X(n0FHPySKIm7YcO-=)8T1A}`i
z%4d;l4NwJ(n$SDTSWBZhMV`7ZntX3MUfg_T!8IkIPuRyYX5*RmgVwXRWqXCRW(_Tb
zY)|B1L#qUDrhUnrla=&SXcyix1QphpKec~~)!T5L%bb0@kEOp6xPLnBYo|3GoUCr8
zYci~E31{Fjt-bw=Nq19U^_yD5a+>Z$IK@5z|~_e;V?H6|VV@Hh@?Ul+#Bf*zb|
z|H->AZ_8YS9dq3Pk2Q{vHKLluG=zdj)kHIKd-d;2s~!9-4UxcJeaaun%zCr
zuX7^?ZI*58W)AX9M>dm}ULy>UOJ7oF+HhHW5v&biWeb;&vhQ8$cRkRQ%q;o1+tSy;VqFd)7*4Hh(?XL`lzH>rLYAwAqJLE8m<+@^+E0|Xdorm8%&+b-Q7=X2ObLf$bX)8
z;=P8)ra(}Z99ePugvriucj?i|<^3s&);mak^LY?#r~Tj=%jY$j;{0vou+0J!lsp4@>rLAtd~6y
zmKv~zCka47tO%LI=@O&E+{f79Q(cO?PK*+Mu3E1ouGmMmc6=g%zF>T0xZ&5HLDO`M
zf_tlRnSh}ln~vB8|H>Ts9^w(b353m~DxV#C7&~|UOvF4T6w_48W*usKC8Wv}Ej8kV
z2)jQa&3At5cLE5m+_*i)O-X=^O64XR_3U@-X}-)Tk6wj_50LqA3R?2RpfVDpIyD`P
zg!rEKsWLRB7lTO^S1v1J{w)c2dP&WimjtZq?rmF}A)9_kHF!6Q6Az`ID|08cmp$*P
zL|6#dlUQ#lCBh^|s&%;Ic!|q-_vq-b
zY_PU1XW8rcEbx$bX!N12nCbAwkjxRfe6k34<(I>sdH7l!@<1-Wg`z){;|T3
zeZvda)ks)Oky3f~+n#9q4{boaQl3Scc&)y4T%+@W|teMmjar1t!O*uan
z{de~7;f!q0IJE*IwoUo0m%kdHBi^*qw=O#oy`(`t|?xX`>%(>Us+
z*_{X6J^Q3gZCOj6X{4VHJ*H3_;E0i;*t4K^V@$El5UkZW{o
z4AF&Qrwg{r1X~fA0kVI|Sj*gLvgftwV;YaN(YQF+WcUH@Z;^C(q
z0cO99M+xGD@)Fawv05u*&4I#?Hd&Mrq?{lCU4#ul4G=}dNdzSSKOEXhXj9cYw=C_o
z-^b0dAyNfQdKyzH*|^4eN&c50+dJn!|5dBdj5FuJJ0SG-oHW_Z9#@#F5@91tsWh@<
z`qnN>hWW>^3G!!1nFuEkUJ&793}~`<1va^yMFMNF}fcbWeJclW renderMapToPng(
) async {
final canvasRecorder = PictureRecorder();
final canvas = Canvas(canvasRecorder);
- component.tileMap.render(canvas);
+ component.tileMap.renderTree(canvas);
final picture = canvasRecorder.endRecording();
final size = component.size;
diff --git a/packages/flame_tiled/test/tiled_test.dart b/packages/flame_tiled/test/tiled_test.dart
index b82530a5784..dd980d4d52f 100644
--- a/packages/flame_tiled/test/tiled_test.dart
+++ b/packages/flame_tiled/test/tiled_test.dart
@@ -5,15 +5,20 @@ import 'package:flame/components.dart';
import 'package:flame/extensions.dart';
import 'package:flame/flame.dart';
import 'package:flame/game.dart';
+import 'package:flame_test/flame_test.dart';
import 'package:flame_tiled/flame_tiled.dart';
import 'package:flame_tiled/src/renderable_layers/tile_layers/tile_layer.dart';
import 'package:flutter/services.dart';
-import 'package:flutter_test/flutter_test.dart';
+import 'package:flutter_test/flutter_test.dart'
+ hide expect, group, setUp, isInstanceOf, expectLater;
+import 'package:test/test.dart';
import 'test_asset_bundle.dart';
import 'test_image_utils.dart';
void main() {
+ TestWidgetsFlutterBinding.ensureInitialized();
+
/// This represents the byte count of one pixel.
///
/// Usually, Color is represented as [Uint8List] and Uint8 has the ability to
@@ -23,42 +28,50 @@ void main() {
/// RGBA [255, 0, 0 255] => red,
/// RGBA [255, 255, 0 255] => Yellow.
const pixel = 4;
- TestWidgetsFlutterBinding.ensureInitialized();
- final game = FlameGame();
-
- setUp(TiledAtlas.atlasMap.clear);
group('TiledComponent', () {
late TiledComponent tiled;
- setUp(() async {
+ Future setUp(FlameGame game) async {
Flame.bundle = TestAssetBundle(
imageNames: ['map-level1.png', 'image1.png'],
stringNames: ['map.tmx', 'tiles_custom_path/map_custom_path.tmx'],
);
- tiled = await TiledComponent.load('map.tmx', Vector2.all(16));
- });
+ tiled = await TiledComponent.load(
+ 'map.tmx',
+ Vector2.all(16),
+ images: Images(bundle: Flame.bundle),
+ );
+ }
- test('correct loads the file', () {
+ testWithFlameGame('correct loads the file', (game) async {
+ await setUp(game);
expect(tiled.tileMap.renderableLayers.length, equals(4));
});
- test('component atlases returns the loaded atlases', () {
+ testWithFlameGame('component atlases returns the loaded atlases',
+ (game) async {
+ await setUp(game);
final atlases = tiled.atlases();
expect(atlases, hasLength(1));
expect(atlases.first.$1, equals('map-level1.png'));
});
- test('correct loads the file, with different prefix', () async {
+ testWithFlameGame('correct loads the file, with different prefix',
+ (game) async {
+ await setUp(game);
tiled = await TiledComponent.load(
'map_custom_path.tmx',
Vector2.all(16),
prefix: 'assets/tiles/tiles_custom_path/',
+ images: Images(bundle: Flame.bundle),
);
expect(tiled.tileMap.renderableLayers.length, equals(3));
});
group('is positionable', () {
- test('size, width, and height are readable - not writable', () {
+ testWithFlameGame('size, width, and height are readable - not writable',
+ (game) async {
+ await setUp(game);
expect(tiled.size, Vector2(512.0, 2048.0));
expect(tiled.width, 512);
expect(tiled.height, 2048);
@@ -71,7 +84,7 @@ void main() {
expect(tiled.size, Vector2(512.0, 2048.0));
});
- test('from constructor', () {
+ testWithFlameGame('from constructor', (game) async {
final map = TiledComponent(
tiled.tileMap,
position: Vector2(10, 20),
@@ -92,7 +105,7 @@ void main() {
});
});
- test('correctly loads external tileset', () async {
+ testWithFlameGame('correctly loads external tileset', (game) async {
// Flame.bundle is a global static. Updating these in tests can lead to
// odd errors if you're trying to debug.
Flame.bundle = TestAssetBundle(
@@ -120,7 +133,8 @@ void main() {
);
});
- test('correctly loads external tileset with custom path', () async {
+ testWithFlameGame('correctly loads external tileset with custom path',
+ (game) async {
// Flame.bundle is a global static. Updating these in tests can lead to
// odd errors if you're trying to debug.
Flame.bundle = TestAssetBundle(
@@ -156,7 +170,8 @@ void main() {
group('Layered tiles render correctly with layered sprite batch', () {
late Uint8List canvasPixelData;
late RenderableTiledMap overlapMap;
- setUp(() async {
+
+ Future setUp(FlameGame game) async {
final bundle = TestAssetBundle(
imageNames: [
'green_sprite.png',
@@ -171,31 +186,34 @@ void main() {
images: Images(bundle: bundle),
);
await game.add(overlapMap);
+ await game.ready();
final canvasRecorder = PictureRecorder();
final canvas = Canvas(canvasRecorder);
- overlapMap.render(canvas);
+ overlapMap.renderTree(canvas);
final picture = canvasRecorder.endRecording();
final image = await picture.toImageSafe(32, 16);
final bytes = await image.toByteData();
canvasPixelData = bytes!.buffer.asUint8List();
- });
+ }
- test(
- 'Correctly loads batches list',
- () => expect(overlapMap.renderableLayers.length == 2, true),
- );
+ testWithFlameGame('Correctly loads batches list', (game) async {
+ await setUp(game);
+ expect(overlapMap.renderableLayers.length == 2, true);
+ });
- test(
- 'Canvas pixel dimensions match',
- () => expect(
+ testWithFlameGame('Canvas pixel dimensions match', (game) async {
+ await setUp(game);
+ expect(
canvasPixelData.length == 16 * 32 * pixel,
true,
- ),
- );
+ );
+ });
+
+ testWithFlameGame('Base test - right tile pixel is red', (game) async {
+ await setUp(game);
- test('Base test - right tile pixel is red', () {
expect(
canvasPixelData[16 * pixel] == 255 &&
canvasPixelData[(16 * pixel) + 1] == 0 &&
@@ -218,7 +236,9 @@ void main() {
expect(allRed, true);
});
- test('Left tile pixel is green', () {
+ testWithFlameGame('Left tile pixel is green', (game) async {
+ await setUp(game);
+
expect(
canvasPixelData[15 * pixel] == 0 &&
canvasPixelData[(15 * pixel) + 1] == 255 &&
@@ -251,7 +271,7 @@ void main() {
Future renderMap() async {
final canvasRecorder = PictureRecorder();
final canvas = Canvas(canvasRecorder);
- overlapMap.render(canvas);
+ overlapMap.renderTree(canvas);
final picture = canvasRecorder.endRecording();
final image = await picture.toImageSafe(64, 32);
@@ -259,7 +279,7 @@ void main() {
return bytes!.buffer.asUint8List();
}
- setUp(() async {
+ Future setUp(FlameGame game) async {
final bundle = TestAssetBundle(
imageNames: [
'4_color_sprite.png',
@@ -272,13 +292,18 @@ void main() {
bundle: bundle,
images: Images(bundle: bundle),
);
-
+ await game.add(overlapMap);
+ await game.ready();
pixelsBeforeFlipApplied = await renderMap();
await Flame.images.ready();
pixelsAfterFlipApplied = await renderMap();
- });
+ }
+
+ testWithFlameGame(
+ '[useAtlas = true] Green tile pixels are in correct spots',
+ (game) async {
+ await setUp(game);
- test('[useAtlas = true] Green tile pixels are in correct spots', () {
const oneColorRect = 8;
final leftTilePixels = [];
for (var i = 65 * oneColorRect * pixel;
@@ -314,7 +339,11 @@ void main() {
expect(allGreen, true);
});
- test('[useAtlas = false] Green tile pixels are in correct spots', () {
+ testWithFlameGame(
+ '[useAtlas = false] Green tile pixels are in correct spots',
+ (game) async {
+ await setUp(game);
+
final leftTilePixels = [];
for (var i = 65 * 8 * pixel;
i < ((64 * 23) + (8 * 3)) * pixel;
@@ -381,13 +410,13 @@ void main() {
rendered = await renderMapToPng(tiledComponent);
}
- test('flip works with [ignoreFlip = false]', () async {
+ testWithFlameGame('flip works with [ignoreFlip = false]', (game) async {
await prepareForGolden(ignoreFlip: false);
expect(texture, matchesGoldenFile('goldens/texture_with_flip.png'));
expect(rendered, matchesGoldenFile('goldens/rendered_with_flip.png'));
});
- test('flip ignored with [ignoreFlip = true]', () async {
+ testWithFlameGame('flip ignored with [ignoreFlip = true]', (game) async {
await prepareForGolden(ignoreFlip: true);
expect(
texture,
@@ -402,7 +431,7 @@ void main() {
group('Test getLayer:', () {
late RenderableTiledMap renderableTiledMap;
- setUp(() async {
+ Future setUp(FlameGame game) async {
Flame.bundle = TestAssetBundle(
imageNames: ['map-level1.png'],
stringNames: ['layers_test.tmx'],
@@ -411,38 +440,44 @@ void main() {
'layers_test.tmx',
Vector2.all(32),
bundle: Flame.bundle,
+ images: Images(bundle: Flame.bundle),
);
- });
+ }
- test('Get Tile Layer', () {
+ testWithFlameGame('Get Tile Layer', (game) async {
+ await setUp(game);
expect(
renderableTiledMap.getLayer('MyTileLayer'),
isNotNull,
);
});
- test('Get Object Layer', () {
+ testWithFlameGame('Get Object Layer', (game) async {
+ await setUp(game);
expect(
renderableTiledMap.getLayer('MyObjectLayer'),
isNotNull,
);
});
- test('Get Image Layer', () {
+ testWithFlameGame('Get Image Layer', (game) async {
+ await setUp(game);
expect(
renderableTiledMap.getLayer('MyImageLayer'),
isNotNull,
);
});
- test('Get Group Layer', () {
+ testWithFlameGame('Get Group Layer', (game) async {
+ await setUp(game);
expect(
renderableTiledMap.getLayer('MyGroupLayer'),
isNotNull,
);
});
- test('Get no layer', () {
+ testWithFlameGame('Get no layer', (game) async {
+ await setUp(game);
expect(
renderableTiledMap.getLayer('Nonexistent layer'),
isNull,
@@ -454,7 +489,7 @@ void main() {
late TiledComponent component;
final mapSizePx = Vector2(32 * 16, 128 * 16);
- setUp(() async {
+ Future setUp(FlameGame game) async {
Flame.bundle = TestAssetBundle(
imageNames: [
'image1.png',
@@ -462,36 +497,36 @@ void main() {
],
stringNames: ['map.tmx'],
);
+
component = await TiledComponent.load(
'map.tmx',
Vector2(16, 16),
bundle: Flame.bundle,
+ images: Images(bundle: Flame.bundle),
);
// Need to initialize a game and call `onLoad` and `onGameResize` to
// get the camera and canvas sizes all initialized
- final game = FlameGame();
game.onGameResize(mapSizePx);
final camera = game.camera;
- game.world.add(component);
+ await game.world.add(component);
camera.viewfinder.position = Vector2(150, 20);
camera.viewport.size = mapSizePx.clone();
game.onGameResize(mapSizePx);
- component.onGameResize(mapSizePx);
- await component.onLoad();
await game.ready();
- });
+ }
- test('component size', () {
+ testWithFlameGame('component size', (game) async {
+ await setUp(game);
expect(component.tileMap.destTileSize, Vector2(16, 16));
expect(component.size, mapSizePx);
});
- test(
+ testWithFlameGame(
'renders',
- () async {
+ (game) async {
+ await setUp(game);
final pngData = await renderMapToPng(component);
-
expect(pngData, matchesGoldenFile('goldens/orthogonal.png'));
},
);
@@ -500,27 +535,30 @@ void main() {
group('isometric', () {
late TiledComponent component;
- setUp(() async {
+ Future setUp(FlameGame game) async {
final bundle = TestAssetBundle(
imageNames: [
'isometric_spritesheet.png',
],
stringNames: ['test_isometric.tmx'],
);
+
component = await TiledComponent.load(
'test_isometric.tmx',
Vector2(256 / 4, 128 / 4),
bundle: bundle,
images: Images(bundle: bundle),
);
- });
+ }
- test('component size', () {
+ testWithFlameGame('component size', (game) async {
+ await setUp(game);
expect(component.tileMap.destTileSize, Vector2(64, 32));
expect(component.size, Vector2(320, 160));
});
- test('renders', () async {
+ testWithFlameGame('renders', (game) async {
+ await setUp(game);
// Map size is now 320 wide, but it has 1 extra tile of height because
// its actually double-height tiles.
final pngData = await renderMapToPng(component);
@@ -551,7 +589,7 @@ void main() {
);
}
- test('flat + even staggered', () async {
+ testWithFlameGame('flat + even staggered', (game) async {
await setupMap(
'flat_hex_even.tmx',
'Tileset_Hexagonal_FlatTop_60x39_60x60.png',
@@ -565,7 +603,7 @@ void main() {
expect(pngData, matchesGoldenFile('goldens/flat_hex_even.png'));
});
- test('flat + odd staggered', () async {
+ testWithFlameGame('flat + odd staggered', (game) async {
await setupMap(
'flat_hex_odd.tmx',
'Tileset_Hexagonal_FlatTop_60x39_60x60.png',
@@ -579,7 +617,7 @@ void main() {
expect(pngData, matchesGoldenFile('goldens/flat_hex_odd.png'));
});
- test('pointy + even staggered', () async {
+ testWithFlameGame('pointy + even staggered', (game) async {
await setupMap(
'pointy_hex_even.tmx',
'Tileset_Hexagonal_PointyTop_60x52_60x80.png',
@@ -593,7 +631,7 @@ void main() {
expect(pngData, matchesGoldenFile('goldens/pointy_hex_even.png'));
});
- test('pointy + odd staggered', () async {
+ testWithFlameGame('pointy + odd staggered', (game) async {
await setupMap(
'pointy_hex_odd.tmx',
'Tileset_Hexagonal_PointyTop_60x52_60x80.png',
@@ -630,7 +668,7 @@ void main() {
);
}
- test('tile offset hexagonal', () async {
+ testWithFlameGame('tile offset hexagonal', (game) async {
await setupMap(
// flame tiled currently does not support hexagon side length property,
// to use export from Tiled, tweak that value
@@ -649,7 +687,7 @@ void main() {
);
});
- test('tile offset isometric', () async {
+ testWithFlameGame('tile offset isometric', (game) async {
await setupMap(
'test_tile_offset_isometric.tmx',
'4_color_sprite.png',
@@ -666,7 +704,7 @@ void main() {
);
});
- test('tile offset orthogonal', () async {
+ testWithFlameGame('tile offset orthogonal', (game) async {
await setupMap(
'test_tile_offset_orthogonal.tmx',
'4_color_sprite.png',
@@ -683,7 +721,7 @@ void main() {
);
});
- test('tile offset staggered', () async {
+ testWithFlameGame('tile offset staggered', (game) async {
await setupMap(
'test_tile_offset_staggered.tmx',
'4_color_sprite.png',
@@ -723,7 +761,7 @@ void main() {
);
}
- test('x + odd', () async {
+ testWithFlameGame('x + odd', (game) async {
await setupMap(
'iso_staggered_overlap_x_odd.tmx',
'dirt_atlas.png',
@@ -740,7 +778,7 @@ void main() {
);
});
- test('x + even + half sized', () async {
+ testWithFlameGame('x + even + half sized', (game) async {
await setupMap(
'iso_staggered_overlap_x_even.tmx',
'dirt_atlas.png',
@@ -757,7 +795,7 @@ void main() {
);
});
- test('y + odd + half', () async {
+ testWithFlameGame('y + odd + half', (game) async {
await setupMap(
'iso_staggered_overlap_y_odd.tmx',
'dirt_atlas.png',
@@ -774,7 +812,7 @@ void main() {
);
});
- test('y + even', () async {
+ testWithFlameGame('y + even', (game) async {
await setupMap(
'iso_staggered_overlap_y_even.tmx',
'dirt_atlas.png',
@@ -813,7 +851,7 @@ void main() {
);
}
- test('regular', () async {
+ testWithFlameGame('regular', (game) async {
await setupMap(size);
final pngData = await renderMapToPng(component);
@@ -823,7 +861,7 @@ void main() {
);
});
- test('smaller', () async {
+ testWithFlameGame('smaller', (game) async {
final smallSize = size / 3;
await setupMap(smallSize);
final pngData = await renderMapToPng(component);
@@ -834,7 +872,7 @@ void main() {
);
});
- test('larger', () async {
+ testWithFlameGame('larger', (game) async {
final largeSize = size * 2;
await setupMap(largeSize);
final pngData = await renderMapToPng(component);
@@ -850,7 +888,7 @@ void main() {
late TiledComponent component;
final size = Vector2(256 / 2, 128 / 2);
- setUp(() async {
+ Future setUp(FlameGame game) async {
final bundle = TestAssetBundle(
imageNames: [
'isometric_spritesheet.png',
@@ -863,8 +901,12 @@ void main() {
bundle: bundle,
images: Images(bundle: bundle),
);
- });
- test('from all layers', () {
+ await game.add(component);
+ await game.ready();
+ }
+
+ testWithFlameGame('from all layers', (game) async {
+ await setUp(game);
var stack = component.tileMap.tileStack(0, 0, all: true);
expect(stack.length, 2);
@@ -872,7 +914,8 @@ void main() {
expect(stack.length, 1);
});
- test('from some layers', () {
+ testWithFlameGame('from some layers', (game) async {
+ await setUp(game);
var stack = component.tileMap.tileStack(0, 0, named: {'empty'});
expect(stack.length, 0);
@@ -886,7 +929,9 @@ void main() {
expect(stack.length, 2);
});
- test('can be positioned together', () async {
+ testWithFlameGame('can be positioned together', (game) async {
+ await setUp(game);
+
final stack = component.tileMap.tileStack(0, 0, all: true);
stack.position = stack.position + Vector2.all(20);
@@ -897,7 +942,9 @@ void main() {
);
});
- test('can be positioned singularly', () async {
+ testWithFlameGame('can be positioned singularly', (game) async {
+ await setUp(game);
+
final stack = component.tileMap.tileStack(0, 0, named: {'item'});
stack.position = stack.position + Vector2(-20, 20);
@@ -921,7 +968,7 @@ void main() {
'staggered',
]) {
group(mapType, () {
- setUp(() async {
+ Future setUp(FlameGame game) async {
final bundle = TestAssetBundle(
imageNames: [
'0x72_DungeonTilesetII_v1.4.png',
@@ -935,10 +982,15 @@ void main() {
images: Images(bundle: bundle),
);
map = component.tileMap;
- });
+ await game.add(component);
+ await game.ready();
+ }
+
+ testWithFlameGame('handle single frame animations ($mapType)',
+ (game) async {
+ await setUp(game);
- test('handle single frame animations ($mapType)', () {
- expect(map.renderableLayers.first, isInstanceOf());
+ expect(map.renderableLayers.first is FlameTileLayer, true);
final layer = map.renderableLayers.first as FlameTileLayer;
expect(
layer.animations,
@@ -953,10 +1005,13 @@ void main() {
expect(layer.animations.first.frames.sources, hasLength(1));
});
- test('handle single frame animations ($mapType)', () {
+ testWithFlameGame('handle single frame animations ($mapType)',
+ (game) async {
+ await setUp(game);
+
expect(
- map.renderableLayers[1],
- isInstanceOf(),
+ map.renderableLayers[1] is FlameTileLayer,
+ true,
);
final layer = map.renderableLayers[1] as FlameTileLayer;
expect(
@@ -975,7 +1030,8 @@ void main() {
expect(waterAnimation.frames.durations, [0.18, 0.17, 0.15]);
expect(spikeAnimation.frames.durations, [0.176, 0.176, 0.176, 0.176]);
- map.update(0.177);
+ game.update(0.177);
+
expect(waterAnimation.frame, 0);
expect(waterAnimation.frames.frameTime, 0.177);
expect(
@@ -990,13 +1046,13 @@ void main() {
spikeAnimation.frames.sources[1],
);
- map.update(0.003);
+ game.update(0.003);
expect(waterAnimation.frame, 1);
expect(waterAnimation.frames.frameTime, moreOrLessEquals(0.0));
expect(spikeAnimation.frame, 1);
expect(spikeAnimation.frames.frameTime, moreOrLessEquals(0.004));
- map.update(0.17 + 0.15);
+ game.update(0.17 + 0.15);
expect(waterAnimation.frame, 0, reason: 'wraps around');
expect(
waterAnimation.batchedSource.toRect(),
@@ -1006,7 +1062,9 @@ void main() {
/// This will not produce a pretty map for non-orthogonal, but that's
/// OK, we're looking for parsing and handling of animations.
- test('renders ($mapType)', () async {
+ testWithFlameGame('renders ($mapType)', (game) async {
+ await setUp(game);
+
var pngData = await renderMapToPng(component);
await expectLater(
pngData,
@@ -1049,7 +1107,7 @@ void main() {
'staggered',
]) {
group(mapType, () {
- setUp(() async {
+ Future setUp(game) async {
final bundle = TestAssetBundle(
imageNames: [
'0x72_DungeonTilesetII_v1.4.png',
@@ -1062,9 +1120,10 @@ void main() {
bundle: bundle,
images: Images(bundle: bundle),
);
- });
+ }
- test('renders ($mapType)', () async {
+ testWithFlameGame('renders ($mapType)', (game) async {
+ await setUp(game);
final pngData = await renderMapToPng(component);
await expectLater(
pngData,
From 2d36b26be7a4a5841a9eb3236a05e1541061f688 Mon Sep 17 00:00:00 2001
From: TheMaverickProgrammer
<91709+TheMaverickProgrammer@users.noreply.github.com>
Date: Sun, 27 Jul 2025 09:24:31 -0500
Subject: [PATCH 11/28] camera needs to be passed into the child components in
new version of tiled map component.
---
packages/flame_tiled/lib/src/tiled_component.dart | 2 ++
1 file changed, 2 insertions(+)
diff --git a/packages/flame_tiled/lib/src/tiled_component.dart b/packages/flame_tiled/lib/src/tiled_component.dart
index 358c51d04e5..102ce56cb6d 100644
--- a/packages/flame_tiled/lib/src/tiled_component.dart
+++ b/packages/flame_tiled/lib/src/tiled_component.dart
@@ -96,6 +96,7 @@ class TiledComponent extends PositionComponent
int? priority,
bool? ignoreFlip,
AssetBundle? bundle,
+ CameraComponent? camera,
Images? images,
bool Function(Tileset)? tsxPackingFilter,
bool useAtlas = true,
@@ -112,6 +113,7 @@ class TiledComponent extends PositionComponent
ignoreFlip: ignoreFlip,
prefix: prefix,
bundle: bundle,
+ camera: camera,
images: images,
tsxPackingFilter: tsxPackingFilter,
useAtlas: useAtlas,
From 63f2d43ec65801b23efeda8432bb180360ed259d Mon Sep 17 00:00:00 2001
From: TheMaverickProgrammer
<91709+TheMaverickProgrammer@users.noreply.github.com>
Date: Tue, 29 Jul 2025 19:47:27 -0500
Subject: [PATCH 12/28] image layer repeat draw behavior now accurate AND
optimized
---
packages/flame_tiled/example/lib/main.dart | 2 +-
.../src/renderable_layers/group_layer.dart | 1 -
.../src/renderable_layers/image_layer.dart | 99 +++++++++++++++----
.../renderable_layers/renderable_layer.dart | 14 ++-
.../tile_layers/tile_layer.dart | 6 +-
packages/flame_tiled/test/tiled_test.dart | 11 +--
6 files changed, 102 insertions(+), 31 deletions(-)
diff --git a/packages/flame_tiled/example/lib/main.dart b/packages/flame_tiled/example/lib/main.dart
index 4dbcd24452b..44c34e29beb 100644
--- a/packages/flame_tiled/example/lib/main.dart
+++ b/packages/flame_tiled/example/lib/main.dart
@@ -37,7 +37,7 @@ class TiledGame extends FlameGame {
);
mapComponent = await TiledComponent.load('map.tmx', Vector2.all(16));
- world.add(mapComponent);
+ await world.add(mapComponent);
final objectGroup =
mapComponent.tileMap.getLayer('AnimatedCoins');
diff --git a/packages/flame_tiled/lib/src/renderable_layers/group_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/group_layer.dart
index 24a0b7326b3..026de974687 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/group_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/group_layer.dart
@@ -1,4 +1,3 @@
-import 'package:flame/extensions.dart';
import 'package:flame_tiled/src/renderable_layers/renderable_layer.dart';
import 'package:meta/meta.dart';
import 'package:tiled/tiled.dart';
diff --git a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
index 34bd1fe4617..a8f42a84a4b 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
@@ -8,6 +8,7 @@ import 'package:flame_tiled/src/renderable_layers/renderable_layer.dart';
import 'package:flutter/rendering.dart';
import 'package:meta/meta.dart';
import 'package:tiled/tiled.dart';
+import 'dart:math';
@internal
class FlameImageLayer extends RenderableLayer {
@@ -38,11 +39,7 @@ class FlameImageLayer extends RenderableLayer {
@override
void render(Canvas canvas) {
canvas.save();
-
- canvas.translate(offsetX, offsetY);
- //applyParallaxOffset(canvas);
_resizePaintArea(camera);
-
paintImage(
canvas: canvas,
rect: _paintArea,
@@ -60,37 +57,66 @@ class FlameImageLayer extends RenderableLayer {
// Track the maximum amount the canvas could have been translated
// for this layer so we can calculate how many extra images to draw
if (camera != null) {
- _maxTranslation.x =
- offsetX.abs() + camera.viewfinder.position.x.abs() * parallaxX;
- _maxTranslation.y =
- offsetY.abs() + camera.viewfinder.position.y.abs() * parallaxY;
+ _maxTranslation.x = offsetX - camera.viewfinder.position.x * parallaxX;
+ _maxTranslation.y = offsetY - camera.viewfinder.position.y * parallaxY;
} else {
- _maxTranslation.x = offsetX.abs();
- _maxTranslation.y = offsetY.abs();
+ _maxTranslation.x = offsetX;
+ _maxTranslation.y = offsetY;
}
+ final virtualSize = camera?.viewport.virtualSize;
+ final destSize = virtualSize ?? _canvasSize;
+ final imageW = _image.size.x;
+ final imageH = _image.size.y;
+
// When the image is being repeated, make sure the _paintArea rect is
// big enough that it repeats off the edge of the canvas in both positive
// and negative directions on that axis (Tiled repeats forever on an axis).
// Also, make sure the rect's left and top are only moved by exactly the
// image's length along that axis (width or height) so that with repeats
// it still matches up with its initial layer offsets.
-
if (_repeat == ImageRepeat.repeatX || _repeat == ImageRepeat.repeat) {
- final xImages = (_maxTranslation.x / _image.size.x).ceil();
- _paintArea.left = -_image.size.x * xImages;
- _paintArea.right = _canvasSize.x + _image.size.x * xImages;
+ final (left, right) = _calculatePaintRange(
+ translation: _maxTranslation.x,
+ destSize: destSize.x,
+ imageSideLen: imageW,
+ );
+ _paintArea
+ ..left = left
+ ..right = right;
+
+ // The canvas will already be shifted by the parent component's
+ // render step. Account for this offset to match expectations while
+ // also respect camera bounds, if any. This prevents scaling the
+ // painted image down when the window resizes to small values.
+ final worldRect = camera?.viewfinder.visibleWorldRect ?? Rect.zero;
+ _paintArea.left += worldRect.left - super.cachedLayerOffset.x;
+ _paintArea.right += worldRect.right - super.cachedLayerOffset.x;
} else {
+ // Simply draw the full width of the image.
_paintArea.left = 0;
- _paintArea.right = _canvasSize.x;
+ _paintArea.right = imageW;
}
if (_repeat == ImageRepeat.repeatY || _repeat == ImageRepeat.repeat) {
- final yImages = (_maxTranslation.y / _image.size.y).ceil();
- _paintArea.top = -_image.size.y * yImages;
- _paintArea.bottom = _canvasSize.y + _image.size.y * yImages;
+ final (top, bottom) = _calculatePaintRange(
+ translation: _maxTranslation.y,
+ destSize: destSize.y,
+ imageSideLen: imageH,
+ );
+ _paintArea
+ ..top = top
+ ..bottom = bottom;
+ // The canvas will already be shifted by the parent component's
+ // render step. Account for this offset to match expectations while
+ // also respect camera bounds, if any. This prevents scaling the
+ // painted image down when the window resizes to small values.
+ final worldRect = camera?.viewfinder.visibleWorldRect ?? Rect.zero;
+ _paintArea.top += worldRect.top - super.cachedLayerOffset.y;
+ _paintArea.bottom += worldRect.bottom - super.cachedLayerOffset.y;
} else {
+ // Simply draw the full height of the image.
_paintArea.top = 0;
- _paintArea.bottom = _canvasSize.y;
+ _paintArea.bottom = imageH;
}
}
@@ -106,6 +132,41 @@ class FlameImageLayer extends RenderableLayer {
}
}
+ // As an optimization, the [_paintArea] rect can be positioned in a
+ // particular way that reduces the time spent on computation and clip steps
+ // in flutter when drawing infinitely across an axis. This method accounts
+ // for the destination canvas size, camera viewport size, and the exact
+ // coverage of the image w.r.t. [translation].
+ // This is achieved by wrapping the rect coordinates around [destSize]
+ // after calculating the image coverage with [imageSideLen] and adding the
+ // unseen portion of the image in the span of the wrap range, if any.
+ (double min, double max) _calculatePaintRange({
+ required double translation,
+ required double destSize,
+ required double imageSideLen,
+ }) {
+ // What portion of the image is seen.
+ final seen = destSize / imageSideLen;
+
+ // Integer count of whole images to draw.
+ final imageCount = seen.ceil();
+
+ // Calculate unseen part of image(s).
+ final unseen = (imageCount - seen) * imageSideLen;
+
+ // Wrap around the target axis w.r.t. parallax.
+ final wrapPoint = translation.ceil() % (destSize + unseen).toInt();
+
+ // Partition the _paintArea into two parts.
+ final part = (wrapPoint / imageSideLen).ceil();
+
+ // Return the range where the centroid is the wrap point.
+ return (
+ wrapPoint - (part * imageSideLen),
+ wrapPoint + (imageSideLen * (imageCount - part)),
+ );
+ }
+
static Future load({
required ImageLayer layer,
required GroupLayer? parent,
diff --git a/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
index 2a97e14b775..727f569402d 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
@@ -22,6 +22,11 @@ abstract class RenderableLayer extends PositionComponent
/// The [FilterQuality] that should be used by all the layers.
final FilterQuality filterQuality;
+ /// Cached canvas translation used in parallax effects.
+ /// This field needs to be read in the case of repeated textures
+ /// as part of an optimization.
+ Vector2 cachedLayerOffset = Vector2.zero();
+
RenderableLayer({
required this.layer,
required Component? parent,
@@ -147,8 +152,8 @@ abstract class RenderableLayer extends PositionComponent
final anchor = camera?.viewfinder.anchor ?? Anchor.center;
final cameraX = camera?.viewfinder.position.x ?? 0.0;
final cameraY = camera?.viewfinder.position.y ?? 0.0;
- final viewportCenterX = camera?.viewport.size.x ?? 0.0 * anchor.x;
- final viewportCenterY = camera?.viewport.size.y ?? 0.0 * anchor.y;
+ final viewportCenterX = (camera?.viewport.virtualSize.x ?? 0.0) * anchor.x;
+ final viewportCenterY = (camera?.viewport.virtualSize.y ?? 0.0) * anchor.y;
// Due to how Tiled treats the center of the view as the reference
// point for parallax positioning (see Tiled docs), we need to offset the
@@ -166,6 +171,11 @@ abstract class RenderableLayer extends PositionComponent
x += cameraX - (cameraX * parallaxX);
y += cameraY - (cameraY * parallaxY);
+ // Apply layer offset.
+ x += offsetX;
+ y += offsetY;
+
+ cachedLayerOffset = Vector2(x, y);
canvas.translate(x, y);
}
diff --git a/packages/flame_tiled/lib/src/renderable_layers/tile_layers/tile_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/tile_layers/tile_layer.dart
index 72154459bd7..8f55f57796a 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/tile_layers/tile_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/tile_layers/tile_layer.dart
@@ -31,7 +31,9 @@ import 'package:meta/meta.dart';
/// {@endtemplate}
@internal
abstract class FlameTileLayer extends RenderableLayer {
- late final _layerPaint = layerPaintFactory(opacity);
+ @override
+ late Paint paint = layerPaintFactory(opacity);
+
final TiledAtlas tiledAtlas;
late List> transforms;
final animations = [];
@@ -139,7 +141,7 @@ abstract class FlameTileLayer extends RenderableLayer {
canvas.save();
canvas.translate(offsetX, offsetY);
//applyParallaxOffset(canvas);
- tiledAtlas.batch!.render(canvas, paint: _layerPaint);
+ tiledAtlas.batch!.render(canvas, paint: paint);
canvas.restore();
}
diff --git a/packages/flame_tiled/test/tiled_test.dart b/packages/flame_tiled/test/tiled_test.dart
index dd980d4d52f..471824a1fad 100644
--- a/packages/flame_tiled/test/tiled_test.dart
+++ b/packages/flame_tiled/test/tiled_test.dart
@@ -498,18 +498,16 @@ void main() {
stringNames: ['map.tmx'],
);
+ final camera = game.camera;
component = await TiledComponent.load(
'map.tmx',
Vector2(16, 16),
bundle: Flame.bundle,
images: Images(bundle: Flame.bundle),
+ camera: camera,
);
- // Need to initialize a game and call `onLoad` and `onGameResize` to
- // get the camera and canvas sizes all initialized
- game.onGameResize(mapSizePx);
- final camera = game.camera;
- await game.world.add(component);
+ await game.world.ensureAdd(component);
camera.viewfinder.position = Vector2(150, 20);
camera.viewport.size = mapSizePx.clone();
game.onGameResize(mapSizePx);
@@ -980,9 +978,10 @@ void main() {
size,
bundle: bundle,
images: Images(bundle: bundle),
+ camera: game.camera,
);
map = component.tileMap;
- await game.add(component);
+ await game.ensureAdd(component);
await game.ready();
}
From 311256efdd43d08ec6f022b727eb7502c1bc5fe0 Mon Sep 17 00:00:00 2001
From: TheMaverickProgrammer
<91709+TheMaverickProgrammer@users.noreply.github.com>
Date: Tue, 29 Jul 2025 20:37:44 -0500
Subject: [PATCH 13/28] cleanup and prettify code
---
.../src/renderable_layers/image_layer.dart | 57 +++++++++----------
1 file changed, 28 insertions(+), 29 deletions(-)
diff --git a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
index a8f42a84a4b..a2789a2acbf 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
@@ -8,7 +8,6 @@ import 'package:flame_tiled/src/renderable_layers/renderable_layer.dart';
import 'package:flutter/rendering.dart';
import 'package:meta/meta.dart';
import 'package:tiled/tiled.dart';
-import 'dart:math';
@internal
class FlameImageLayer extends RenderableLayer {
@@ -64,8 +63,8 @@ class FlameImageLayer extends RenderableLayer {
_maxTranslation.y = offsetY;
}
- final virtualSize = camera?.viewport.virtualSize;
- final destSize = virtualSize ?? _canvasSize;
+ final visibleWorldRect = camera?.visibleWorldRect ?? Rect.zero;
+ final destSize = camera?.viewport.virtualSize ?? _canvasSize;
final imageW = _image.size.x;
final imageH = _image.size.y;
@@ -77,21 +76,16 @@ class FlameImageLayer extends RenderableLayer {
// it still matches up with its initial layer offsets.
if (_repeat == ImageRepeat.repeatX || _repeat == ImageRepeat.repeat) {
final (left, right) = _calculatePaintRange(
- translation: _maxTranslation.x,
- destSize: destSize.x,
- imageSideLen: imageW,
- );
+ translation: _maxTranslation.x,
+ destSize: destSize.x,
+ imageSideLen: imageW,
+ layerOffset: cachedLayerOffset.x,
+ ) + // Apply camera left/right to range.
+ (visibleWorldRect.left, visibleWorldRect.right);
+
_paintArea
..left = left
..right = right;
-
- // The canvas will already be shifted by the parent component's
- // render step. Account for this offset to match expectations while
- // also respect camera bounds, if any. This prevents scaling the
- // painted image down when the window resizes to small values.
- final worldRect = camera?.viewfinder.visibleWorldRect ?? Rect.zero;
- _paintArea.left += worldRect.left - super.cachedLayerOffset.x;
- _paintArea.right += worldRect.right - super.cachedLayerOffset.x;
} else {
// Simply draw the full width of the image.
_paintArea.left = 0;
@@ -99,20 +93,16 @@ class FlameImageLayer extends RenderableLayer {
}
if (_repeat == ImageRepeat.repeatY || _repeat == ImageRepeat.repeat) {
final (top, bottom) = _calculatePaintRange(
- translation: _maxTranslation.y,
- destSize: destSize.y,
- imageSideLen: imageH,
- );
+ translation: _maxTranslation.y,
+ destSize: destSize.y,
+ imageSideLen: imageH,
+ layerOffset: cachedLayerOffset.y,
+ ) + // Apply camera top/bottom to range.
+ (visibleWorldRect.top, visibleWorldRect.bottom);
+
_paintArea
..top = top
..bottom = bottom;
- // The canvas will already be shifted by the parent component's
- // render step. Account for this offset to match expectations while
- // also respect camera bounds, if any. This prevents scaling the
- // painted image down when the window resizes to small values.
- final worldRect = camera?.viewfinder.visibleWorldRect ?? Rect.zero;
- _paintArea.top += worldRect.top - super.cachedLayerOffset.y;
- _paintArea.bottom += worldRect.bottom - super.cachedLayerOffset.y;
} else {
// Simply draw the full height of the image.
_paintArea.top = 0;
@@ -140,10 +130,15 @@ class FlameImageLayer extends RenderableLayer {
// This is achieved by wrapping the rect coordinates around [destSize]
// after calculating the image coverage with [imageSideLen] and adding the
// unseen portion of the image in the span of the wrap range, if any.
+ //
+ // The return tuple value is the range where its centroid is the wrap point
+ // plus the [layerOffset] which shifts the range to account for earlier
+ // transformations applied to the canvas.
(double min, double max) _calculatePaintRange({
required double translation,
required double destSize,
required double imageSideLen,
+ required double layerOffset,
}) {
// What portion of the image is seen.
final seen = destSize / imageSideLen;
@@ -160,10 +155,9 @@ class FlameImageLayer extends RenderableLayer {
// Partition the _paintArea into two parts.
final part = (wrapPoint / imageSideLen).ceil();
- // Return the range where the centroid is the wrap point.
return (
- wrapPoint - (part * imageSideLen),
- wrapPoint + (imageSideLen * (imageCount - part)),
+ wrapPoint - (part * imageSideLen) - layerOffset,
+ wrapPoint + (imageSideLen * (imageCount - part)) - layerOffset,
);
}
@@ -190,3 +184,8 @@ class FlameImageLayer extends RenderableLayer {
@override
void refreshCache() {}
}
+
+extension _PrivRangeTupleHelper on (double, double) {
+ (double, double) operator +((double, double) other) =>
+ ($1 + other.$1, $2 + other.$2);
+}
From b913f32801e604b89e012afecc94daa8d11584b8 Mon Sep 17 00:00:00 2001
From: TheMaverickProgrammer
<91709+TheMaverickProgrammer@users.noreply.github.com>
Date: Wed, 30 Jul 2025 20:01:41 -0500
Subject: [PATCH 14/28] parallax math almost perfect with what Tiled shows.
WYSIWYG.
---
.../src/renderable_layers/image_layer.dart | 27 +++++++++++--------
1 file changed, 16 insertions(+), 11 deletions(-)
diff --git a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
index a2789a2acbf..e994071d7d4 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
@@ -53,21 +53,22 @@ class FlameImageLayer extends RenderableLayer {
}
void _resizePaintArea(CameraComponent? camera) {
- // Track the maximum amount the canvas could have been translated
- // for this layer so we can calculate how many extra images to draw
- if (camera != null) {
- _maxTranslation.x = offsetX - camera.viewfinder.position.x * parallaxX;
- _maxTranslation.y = offsetY - camera.viewfinder.position.y * parallaxY;
- } else {
- _maxTranslation.x = offsetX;
- _maxTranslation.y = offsetY;
- }
-
final visibleWorldRect = camera?.visibleWorldRect ?? Rect.zero;
final destSize = camera?.viewport.virtualSize ?? _canvasSize;
final imageW = _image.size.x;
final imageH = _image.size.y;
+ // Track the maximum amount the canvas could have been translated
+ // for this layer so we can calculate the wrap point within the
+ // paint area.
+ _maxTranslation.x = offsetX - (visibleWorldRect.left * parallaxX);
+ _maxTranslation.y = offsetY - (visibleWorldRect.top * parallaxY);
+
+ /*
+ _maxTranslation.x = offsetX - camera.viewfinder.position.x * parallaxX;
+ _maxTranslation.y = offsetY - camera.viewfinder.position.y * parallaxY;
+*/
+
// When the image is being repeated, make sure the _paintArea rect is
// big enough that it repeats off the edge of the canvas in both positive
// and negative directions on that axis (Tiled repeats forever on an axis).
@@ -140,6 +141,10 @@ class FlameImageLayer extends RenderableLayer {
required double imageSideLen,
required double layerOffset,
}) {
+ // Prevent DBZ error.
+ if (imageSideLen < 1) {
+ return (0, 0);
+ }
// What portion of the image is seen.
final seen = destSize / imageSideLen;
@@ -156,7 +161,7 @@ class FlameImageLayer extends RenderableLayer {
final part = (wrapPoint / imageSideLen).ceil();
return (
- wrapPoint - (part * imageSideLen) - layerOffset,
+ wrapPoint - (imageSideLen * part) - layerOffset,
wrapPoint + (imageSideLen * (imageCount - part)) - layerOffset,
);
}
From 21b53ff2f773cc722b2988c460b27c8a354b0a8c Mon Sep 17 00:00:00 2001
From: TheMaverickProgrammer
<91709+TheMaverickProgrammer@users.noreply.github.com>
Date: Wed, 13 Aug 2025 18:51:33 -0500
Subject: [PATCH 15/28] stable changes tested in other repos. commiting
checkpoint before tests.
---
.../src/renderable_layers/image_layer.dart | 7 ++--
.../renderable_layers/renderable_layer.dart | 32 +++++++------------
.../tile_layers/tile_layer.dart | 2 +-
.../lib/src/renderable_tile_map.dart | 2 +-
4 files changed, 19 insertions(+), 24 deletions(-)
diff --git a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
index e994071d7d4..d996707265f 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
@@ -61,8 +61,10 @@ class FlameImageLayer extends RenderableLayer {
// Track the maximum amount the canvas could have been translated
// for this layer so we can calculate the wrap point within the
// paint area.
- _maxTranslation.x = offsetX - (visibleWorldRect.left * parallaxX);
- _maxTranslation.y = offsetY - (visibleWorldRect.top * parallaxY);
+ _maxTranslation.x = cachedLayerOffset.x;
+ // offsetX; //- (visibleWorldRect.left * parallaxX);
+ _maxTranslation.y = cachedLayerOffset.y;
+ // offsetY; //- (visibleWorldRect.top * parallaxY);
/*
_maxTranslation.x = offsetX - camera.viewfinder.position.x * parallaxX;
@@ -190,6 +192,7 @@ class FlameImageLayer extends RenderableLayer {
void refreshCache() {}
}
+/// Provide tuples with addition.
extension _PrivRangeTupleHelper on (double, double) {
(double, double) operator +((double, double) other) =>
($1 + other.$1, $2 + other.$2);
diff --git a/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
index 727f569402d..e7126dfe778 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
@@ -154,29 +154,21 @@ abstract class RenderableLayer extends PositionComponent
final cameraY = camera?.viewfinder.position.y ?? 0.0;
final viewportCenterX = (camera?.viewport.virtualSize.x ?? 0.0) * anchor.x;
final viewportCenterY = (camera?.viewport.virtualSize.y ?? 0.0) * anchor.y;
+ final topLeftX = cameraX - viewportCenterX;
+ final topLeftY = cameraY - viewportCenterY;
- // Due to how Tiled treats the center of the view as the reference
- // point for parallax positioning (see Tiled docs), we need to offset the
- // entire layer
- var x = (1 - parallaxX) * viewportCenterX;
- var y = (1 - parallaxY) * viewportCenterY;
- // Compensate the offset for zoom.
- x /= camera?.viewfinder.zoom ?? 1.0;
- y /= camera?.viewfinder.zoom ?? 1.0;
- // Scale to tile space.
- x /= destTileSize.x;
- y /= destTileSize.y;
-
- // Now add the scroll for the current camera position
- x += cameraX - (cameraX * parallaxX);
- y += cameraY - (cameraY * parallaxY);
-
- // Apply layer offset.
- x += offsetX;
- y += offsetY;
+ final double initOffsetX = viewportCenterX * parallaxX;
+ final double initOffsetY = viewportCenterY * parallaxY;
+
+ // Use the complement of the parallax coefficient in order to account for
+ // the camera applying its transformations earlier in the render cycle
+ // of Flame. This adjustment draws the layers correctly w.r.t. their own
+ // offset and parallax values.
+ final double x = offsetX + (cameraX * (1.0 - parallaxX));
+ final double y = offsetY + (cameraY * (1.0 - parallaxY));
cachedLayerOffset = Vector2(x, y);
- canvas.translate(x, y);
+ canvas.translate(cachedLayerOffset.x, cachedLayerOffset.y);
}
// Only render if this layer is [visible].
diff --git a/packages/flame_tiled/lib/src/renderable_layers/tile_layers/tile_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/tile_layers/tile_layer.dart
index 8f55f57796a..0a5af95c3b7 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/tile_layers/tile_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/tile_layers/tile_layer.dart
@@ -139,7 +139,7 @@ abstract class FlameTileLayer extends RenderableLayer {
}
canvas.save();
- canvas.translate(offsetX, offsetY);
+ //canvas.translate(offsetX, offsetY);
//applyParallaxOffset(canvas);
tiledAtlas.batch!.render(canvas, paint: paint);
canvas.restore();
diff --git a/packages/flame_tiled/lib/src/renderable_tile_map.dart b/packages/flame_tiled/lib/src/renderable_tile_map.dart
index 03e299b177b..b028d21d1c2 100644
--- a/packages/flame_tiled/lib/src/renderable_tile_map.dart
+++ b/packages/flame_tiled/lib/src/renderable_tile_map.dart
@@ -417,7 +417,7 @@ class RenderableTiledMap extends Component
}
}
- /// Renders the background and then calls super.
+ /// Fills canvas with [_backgroundPaint] and then calls super.
@override
void render(Canvas c) {
if (_backgroundPaint != null) {
From 97c85db45eb8487c971eb385693d2de4fc59b908 Mon Sep 17 00:00:00 2001
From: TheMaverickProgrammer
<91709+TheMaverickProgrammer@users.noreply.github.com>
Date: Wed, 13 Aug 2025 22:33:44 -0500
Subject: [PATCH 16/28] fixed the animation tests. the only remaining test to
fix is test_shifted.tmx. Also, added an override for field 'paint' at the
RenderLayer class so that any layer can have their paint easily modified.
---
.../src/renderable_layers/group_layer.dart | 1 +
.../src/renderable_layers/image_layer.dart | 3 ++
.../src/renderable_layers/object_layer.dart | 3 ++
.../renderable_layers/renderable_layer.dart | 48 ++++++++++++-------
.../tile_layers/tile_layer.dart | 11 +----
.../tile_layers/unsupported_layer.dart | 1 +
packages/flame_tiled/test/tiled_test.dart | 10 ++--
7 files changed, 45 insertions(+), 32 deletions(-)
diff --git a/packages/flame_tiled/lib/src/renderable_layers/group_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/group_layer.dart
index 026de974687..25ad661428d 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/group_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/group_layer.dart
@@ -10,6 +10,7 @@ class GroupLayer extends RenderableLayer {
required super.camera,
required super.map,
required super.destTileSize,
+ required super.layerPaintFactory,
super.filterQuality,
});
diff --git a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
index d996707265f..1b41afb8203 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
@@ -24,6 +24,7 @@ class FlameImageLayer extends RenderableLayer {
required super.map,
required super.destTileSize,
required Image image,
+ required super.layerPaintFactory,
super.filterQuality,
}) : _image = image {
_initImageRepeat();
@@ -174,6 +175,7 @@ class FlameImageLayer extends RenderableLayer {
required CameraComponent? camera,
required TiledMap map,
required Vector2 destTileSize,
+ required Paint Function(double opacity) layerPaintFactory,
FilterQuality? filterQuality,
Images? images,
}) async {
@@ -184,6 +186,7 @@ class FlameImageLayer extends RenderableLayer {
map: map,
destTileSize: destTileSize,
filterQuality: filterQuality,
+ layerPaintFactory: layerPaintFactory,
image: await (images ?? Flame.images).load(layer.image.source!),
);
}
diff --git a/packages/flame_tiled/lib/src/renderable_layers/object_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/object_layer.dart
index 7516e5da5bd..7367e895773 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/object_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/object_layer.dart
@@ -13,6 +13,7 @@ class ObjectLayer extends RenderableLayer {
required super.camera,
required super.map,
required super.destTileSize,
+ required super.layerPaintFactory,
super.filterQuality,
});
@@ -22,6 +23,7 @@ class ObjectLayer extends RenderableLayer {
CameraComponent? camera,
TiledMap map,
Vector2 destTileSize,
+ Paint Function(double opacity) layerPaintFactory,
FilterQuality? filterQuality,
) async {
return ObjectLayer(
@@ -30,6 +32,7 @@ class ObjectLayer extends RenderableLayer {
camera: camera,
map: map,
destTileSize: destTileSize,
+ layerPaintFactory: layerPaintFactory,
filterQuality: filterQuality,
);
}
diff --git a/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
index e7126dfe778..5eb05df7e7b 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
@@ -24,15 +24,24 @@ abstract class RenderableLayer extends PositionComponent
/// Cached canvas translation used in parallax effects.
/// This field needs to be read in the case of repeated textures
- /// as part of an optimization.
+ /// as an optimization step.
Vector2 cachedLayerOffset = Vector2.zero();
+ /// Given the layer's [opacity], compute [Paint] for drawing.
+ /// This is useful if your layer requires translucency or other effects.
+ Paint Function(double opacity) layerPaintFactory;
+
+ /// A read on [paint] will compute the value from [layerPaintFactory].
+ @override
+ Paint get paint => layerPaintFactory(opacity);
+
RenderableLayer({
required this.layer,
required Component? parent,
required this.map,
required this.camera,
required this.destTileSize,
+ required this.layerPaintFactory,
FilterQuality? filterQuality,
}) : filterQuality = filterQuality ?? FilterQuality.none {
this.parent = parent;
@@ -73,6 +82,7 @@ abstract class RenderableLayer extends PositionComponent
map: map,
destTileSize: destTileSize,
filterQuality: filterQuality,
+ layerPaintFactory: layerPaintFactory,
images: images,
);
} else if (layer is ObjectGroup) {
@@ -82,17 +92,18 @@ abstract class RenderableLayer extends PositionComponent
camera,
map,
destTileSize,
+ layerPaintFactory,
filterQuality,
);
} else if (layer is Group) {
- final groupLayer = layer;
return GroupLayer(
- layer: groupLayer,
+ layer: layer,
parent: parent,
camera: camera,
map: map,
destTileSize: destTileSize,
filterQuality: filterQuality,
+ layerPaintFactory: layerPaintFactory,
);
}
@@ -102,6 +113,7 @@ abstract class RenderableLayer extends PositionComponent
camera: camera,
map: map,
destTileSize: destTileSize,
+ layerPaintFactory: layerPaintFactory,
);
}
@@ -149,25 +161,29 @@ abstract class RenderableLayer extends PositionComponent
/// position.
/// https://doc.mapeditor.org/en/latest/manual/layers/#parallax-scrolling-factor
void applyParallaxOffset(Canvas canvas) {
- final anchor = camera?.viewfinder.anchor ?? Anchor.center;
- final cameraX = camera?.viewfinder.position.x ?? 0.0;
- final cameraY = camera?.viewfinder.position.y ?? 0.0;
- final viewportCenterX = (camera?.viewport.virtualSize.x ?? 0.0) * anchor.x;
- final viewportCenterY = (camera?.viewport.virtualSize.y ?? 0.0) * anchor.y;
- final topLeftX = cameraX - viewportCenterX;
- final topLeftY = cameraY - viewportCenterY;
-
- final double initOffsetX = viewportCenterX * parallaxX;
- final double initOffsetY = viewportCenterY * parallaxY;
+ final viewfinder = camera?.viewfinder;
+ final cameraX = viewfinder?.position.x ?? 0.0;
+ final cameraY = viewfinder?.position.y ?? 0.0;
+ final zoom = viewfinder?.zoom ?? 1.0;
// Use the complement of the parallax coefficient in order to account for
// the camera applying its transformations earlier in the render cycle
// of Flame. This adjustment draws the layers correctly w.r.t. their own
// offset and parallax values.
- final double x = offsetX + (cameraX * (1.0 - parallaxX));
- final double y = offsetY + (cameraY * (1.0 - parallaxY));
-
+ final viewportCenterX = (camera?.viewport.size.x ?? 0) * anchor.x;
+ final viewportCenterY = (camera?.viewport.size.y ?? 0) * anchor.y;
+
+ //final x = offsetX + (cameraX * (1.0 - parallaxX));
+ //final y = offsetY + (cameraY * (1.0 - parallaxY));
+ var x = (1.0 - parallaxX) * viewportCenterX;
+ var y = (1.0 - parallaxY) * viewportCenterY;
+ x /= zoom;
+ y /= zoom;
+ x += offsetX + cameraX - (cameraX * parallaxX);
+ y += offsetY + cameraY - (cameraY * parallaxY);
+ //cachedLayerOffset = Vector2(x / zoom, y / zoom);
cachedLayerOffset = Vector2(x, y);
+
canvas.translate(cachedLayerOffset.x, cachedLayerOffset.y);
}
diff --git a/packages/flame_tiled/lib/src/renderable_layers/tile_layers/tile_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/tile_layers/tile_layer.dart
index 0a5af95c3b7..f6efb562250 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/tile_layers/tile_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/tile_layers/tile_layer.dart
@@ -31,15 +31,11 @@ import 'package:meta/meta.dart';
/// {@endtemplate}
@internal
abstract class FlameTileLayer extends RenderableLayer {
- @override
- late Paint paint = layerPaintFactory(opacity);
-
final TiledAtlas tiledAtlas;
late List> transforms;
final animations = [];
final Map animationFrames;
final bool ignoreFlip;
- Paint Function(double opacity) layerPaintFactory;
FlameTileLayer({
required super.layer,
@@ -50,7 +46,7 @@ abstract class FlameTileLayer extends RenderableLayer {
required this.tiledAtlas,
required this.animationFrames,
required this.ignoreFlip,
- required this.layerPaintFactory,
+ required super.layerPaintFactory,
super.filterQuality,
});
@@ -137,12 +133,7 @@ abstract class FlameTileLayer extends RenderableLayer {
if (tiledAtlas.batch == null) {
return;
}
-
- canvas.save();
- //canvas.translate(offsetX, offsetY);
- //applyParallaxOffset(canvas);
tiledAtlas.batch!.render(canvas, paint: paint);
- canvas.restore();
}
@protected
diff --git a/packages/flame_tiled/lib/src/renderable_layers/tile_layers/unsupported_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/tile_layers/unsupported_layer.dart
index 9ba0c916aa3..895b20b70b6 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/tile_layers/unsupported_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/tile_layers/unsupported_layer.dart
@@ -10,6 +10,7 @@ class UnsupportedLayer extends RenderableLayer {
required super.parent,
required super.camera,
required super.destTileSize,
+ required super.layerPaintFactory,
});
@override
diff --git a/packages/flame_tiled/test/tiled_test.dart b/packages/flame_tiled/test/tiled_test.dart
index 471824a1fad..c9ab3619fc1 100644
--- a/packages/flame_tiled/test/tiled_test.dart
+++ b/packages/flame_tiled/test/tiled_test.dart
@@ -832,9 +832,7 @@ void main() {
late TiledComponent component;
final size = Vector2(256, 128);
- Future setupMap(
- Vector2 destTileSize,
- ) async {
+ Future setupMap(Vector2 destTileSize) async {
final bundle = TestAssetBundle(
imageNames: [
'isometric_spritesheet.png',
@@ -1070,21 +1068,21 @@ void main() {
matchesGoldenFile('goldens/dungeon_animation_${mapType}_0.png'),
);
- component.update(0.18);
+ component.updateTree(0.18);
pngData = await renderMapToPng(component);
await expectLater(
pngData,
matchesGoldenFile('goldens/dungeon_animation_${mapType}_1.png'),
);
- component.update(0.18);
+ component.updateTree(0.18);
pngData = await renderMapToPng(component);
await expectLater(
pngData,
matchesGoldenFile('goldens/dungeon_animation_${mapType}_2.png'),
);
- component.update(0.18);
+ component.updateTree(0.18);
pngData = await renderMapToPng(component);
await expectLater(
pngData,
From 91795aca24360fba6c1c833b4b87c96d087571d7 Mon Sep 17 00:00:00 2001
From: TheMaverickProgrammer
<91709+TheMaverickProgrammer@users.noreply.github.com>
Date: Wed, 13 Aug 2025 23:15:02 -0500
Subject: [PATCH 17/28] cleanup for review.
---
.../src/renderable_layers/image_layer.dart | 25 +++----------------
.../renderable_layers/renderable_layer.dart | 5 +---
.../lib/src/renderable_tile_map.dart | 11 ++++----
3 files changed, 10 insertions(+), 31 deletions(-)
diff --git a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
index 1b41afb8203..b33d9d8c870 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
@@ -15,7 +15,6 @@ class FlameImageLayer extends RenderableLayer {
late final ImageRepeat _repeat;
final MutableRect _paintArea = MutableRect.fromLTRB(0, 0, 0, 0);
final Vector2 _canvasSize = Vector2.zero();
- final Vector2 _maxTranslation = Vector2.zero();
FlameImageLayer({
required super.layer,
@@ -59,19 +58,6 @@ class FlameImageLayer extends RenderableLayer {
final imageW = _image.size.x;
final imageH = _image.size.y;
- // Track the maximum amount the canvas could have been translated
- // for this layer so we can calculate the wrap point within the
- // paint area.
- _maxTranslation.x = cachedLayerOffset.x;
- // offsetX; //- (visibleWorldRect.left * parallaxX);
- _maxTranslation.y = cachedLayerOffset.y;
- // offsetY; //- (visibleWorldRect.top * parallaxY);
-
- /*
- _maxTranslation.x = offsetX - camera.viewfinder.position.x * parallaxX;
- _maxTranslation.y = offsetY - camera.viewfinder.position.y * parallaxY;
-*/
-
// When the image is being repeated, make sure the _paintArea rect is
// big enough that it repeats off the edge of the canvas in both positive
// and negative directions on that axis (Tiled repeats forever on an axis).
@@ -80,7 +66,6 @@ class FlameImageLayer extends RenderableLayer {
// it still matches up with its initial layer offsets.
if (_repeat == ImageRepeat.repeatX || _repeat == ImageRepeat.repeat) {
final (left, right) = _calculatePaintRange(
- translation: _maxTranslation.x,
destSize: destSize.x,
imageSideLen: imageW,
layerOffset: cachedLayerOffset.x,
@@ -97,7 +82,6 @@ class FlameImageLayer extends RenderableLayer {
}
if (_repeat == ImageRepeat.repeatY || _repeat == ImageRepeat.repeat) {
final (top, bottom) = _calculatePaintRange(
- translation: _maxTranslation.y,
destSize: destSize.y,
imageSideLen: imageH,
layerOffset: cachedLayerOffset.y,
@@ -130,16 +114,15 @@ class FlameImageLayer extends RenderableLayer {
// particular way that reduces the time spent on computation and clip steps
// in flutter when drawing infinitely across an axis. This method accounts
// for the destination canvas size, camera viewport size, and the exact
- // coverage of the image w.r.t. [translation].
+ // coverage of the image w.r.t. translation.
// This is achieved by wrapping the rect coordinates around [destSize]
// after calculating the image coverage with [imageSideLen] and adding the
// unseen portion of the image in the span of the wrap range, if any.
//
// The return tuple value is the range where its centroid is the wrap point
- // plus the [layerOffset] which shifts the range to account for earlier
- // transformations applied to the canvas.
+ // plus the [layerOffset] which is the accumulation of all translations
+ // applied to this layer earlier in the render pipeline.
(double min, double max) _calculatePaintRange({
- required double translation,
required double destSize,
required double imageSideLen,
required double layerOffset,
@@ -158,7 +141,7 @@ class FlameImageLayer extends RenderableLayer {
final unseen = (imageCount - seen) * imageSideLen;
// Wrap around the target axis w.r.t. parallax.
- final wrapPoint = translation.ceil() % (destSize + unseen).toInt();
+ final wrapPoint = layerOffset.ceil() % (destSize + unseen).toInt();
// Partition the _paintArea into two parts.
final part = (wrapPoint / imageSideLen).ceil();
diff --git a/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
index 5eb05df7e7b..902d06a3b27 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
@@ -173,17 +173,14 @@ abstract class RenderableLayer extends PositionComponent
final viewportCenterX = (camera?.viewport.size.x ?? 0) * anchor.x;
final viewportCenterY = (camera?.viewport.size.y ?? 0) * anchor.y;
- //final x = offsetX + (cameraX * (1.0 - parallaxX));
- //final y = offsetY + (cameraY * (1.0 - parallaxY));
var x = (1.0 - parallaxX) * viewportCenterX;
var y = (1.0 - parallaxY) * viewportCenterY;
x /= zoom;
y /= zoom;
x += offsetX + cameraX - (cameraX * parallaxX);
y += offsetY + cameraY - (cameraY * parallaxY);
- //cachedLayerOffset = Vector2(x / zoom, y / zoom);
- cachedLayerOffset = Vector2(x, y);
+ cachedLayerOffset = Vector2(x, y);
canvas.translate(cachedLayerOffset.x, cachedLayerOffset.y);
}
diff --git a/packages/flame_tiled/lib/src/renderable_tile_map.dart b/packages/flame_tiled/lib/src/renderable_tile_map.dart
index b028d21d1c2..8164761c514 100644
--- a/packages/flame_tiled/lib/src/renderable_tile_map.dart
+++ b/packages/flame_tiled/lib/src/renderable_tile_map.dart
@@ -394,7 +394,6 @@ class RenderableTiledMap extends Component
@override
Future? onLoad() async {
- await super.onLoad();
// Automatically use the first attached CameraComponent camera if it's not
// already set..
camera ??= game.children.query().firstOrNull;
@@ -423,23 +422,23 @@ class RenderableTiledMap extends Component
if (_backgroundPaint != null) {
c.drawPaint(_backgroundPaint);
}
-
super.render(c);
}
- /// Returns a layer of type [T] with given [name] from all the layers
+ /// Returns a [Layer] of type [L] with given [name] from all the layers
/// of this map. If no such layer is found, null is returned.
L? getLayer(String name) {
try {
- // layerByName will searches recursively starting with tiled.dart v0.8.5
+ // layerByName searches recursively starting with tiled.dart v0.8.5
return map.layerByName(name) as L;
} on ArgumentError {
return null;
}
}
- /// Returns a [RenderableLayer] with given [name] from all the layers
- /// of this map. If no such layer is found, null is returned.
+ /// Returns a [RenderableLayer] with given [name] from all the
+ /// [renderableLayers] tracked by this component.
+ /// If no such layer is found, null is returned.
RenderableLayer? getRenderableLayer(String name) =>
switch (renderableLayers.indexWhere((e) => e.layer.name == name)) {
-1 => null,
From 991eea6bc96f5396b3ced337b456e662e6068eff Mon Sep 17 00:00:00 2001
From: TheMaverickProgrammer
<91709+TheMaverickProgrammer@users.noreply.github.com>
Date: Thu, 14 Aug 2025 00:07:44 -0500
Subject: [PATCH 18/28] patch yaml
---
packages/flame_tiled/pubspec.yaml | 2 +-
packages/flame_tiled/test/tiled_test.dart | 206 +++++++++++-----------
2 files changed, 108 insertions(+), 100 deletions(-)
diff --git a/packages/flame_tiled/pubspec.yaml b/packages/flame_tiled/pubspec.yaml
index cb852ccb1c7..43634a17d5c 100644
--- a/packages/flame_tiled/pubspec.yaml
+++ b/packages/flame_tiled/pubspec.yaml
@@ -27,8 +27,8 @@ dependencies:
dev_dependencies:
dartdoc: ^8.0.8
- flame_test: ^1.17.2
flame_lint: ^1.4.1
+ flame_test: ^2.0.1
flutter_test:
sdk: flutter
test: any
diff --git a/packages/flame_tiled/test/tiled_test.dart b/packages/flame_tiled/test/tiled_test.dart
index 7e18b8ea71e..e553157351c 100644
--- a/packages/flame_tiled/test/tiled_test.dart
+++ b/packages/flame_tiled/test/tiled_test.dart
@@ -11,7 +11,7 @@ import 'package:flame_tiled/src/renderable_layers/tile_layers/tile_layer.dart';
import 'package:flutter/services.dart';
import 'package:flutter_test/flutter_test.dart'
hide expect, group, setUp, isInstanceOf, expectLater;
-import 'package:test/test.dart';
+import 'package:test/test.dart' hide test;
import 'test_asset_bundle.dart';
import 'test_image_utils.dart';
@@ -41,23 +41,25 @@ void main() {
images: Images(bundle: Flame.bundle),
key: ComponentKey.named('test'),
);
- });
+ }
testWithFlameGame('correct loads the file', (game) async {
await setUp(game);
expect(tiled.tileMap.renderableLayers.length, equals(4));
});
- testWithFlameGame('component atlases returns the loaded atlases',
- (game) async {
+ testWithFlameGame('component atlases returns the loaded atlases', (
+ game,
+ ) async {
await setUp(game);
final atlases = tiled.atlases();
expect(atlases, hasLength(1));
expect(atlases.first.$1, equals('map-level1.png'));
});
- testWithFlameGame('correct loads the file, with different prefix',
- (game) async {
+ testWithFlameGame('correct loads the file, with different prefix', (
+ game,
+ ) async {
await setUp(game);
tiled = await TiledComponent.load(
'map_custom_path.tmx',
@@ -74,8 +76,9 @@ void main() {
});
group('is positionable', () {
- testWithFlameGame('size, width, and height are readable - not writable',
- (game) async {
+ testWithFlameGame('size, width, and height are readable - not writable', (
+ game,
+ ) async {
await setUp(game);
expect(tiled.size, Vector2(512.0, 2048.0));
expect(tiled.width, 512);
@@ -138,8 +141,9 @@ void main() {
);
});
- testWithFlameGame('correctly loads external tileset with custom path',
- (game) async {
+ testWithFlameGame('correctly loads external tileset with custom path', (
+ game,
+ ) async {
// Flame.bundle is a global static. Updating these in tests can lead to
// odd errors if you're trying to debug.
Flame.bundle = TestAssetBundle(
@@ -307,99 +311,101 @@ void main() {
}
testWithFlameGame(
- '[useAtlas = true] Green tile pixels are in correct spots',
- (game) async {
- await setUp(game);
-
- const oneColorRect = 8;
- final leftTilePixels = [];
- for (
- var i = 65 * oneColorRect * pixel;
- i < ((64 * 23) + (oneColorRect * 3)) * pixel;
- i += 64 * pixel
- ) {
- leftTilePixels.addAll(
- pixelsAfterFlipApplied.getRange(i, i + (16 * pixel)),
- );
- }
+ '[useAtlas = true] Green tile pixels are in correct spots',
+ (game) async {
+ await setUp(game);
- var allGreen = true;
- for (var i = 0; i < leftTilePixels.length; i += pixel) {
- allGreen &=
- leftTilePixels[i] == 0 &&
- leftTilePixels[i + 1] == 255 &&
- leftTilePixels[i + 2] == 0 &&
- leftTilePixels[i + 3] == 255;
- }
- expect(allGreen, true);
+ const oneColorRect = 8;
+ final leftTilePixels = [];
+ for (
+ var i = 65 * oneColorRect * pixel;
+ i < ((64 * 23) + (oneColorRect * 3)) * pixel;
+ i += 64 * pixel
+ ) {
+ leftTilePixels.addAll(
+ pixelsAfterFlipApplied.getRange(i, i + (16 * pixel)),
+ );
+ }
- final rightTilePixels = [];
- for (
- var i = 69 * 8 * pixel;
- i < ((64 * 23) + (8 * 7)) * pixel;
- i += 64 * pixel
- ) {
- rightTilePixels.addAll(
- pixelsAfterFlipApplied.getRange(i, i + (16 * pixel)),
- );
- }
+ var allGreen = true;
+ for (var i = 0; i < leftTilePixels.length; i += pixel) {
+ allGreen &=
+ leftTilePixels[i] == 0 &&
+ leftTilePixels[i + 1] == 255 &&
+ leftTilePixels[i + 2] == 0 &&
+ leftTilePixels[i + 3] == 255;
+ }
+ expect(allGreen, true);
+
+ final rightTilePixels = [];
+ for (
+ var i = 69 * 8 * pixel;
+ i < ((64 * 23) + (8 * 7)) * pixel;
+ i += 64 * pixel
+ ) {
+ rightTilePixels.addAll(
+ pixelsAfterFlipApplied.getRange(i, i + (16 * pixel)),
+ );
+ }
- for (var i = 0; i < rightTilePixels.length; i += pixel) {
- allGreen &=
- rightTilePixels[i] == 0 &&
- rightTilePixels[i + 1] == 255 &&
- rightTilePixels[i + 2] == 0 &&
- rightTilePixels[i + 3] == 255;
- }
- expect(allGreen, true);
- });
+ for (var i = 0; i < rightTilePixels.length; i += pixel) {
+ allGreen &=
+ rightTilePixels[i] == 0 &&
+ rightTilePixels[i + 1] == 255 &&
+ rightTilePixels[i + 2] == 0 &&
+ rightTilePixels[i + 3] == 255;
+ }
+ expect(allGreen, true);
+ },
+ );
testWithFlameGame(
- '[useAtlas = false] Green tile pixels are in correct spots',
- (game) async {
- await setUp(game);
-
- final leftTilePixels = [];
- for (
- var i = 65 * 8 * pixel;
- i < ((64 * 23) + (8 * 3)) * pixel;
- i += 64 * pixel
- ) {
- leftTilePixels.addAll(
- pixelsBeforeFlipApplied.getRange(i, i + (16 * pixel)),
- );
- }
+ '[useAtlas = false] Green tile pixels are in correct spots',
+ (game) async {
+ await setUp(game);
- var allGreen = true;
- for (var i = 0; i < leftTilePixels.length; i += pixel) {
- allGreen &=
- leftTilePixels[i] == 0 &&
- leftTilePixels[i + 1] == 255 &&
- leftTilePixels[i + 2] == 0 &&
- leftTilePixels[i + 3] == 255;
- }
- expect(allGreen, true);
+ final leftTilePixels = [];
+ for (
+ var i = 65 * 8 * pixel;
+ i < ((64 * 23) + (8 * 3)) * pixel;
+ i += 64 * pixel
+ ) {
+ leftTilePixels.addAll(
+ pixelsBeforeFlipApplied.getRange(i, i + (16 * pixel)),
+ );
+ }
- final rightTilePixels = [];
- for (
- var i = 69 * 8 * pixel;
- i < ((64 * 23) + (8 * 7)) * pixel;
- i += 64 * pixel
- ) {
- rightTilePixels.addAll(
- pixelsBeforeFlipApplied.getRange(i, i + (16 * pixel)),
- );
- }
+ var allGreen = true;
+ for (var i = 0; i < leftTilePixels.length; i += pixel) {
+ allGreen &=
+ leftTilePixels[i] == 0 &&
+ leftTilePixels[i + 1] == 255 &&
+ leftTilePixels[i + 2] == 0 &&
+ leftTilePixels[i + 3] == 255;
+ }
+ expect(allGreen, true);
+
+ final rightTilePixels = [];
+ for (
+ var i = 69 * 8 * pixel;
+ i < ((64 * 23) + (8 * 7)) * pixel;
+ i += 64 * pixel
+ ) {
+ rightTilePixels.addAll(
+ pixelsBeforeFlipApplied.getRange(i, i + (16 * pixel)),
+ );
+ }
- for (var i = 0; i < rightTilePixels.length; i += pixel) {
- allGreen &=
- rightTilePixels[i] == 0 &&
- rightTilePixels[i + 1] == 255 &&
- rightTilePixels[i + 2] == 0 &&
- rightTilePixels[i + 3] == 255;
- }
- expect(allGreen, true);
- });
+ for (var i = 0; i < rightTilePixels.length; i += pixel) {
+ allGreen &=
+ rightTilePixels[i] == 0 &&
+ rightTilePixels[i + 1] == 255 &&
+ rightTilePixels[i + 2] == 0 &&
+ rightTilePixels[i + 3] == 255;
+ }
+ expect(allGreen, true);
+ },
+ );
});
group('ignoring flip makes different texture and rendering result', () {
@@ -1006,8 +1012,9 @@ void main() {
await game.ready();
}
- testWithFlameGame('handle single frame animations ($mapType)',
- (game) async {
+ testWithFlameGame('handle single frame animations ($mapType)', (
+ game,
+ ) async {
await setUp(game);
expect(map.renderableLayers.first is FlameTileLayer, true);
@@ -1025,8 +1032,9 @@ void main() {
expect(layer.animations.first.frames.sources, hasLength(1));
});
- testWithFlameGame('handle single frame animations ($mapType)',
- (game) async {
+ testWithFlameGame('handle single frame animations ($mapType)', (
+ game,
+ ) async {
await setUp(game);
expect(
From f8070181acc15b06bbaa533bcff6986198a9734d Mon Sep 17 00:00:00 2001
From: TheMaverickProgrammer
<91709+TheMaverickProgrammer@users.noreply.github.com>
Date: Thu, 14 Aug 2025 00:16:54 -0500
Subject: [PATCH 19/28] satisfy word check linter
---
.../flame_tiled/lib/src/renderable_layers/group_layer.dart | 6 +++---
1 file changed, 3 insertions(+), 3 deletions(-)
diff --git a/packages/flame_tiled/lib/src/renderable_layers/group_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/group_layer.dart
index 25ad661428d..2d9a1658aec 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/group_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/group_layer.dart
@@ -16,9 +16,9 @@ class GroupLayer extends RenderableLayer {
@override
void refreshCache() {
- final sublayers = children.whereType();
- for (final sub in sublayers) {
- sub.refreshCache();
+ final childLayers = children.whereType();
+ for (final child in childLayers) {
+ child.refreshCache();
}
}
}
From a9776ea2a49108667d828c4a173223bf4bc78f51 Mon Sep 17 00:00:00 2001
From: TheMaverickProgrammer
<91709+TheMaverickProgrammer@users.noreply.github.com>
Date: Thu, 14 Aug 2025 16:15:35 -0500
Subject: [PATCH 20/28] melos format. fixed tiled component key test.
---
.../lib/src/renderable_layers/image_layer.dart | 6 ++++--
.../lib/src/renderable_layers/renderable_layer.dart | 12 ++++++++----
packages/flame_tiled/test/tiled_test.dart | 3 ++-
3 files changed, 14 insertions(+), 7 deletions(-)
diff --git a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
index b33d9d8c870..3d72cd9a6cd 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
@@ -65,7 +65,8 @@ class FlameImageLayer extends RenderableLayer {
// image's length along that axis (width or height) so that with repeats
// it still matches up with its initial layer offsets.
if (_repeat == ImageRepeat.repeatX || _repeat == ImageRepeat.repeat) {
- final (left, right) = _calculatePaintRange(
+ final (left, right) =
+ _calculatePaintRange(
destSize: destSize.x,
imageSideLen: imageW,
layerOffset: cachedLayerOffset.x,
@@ -81,7 +82,8 @@ class FlameImageLayer extends RenderableLayer {
_paintArea.right = imageW;
}
if (_repeat == ImageRepeat.repeatY || _repeat == ImageRepeat.repeat) {
- final (top, bottom) = _calculatePaintRange(
+ final (top, bottom) =
+ _calculatePaintRange(
destSize: destSize.y,
imageSideLen: imageH,
layerOffset: cachedLayerOffset.y,
diff --git a/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
index 902d06a3b27..00db012b689 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
@@ -124,13 +124,15 @@ abstract class RenderableLayer extends PositionComponent
double get scaleX => destTileSize.x / map.tileWidth;
double get scaleY => destTileSize.y / map.tileHeight;
- late double offsetX = layer.offsetX * scaleX +
+ late double offsetX =
+ layer.offsetX * scaleX +
switch (parent) {
final GroupLayer p => p.offsetX,
_ => 0,
};
- late double offsetY = layer.offsetY * scaleY +
+ late double offsetY =
+ layer.offsetY * scaleY +
switch (parent) {
final GroupLayer p => p.offsetY,
_ => 0,
@@ -144,13 +146,15 @@ abstract class RenderableLayer extends PositionComponent
_ => 1,
};
- late double parallaxX = layer.parallaxX *
+ late double parallaxX =
+ layer.parallaxX *
switch (parent) {
final GroupLayer p => p.parallaxX,
_ => 1,
};
- late double parallaxY = layer.parallaxY *
+ late double parallaxY =
+ layer.parallaxY *
switch (parent) {
final GroupLayer p => p.parallaxY,
_ => 1,
diff --git a/packages/flame_tiled/test/tiled_test.dart b/packages/flame_tiled/test/tiled_test.dart
index e553157351c..a3a2d4c918d 100644
--- a/packages/flame_tiled/test/tiled_test.dart
+++ b/packages/flame_tiled/test/tiled_test.dart
@@ -71,7 +71,8 @@ void main() {
expect(tiled.tileMap.renderableLayers.length, equals(3));
});
- test('assigns key', () async {
+ testWithFlameGame('assigns key', (game) async {
+ await setUp(game);
expect(tiled.key, equals(ComponentKey.named('test')));
});
From d69552a1a78f795c4bbd705c2e82fe2de3fcf253 Mon Sep 17 00:00:00 2001
From: TheMaverickProgrammer
<91709+TheMaverickProgrammer@users.noreply.github.com>
Date: Thu, 14 Aug 2025 16:31:55 -0500
Subject: [PATCH 21/28] PR review: elaborate documentation for UnsupportedLayer
class.
---
.../renderable_layers/tile_layers/unsupported_layer.dart | 8 +++++++-
1 file changed, 7 insertions(+), 1 deletion(-)
diff --git a/packages/flame_tiled/lib/src/renderable_layers/tile_layers/unsupported_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/tile_layers/unsupported_layer.dart
index 895b20b70b6..dafb6928aab 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/tile_layers/unsupported_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/tile_layers/unsupported_layer.dart
@@ -1,7 +1,13 @@
import 'package:flame_tiled/flame_tiled.dart';
import 'package:meta/meta.dart';
-// Represents a [RenderableLayer] that cannot be parsed by this package.
+/// An instance of this class represents a [RenderableLayer] that could not be
+/// parsed by this package.
+///
+/// Because every [RenderableLayer]'s offsets are accumulated and used by their
+/// down-stream child components during rendering, an [UnsupportedLayer]
+/// must be retained in the component tree. In this way, offsets can propogate
+/// correctly even if this package lacks features in newer Tiled versions.
@internal
class UnsupportedLayer extends RenderableLayer {
UnsupportedLayer({
From 3dea410000cd755e46f6b19fb07c0cbe2725db2d Mon Sep 17 00:00:00 2001
From: TheMaverickProgrammer
<91709+TheMaverickProgrammer@users.noreply.github.com>
Date: Thu, 14 Aug 2025 16:33:54 -0500
Subject: [PATCH 22/28] satisfy spell checker
---
.../src/renderable_layers/tile_layers/unsupported_layer.dart | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/packages/flame_tiled/lib/src/renderable_layers/tile_layers/unsupported_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/tile_layers/unsupported_layer.dart
index dafb6928aab..57e1c6c92e6 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/tile_layers/unsupported_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/tile_layers/unsupported_layer.dart
@@ -6,7 +6,7 @@ import 'package:meta/meta.dart';
///
/// Because every [RenderableLayer]'s offsets are accumulated and used by their
/// down-stream child components during rendering, an [UnsupportedLayer]
-/// must be retained in the component tree. In this way, offsets can propogate
+/// must be retained in the component tree. In this way, offsets can propagate
/// correctly even if this package lacks features in newer Tiled versions.
@internal
class UnsupportedLayer extends RenderableLayer {
From 0cf2ea480c72576ee424b12c398516b82af38a6b Mon Sep 17 00:00:00 2001
From: TheMaverickProgrammer
<91709+TheMaverickProgrammer@users.noreply.github.com>
Date: Fri, 15 Aug 2025 00:29:42 -0500
Subject: [PATCH 23/28] new math allows each layer to apply its own offset to
the canvas using partial parallax locality. This is equivalent to the product
of all parent parallax coefficients.
---
.../src/renderable_layers/image_layer.dart | 26 ++++---
.../renderable_layers/renderable_layer.dart | 78 +++++++------------
2 files changed, 44 insertions(+), 60 deletions(-)
diff --git a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
index 3d72cd9a6cd..15365e3db29 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
@@ -37,7 +37,6 @@ class FlameImageLayer extends RenderableLayer {
@override
void render(Canvas canvas) {
- canvas.save();
_resizePaintArea(camera);
paintImage(
canvas: canvas,
@@ -48,8 +47,6 @@ class FlameImageLayer extends RenderableLayer {
repeat: _repeat,
filterQuality: filterQuality,
);
-
- canvas.restore();
}
void _resizePaintArea(CameraComponent? camera) {
@@ -68,8 +65,8 @@ class FlameImageLayer extends RenderableLayer {
final (left, right) =
_calculatePaintRange(
destSize: destSize.x,
- imageSideLen: imageW,
- layerOffset: cachedLayerOffset.x,
+ imageSideLen: imageW.toInt(),
+ layerOffset: cachedLocalParallax.x.round(),
) + // Apply camera left/right to range.
(visibleWorldRect.left, visibleWorldRect.right);
@@ -85,8 +82,8 @@ class FlameImageLayer extends RenderableLayer {
final (top, bottom) =
_calculatePaintRange(
destSize: destSize.y,
- imageSideLen: imageH,
- layerOffset: cachedLayerOffset.y,
+ imageSideLen: imageH.toInt(),
+ layerOffset: cachedLocalParallax.y.round(),
) + // Apply camera top/bottom to range.
(visibleWorldRect.top, visibleWorldRect.bottom);
@@ -126,8 +123,8 @@ class FlameImageLayer extends RenderableLayer {
// applied to this layer earlier in the render pipeline.
(double min, double max) _calculatePaintRange({
required double destSize,
- required double imageSideLen,
- required double layerOffset,
+ required int imageSideLen,
+ required int layerOffset,
}) {
// Prevent DBZ error.
if (imageSideLen < 1) {
@@ -143,7 +140,7 @@ class FlameImageLayer extends RenderableLayer {
final unseen = (imageCount - seen) * imageSideLen;
// Wrap around the target axis w.r.t. parallax.
- final wrapPoint = layerOffset.ceil() % (destSize + unseen).toInt();
+ final wrapPoint = layerOffset % (destSize + unseen).ceil();
// Partition the _paintArea into two parts.
final part = (wrapPoint / imageSideLen).ceil();
@@ -151,7 +148,7 @@ class FlameImageLayer extends RenderableLayer {
return (
wrapPoint - (imageSideLen * part) - layerOffset,
wrapPoint + (imageSideLen * (imageCount - part)) - layerOffset,
- );
+ ).toDouble();
}
static Future load({
@@ -181,7 +178,12 @@ class FlameImageLayer extends RenderableLayer {
}
/// Provide tuples with addition.
-extension _PrivRangeTupleHelper on (double, double) {
+extension _PrivRangeTupleDouble on (double, double) {
(double, double) operator +((double, double) other) =>
($1 + other.$1, $2 + other.$2);
}
+
+/// Provides tuples of ints to doubles.
+extension _PrivRangeTupleInt on (int, int) {
+ (double, double) toDouble() => ($1.toDouble(), $2.toDouble());
+}
diff --git a/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
index 00db012b689..0077d557701 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
@@ -25,7 +25,7 @@ abstract class RenderableLayer extends PositionComponent
/// Cached canvas translation used in parallax effects.
/// This field needs to be read in the case of repeated textures
/// as an optimization step.
- Vector2 cachedLayerOffset = Vector2.zero();
+ Vector2 cachedLocalParallax = Vector2.zero();
/// Given the layer's [opacity], compute [Paint] for drawing.
/// This is useful if your layer requires translucency or other effects.
@@ -121,23 +121,6 @@ abstract class RenderableLayer extends PositionComponent
void refreshCache();
- double get scaleX => destTileSize.x / map.tileWidth;
- double get scaleY => destTileSize.y / map.tileHeight;
-
- late double offsetX =
- layer.offsetX * scaleX +
- switch (parent) {
- final GroupLayer p => p.offsetX,
- _ => 0,
- };
-
- late double offsetY =
- layer.offsetY * scaleY +
- switch (parent) {
- final GroupLayer p => p.offsetY,
- _ => 0,
- };
-
@override
double get opacity =>
layer.opacity *
@@ -146,19 +129,11 @@ abstract class RenderableLayer extends PositionComponent
_ => 1,
};
- late double parallaxX =
- layer.parallaxX *
- switch (parent) {
- final GroupLayer p => p.parallaxX,
- _ => 1,
- };
+ double get scaleX => destTileSize.x / map.tileWidth;
+ double get scaleY => destTileSize.y / map.tileHeight;
- late double parallaxY =
- layer.parallaxY *
- switch (parent) {
- final GroupLayer p => p.parallaxY,
- _ => 1,
- };
+ late double offsetX = layer.offsetX * scaleX;
+ late double offsetY = layer.offsetY * scaleY;
/// Calculates the offset we need to apply to the canvas to compensate for
/// parallax positioning and scroll for the layer and the current camera
@@ -166,26 +141,33 @@ abstract class RenderableLayer extends PositionComponent
/// https://doc.mapeditor.org/en/latest/manual/layers/#parallax-scrolling-factor
void applyParallaxOffset(Canvas canvas) {
final viewfinder = camera?.viewfinder;
- final cameraX = viewfinder?.position.x ?? 0.0;
- final cameraY = viewfinder?.position.y ?? 0.0;
+ final cameraX = viewfinder?.position.x ?? 0;
+ final cameraY = viewfinder?.position.y ?? 0;
final zoom = viewfinder?.zoom ?? 1.0;
- // Use the complement of the parallax coefficient in order to account for
- // the camera applying its transformations earlier in the render cycle
- // of Flame. This adjustment draws the layers correctly w.r.t. their own
- // offset and parallax values.
- final viewportCenterX = (camera?.viewport.size.x ?? 0) * anchor.x;
- final viewportCenterY = (camera?.viewport.size.y ?? 0) * anchor.y;
-
- var x = (1.0 - parallaxX) * viewportCenterX;
- var y = (1.0 - parallaxY) * viewportCenterY;
- x /= zoom;
- y /= zoom;
- x += offsetX + cameraX - (cameraX * parallaxX);
- y += offsetY + cameraY - (cameraY * parallaxY);
-
- cachedLayerOffset = Vector2(x, y);
- canvas.translate(cachedLayerOffset.x, cachedLayerOffset.y);
+ final x = cameraX - (cameraX * layer.parallaxX);
+ final y = cameraY - (cameraY * layer.parallaxY);
+
+ final deltaX =
+ layer.parallaxX *
+ switch (parent) {
+ final GroupLayer p => p.cachedLocalParallax.x,
+ _ => 0,
+ };
+
+ final deltaY =
+ layer.parallaxY *
+ switch (parent) {
+ final GroupLayer p => p.cachedLocalParallax.y,
+ _ => 0,
+ };
+
+ cachedLocalParallax = Vector2(deltaX + x / zoom, deltaY + y / zoom);
+
+ canvas.translate(
+ offsetX + cachedLocalParallax.x,
+ offsetY + cachedLocalParallax.y,
+ );
}
// Only render if this layer is [visible].
From f257c7ff55e4042feee6453e6fe2ee14bbceb5b1 Mon Sep 17 00:00:00 2001
From: TheMaverickProgrammer
<91709+TheMaverickProgrammer@users.noreply.github.com>
Date: Fri, 15 Aug 2025 12:23:06 -0500
Subject: [PATCH 24/28] fixed parallax effect optimization with new layer
translation math
---
.../src/renderable_layers/image_layer.dart | 20 +++---
.../renderable_layers/renderable_layer.dart | 68 +++++++++++++------
2 files changed, 58 insertions(+), 30 deletions(-)
diff --git a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
index 15365e3db29..8a7018de6a2 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
@@ -50,6 +50,7 @@ class FlameImageLayer extends RenderableLayer {
}
void _resizePaintArea(CameraComponent? camera) {
+ final displacement = absoluteParallax + absoluteOffset;
final visibleWorldRect = camera?.visibleWorldRect ?? Rect.zero;
final destSize = camera?.viewport.virtualSize ?? _canvasSize;
final imageW = _image.size.x;
@@ -66,7 +67,8 @@ class FlameImageLayer extends RenderableLayer {
_calculatePaintRange(
destSize: destSize.x,
imageSideLen: imageW.toInt(),
- layerOffset: cachedLocalParallax.x.round(),
+ parallax: absoluteParallax.x.round(),
+ displacement: displacement.x.round(),
) + // Apply camera left/right to range.
(visibleWorldRect.left, visibleWorldRect.right);
@@ -83,7 +85,8 @@ class FlameImageLayer extends RenderableLayer {
_calculatePaintRange(
destSize: destSize.y,
imageSideLen: imageH.toInt(),
- layerOffset: cachedLocalParallax.y.round(),
+ parallax: absoluteParallax.y.round(),
+ displacement: displacement.y.round(),
) + // Apply camera top/bottom to range.
(visibleWorldRect.top, visibleWorldRect.bottom);
@@ -113,18 +116,19 @@ class FlameImageLayer extends RenderableLayer {
// particular way that reduces the time spent on computation and clip steps
// in flutter when drawing infinitely across an axis. This method accounts
// for the destination canvas size, camera viewport size, and the exact
- // coverage of the image w.r.t. translation.
+ // coverage of the image w.r.t. [parallax].
// This is achieved by wrapping the rect coordinates around [destSize]
// after calculating the image coverage with [imageSideLen] and adding the
// unseen portion of the image in the span of the wrap range, if any.
//
// The return tuple value is the range where its centroid is the wrap point
- // plus the [layerOffset] which is the accumulation of all translations
+ // minus the [displacement] which is the accumulation of all translations
// applied to this layer earlier in the render pipeline.
(double min, double max) _calculatePaintRange({
required double destSize,
required int imageSideLen,
- required int layerOffset,
+ required int parallax,
+ required int displacement,
}) {
// Prevent DBZ error.
if (imageSideLen < 1) {
@@ -140,14 +144,14 @@ class FlameImageLayer extends RenderableLayer {
final unseen = (imageCount - seen) * imageSideLen;
// Wrap around the target axis w.r.t. parallax.
- final wrapPoint = layerOffset % (destSize + unseen).ceil();
+ final wrapPoint = (displacement + parallax) % (destSize + unseen).ceil();
// Partition the _paintArea into two parts.
final part = (wrapPoint / imageSideLen).ceil();
return (
- wrapPoint - (imageSideLen * part) - layerOffset,
- wrapPoint + (imageSideLen * (imageCount - part)) - layerOffset,
+ wrapPoint - (imageSideLen * part) - displacement,
+ wrapPoint + (imageSideLen * (imageCount - part)) - displacement,
).toDouble();
}
diff --git a/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
index 0077d557701..406a1591947 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
@@ -22,10 +22,14 @@ abstract class RenderableLayer extends PositionComponent
/// The [FilterQuality] that should be used by all the layers.
final FilterQuality filterQuality;
- /// Cached canvas translation used in parallax effects.
+ /// The total canvas translation used in parallax effects.
/// This field needs to be read in the case of repeated textures
/// as an optimization step.
- Vector2 cachedLocalParallax = Vector2.zero();
+ Vector2 absoluteParallax = Vector2.zero();
+
+ /// The total offsets computed from our [Layer.offsetX] and [Layer.offsetY]
+ /// and all parent layer offsets, if any.
+ Vector2 absoluteOffset = Vector2.zero();
/// Given the layer's [opacity], compute [Paint] for drawing.
/// This is useful if your layer requires translucency or other effects.
@@ -145,28 +149,48 @@ abstract class RenderableLayer extends PositionComponent
final cameraY = viewfinder?.position.y ?? 0;
final zoom = viewfinder?.zoom ?? 1.0;
- final x = cameraX - (cameraX * layer.parallaxX);
- final y = cameraY - (cameraY * layer.parallaxY);
-
- final deltaX =
- layer.parallaxX *
- switch (parent) {
- final GroupLayer p => p.cachedLocalParallax.x,
- _ => 0,
- };
-
- final deltaY =
- layer.parallaxY *
- switch (parent) {
- final GroupLayer p => p.cachedLocalParallax.y,
- _ => 0,
- };
-
- cachedLocalParallax = Vector2(deltaX + x / zoom, deltaY + y / zoom);
+ // Discover parent layer terms used for the calculations below.
+ final parentOffset = switch (parent) {
+ final GroupLayer p => p.absoluteOffset,
+ _ => Vector2.zero(),
+ };
+ final parentParallax = switch (parent) {
+ final GroupLayer p => p.absoluteParallax,
+ _ => Vector2(1.0, 1.0),
+ };
+ final parallaxLocality = Vector2(
+ layer.parallaxX * parentParallax.x,
+ layer.parallaxY * parentParallax.y,
+ );
+ // Calculate our local parallax.
+ double calcParallax(double cam, double parallax) => cam - (cam * parallax);
+ final localParallax =
+ Vector2(
+ calcParallax(cameraX, layer.parallaxX),
+ calcParallax(cameraY, layer.parallaxY),
+ ) /
+ zoom;
+
+ absoluteParallax = localParallax + parallaxLocality;
+ absoluteOffset = parentOffset + Vector2(offsetX, offsetY);
+
+ // Adjustment term for canvas translation below.
+ final delta = switch (parent is GroupLayer) {
+ true => parentParallax - localParallax,
+ false => Vector2.zero(),
+ };
+
+ // Strictly apply local translations in our render step.
+ //
+ // Explanation:
+ // B/c sum of all local translations w.r.t. parallax locality is equal to the
+ // products of all absolute parallax values followed by translation, the
+ // scene graph render by Flame produces the same visuals as painting layers
+ //using absolute values in a traditional composite renderer.
canvas.translate(
- offsetX + cachedLocalParallax.x,
- offsetY + cachedLocalParallax.y,
+ offsetX + localParallax.x + delta.x,
+ offsetY + localParallax.y + delta.y,
);
}
From bfc96414ccb41b747b1d61e1ff220a5102e3d492 Mon Sep 17 00:00:00 2001
From: TheMaverickProgrammer
<91709+TheMaverickProgrammer@users.noreply.github.com>
Date: Fri, 15 Aug 2025 17:38:39 -0500
Subject: [PATCH 25/28] Wrap math fixed.
---
.../src/renderable_layers/image_layer.dart | 52 ++++++++++---------
.../renderable_layers/renderable_layer.dart | 18 +++----
2 files changed, 37 insertions(+), 33 deletions(-)
diff --git a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
index 8a7018de6a2..15be9116da8 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
@@ -50,26 +50,31 @@ class FlameImageLayer extends RenderableLayer {
}
void _resizePaintArea(CameraComponent? camera) {
- final displacement = absoluteParallax + absoluteOffset;
final visibleWorldRect = camera?.visibleWorldRect ?? Rect.zero;
final destSize = camera?.viewport.virtualSize ?? _canvasSize;
final imageW = _image.size.x;
final imageH = _image.size.y;
- // When the image is being repeated, make sure the _paintArea rect is
- // big enough that it repeats off the edge of the canvas in both positive
- // and negative directions on that axis (Tiled repeats forever on an axis).
- // Also, make sure the rect's left and top are only moved by exactly the
- // image's length along that axis (width or height) so that with repeats
- // it still matches up with its initial layer offsets.
+ // In order to recover the displacement w.r.t. the canvas translation,
+ // subtract the center view vector to this accumulated offset vector.
+ // B/c we will wrap the image along the axis infinitely, add the negative
+ // of the accumulated offset in order to cancel out the layer translation
+ // and bake the offset value into the total parallax term instead.
+ final center = visibleWorldRect.center.toVector2();
+ final displacement = absoluteParallax - center - absoluteOffset;
+ final parallax = absoluteOffset + absoluteParallax;
+
+ /// [_calculatePaintRange] ensures the _paintArea rect is
+ /// big enough that it repeats off the edge of the canvas in both positive
+ /// and negative directions on that axis (Tiled repeats forever on an axis).
if (_repeat == ImageRepeat.repeatX || _repeat == ImageRepeat.repeat) {
final (left, right) =
_calculatePaintRange(
destSize: destSize.x,
imageSideLen: imageW.toInt(),
- parallax: absoluteParallax.x.round(),
+ parallax: parallax.x.round(),
displacement: displacement.x.round(),
- ) + // Apply camera left/right to range.
+ ) +
(visibleWorldRect.left, visibleWorldRect.right);
_paintArea
@@ -85,9 +90,9 @@ class FlameImageLayer extends RenderableLayer {
_calculatePaintRange(
destSize: destSize.y,
imageSideLen: imageH.toInt(),
- parallax: absoluteParallax.y.round(),
+ parallax: (absoluteOffset + absoluteParallax).y.round(),
displacement: displacement.y.round(),
- ) + // Apply camera top/bottom to range.
+ ) +
(visibleWorldRect.top, visibleWorldRect.bottom);
_paintArea
@@ -116,13 +121,13 @@ class FlameImageLayer extends RenderableLayer {
// particular way that reduces the time spent on computation and clip steps
// in flutter when drawing infinitely across an axis. This method accounts
// for the destination canvas size, camera viewport size, and the exact
- // coverage of the image w.r.t. [parallax].
+ // coverage of the image w.r.t. translation.
// This is achieved by wrapping the rect coordinates around [destSize]
// after calculating the image coverage with [imageSideLen] and adding the
// unseen portion of the image in the span of the wrap range, if any.
//
// The return tuple value is the range where its centroid is the wrap point
- // minus the [displacement] which is the accumulation of all translations
+ // plus the [displacement] which is the accumulation of all translations
// applied to this layer earlier in the render pipeline.
(double min, double max) _calculatePaintRange({
required double destSize,
@@ -143,16 +148,20 @@ class FlameImageLayer extends RenderableLayer {
// Calculate unseen part of image(s).
final unseen = (imageCount - seen) * imageSideLen;
- // Wrap around the target axis w.r.t. parallax.
- final wrapPoint = (displacement + parallax) % (destSize + unseen).ceil();
+ // Wrap around the target axis w.r.t. negative parallax.
+ final coverage = (destSize + unseen).ceil();
+ final wrapPoint = coverage - (parallax % coverage) - 1;
// Partition the _paintArea into two parts.
final part = (wrapPoint / imageSideLen).ceil();
- return (
- wrapPoint - (imageSideLen * part) - displacement,
- wrapPoint + (imageSideLen * (imageCount - part)) - displacement,
- ).toDouble();
+ // Add displacement to the wrap range to account for the
+ // canvas translations pushing the layer further away from
+ // where this wrap is expected to be seen in the viewport.
+ final min = wrapPoint - (imageSideLen * part) + displacement;
+ final max = wrapPoint + (imageSideLen * (imageCount - part)) + displacement;
+
+ return (min.toDouble(), max.toDouble());
}
static Future load({
@@ -186,8 +195,3 @@ extension _PrivRangeTupleDouble on (double, double) {
(double, double) operator +((double, double) other) =>
($1 + other.$1, $2 + other.$2);
}
-
-/// Provides tuples of ints to doubles.
-extension _PrivRangeTupleInt on (int, int) {
- (double, double) toDouble() => ($1.toDouble(), $2.toDouble());
-}
diff --git a/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart b/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
index 406a1591947..437ac57a961 100644
--- a/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
+++ b/packages/flame_tiled/lib/src/renderable_layers/renderable_layer.dart
@@ -156,7 +156,7 @@ abstract class RenderableLayer extends PositionComponent
};
final parentParallax = switch (parent) {
final GroupLayer p => p.absoluteParallax,
- _ => Vector2(1.0, 1.0),
+ _ => Vector2.zero(),
};
final parallaxLocality = Vector2(
layer.parallaxX * parentParallax.x,
@@ -164,7 +164,7 @@ abstract class RenderableLayer extends PositionComponent
);
// Calculate our local parallax.
- double calcParallax(double cam, double parallax) => cam - (cam * parallax);
+ double calcParallax(double cam, double parallax) => cam * parallax;
final localParallax =
Vector2(
calcParallax(cameraX, layer.parallaxX),
@@ -172,15 +172,15 @@ abstract class RenderableLayer extends PositionComponent
) /
zoom;
- absoluteParallax = localParallax + parallaxLocality;
- absoluteOffset = parentOffset + Vector2(offsetX, offsetY);
-
- // Adjustment term for canvas translation below.
+ // Adjustment term for parallax locality.
final delta = switch (parent is GroupLayer) {
- true => parentParallax - localParallax,
+ true => (localParallax + parallaxLocality) - parentParallax,
false => Vector2.zero(),
};
+ absoluteParallax = localParallax + parallaxLocality;
+ absoluteOffset = parentOffset + Vector2(offsetX, offsetY);
+
// Strictly apply local translations in our render step.
//
// Explanation:
@@ -189,8 +189,8 @@ abstract class RenderableLayer extends PositionComponent
// scene graph render by Flame produces the same visuals as painting layers
//using absolute values in a traditional composite renderer.
canvas.translate(
- offsetX + localParallax.x + delta.x,
- offsetY + localParallax.y + delta.y,
+ (cameraX + offsetX) - (localParallax.x + delta.x),
+ (cameraY + offsetY) - (localParallax.y + delta.y),
);
}
From afd3f38a3f39899bf5d16dc285ce9d9294089384 Mon Sep 17 00:00:00 2001
From: TheMaverickProgrammer
<91709+TheMaverickProgrammer@users.noreply.github.com>
Date: Fri, 15 Aug 2025 22:51:09 -0500
Subject: [PATCH 26/28] Parallax effects are now 1:1 with Tiled. WYSIWYG.
---
.../example/assets/images/snow.png | Bin 0 -> 518 bytes
.../flame_tiled/example/assets/tiles/map.tmx | 5 +-
packages/flame_tiled/example/lib/main.dart | 51 +++++++++++++++---
packages/flame_tiled/example/pubspec.yaml | 1 +
.../src/renderable_layers/image_layer.dart | 22 ++++----
.../renderable_layers/renderable_layer.dart | 17 +++---
.../lib/src/renderable_tile_map.dart | 9 +++-
7 files changed, 78 insertions(+), 27 deletions(-)
create mode 100644 packages/flame_tiled/example/assets/images/snow.png
diff --git a/packages/flame_tiled/example/assets/images/snow.png b/packages/flame_tiled/example/assets/images/snow.png
new file mode 100644
index 0000000000000000000000000000000000000000..3cb3cf57b70e5f82d9be7d0e8234138f11beb400
GIT binary patch
literal 518
zcmeAS@N?(olHy`uVBq!ia0vp^4j|0I1|(Ny7TyC=jKx9jP7LeL$-D$|SkfJR9T^xl
z_H+M9WCij$3p^r=85sBugD~Uq{1qucL5ULAh?3y^w370~qEv=}#LT=BJwMkF1yemk
zJy)37&w3&Rt70XRt82O%L|C5p=^+AG#Ht|;!HrcAtMum0FaIX
z;>>myuy_`b4FU;34AKvy(JWgnPbQW5v|x*``-porVU;~Yr>CNq3)`K&vj
z)Nyi!bOG}twwOs*b7!z@<`&ra+lI${2jk4faytv2&)colCNGIPw{wx{*<>y5_VX`1
zug^Gj=wa-w%UWullTPUzfU;L%cUxq7ne*_*YS>ouE`tSpDZH72dzQ!Z^)lrD9x}{%OCyL}s
T&v$VY0(r*M)z4*}Q$iB}aF>cf
literal 0
HcmV?d00001
diff --git a/packages/flame_tiled/example/assets/tiles/map.tmx b/packages/flame_tiled/example/assets/tiles/map.tmx
index 1fae79cffef..ad51fcb1818 100644
--- a/packages/flame_tiled/example/assets/tiles/map.tmx
+++ b/packages/flame_tiled/example/assets/tiles/map.tmx
@@ -1,5 +1,5 @@
-