Skip to content
Draft
Show file tree
Hide file tree
Changes from 20 commits
Commits
Show all changes
30 commits
Select commit Hold shift + click to select a range
899e2cc
added a list of paintables to RenderableLayer. Added API to fetch ren…
TheMaverickProgrammer Jul 21, 2025
e8d92fc
fix lint
TheMaverickProgrammer Jul 21, 2025
25d6ae6
make RenderableLayer export in lib
TheMaverickProgrammer Jul 21, 2025
e5a950f
converted RenderableLayer and all inherited classes into proper flame…
TheMaverickProgrammer Jul 21, 2025
b02abb4
render fixes. TODO: expose RenderableLayer correctly as a pubic API.
TheMaverickProgrammer Jul 22, 2025
f26d189
fixing @internal lib conflcit warning
TheMaverickProgrammer Jul 22, 2025
1629935
updated change log
TheMaverickProgrammer Jul 23, 2025
7ec0432
PR draft personal review of code caught typos.
TheMaverickProgrammer Jul 23, 2025
303fba0
object layers draw themselves. Now all layers are added to enderable…
TheMaverickProgrammer Jul 25, 2025
7767b89
fixed all tests except for orthogonal: parallax effect is not being c…
TheMaverickProgrammer Jul 26, 2025
2d36b26
camera needs to be passed into the child components in new version of…
TheMaverickProgrammer Jul 27, 2025
63f2d43
image layer repeat draw behavior now accurate AND optimized
TheMaverickProgrammer Jul 30, 2025
311256e
cleanup and prettify code
TheMaverickProgrammer Jul 30, 2025
b913f32
parallax math almost perfect with what Tiled shows. WYSIWYG.
TheMaverickProgrammer Jul 31, 2025
21b53ff
stable changes tested in other repos. commiting checkpoint before tests.
TheMaverickProgrammer Aug 13, 2025
97c85db
fixed the animation tests. the only remaining test to fix is test_shi…
TheMaverickProgrammer Aug 14, 2025
91795ac
cleanup for review.
TheMaverickProgrammer Aug 14, 2025
41aba50
Merge branch 'main' into feature/layer_component_ordering
TheMaverickProgrammer Aug 14, 2025
991eea6
patch yaml
TheMaverickProgrammer Aug 14, 2025
f807018
satisfy word check linter
TheMaverickProgrammer Aug 14, 2025
a9776ea
melos format. fixed tiled component key test.
TheMaverickProgrammer Aug 14, 2025
d69552a
PR review: elaborate documentation for UnsupportedLayer class.
TheMaverickProgrammer Aug 14, 2025
3dea410
satisfy spell checker
TheMaverickProgrammer Aug 14, 2025
0cf2ea4
new math allows each layer to apply its own offset to the canvas usin…
TheMaverickProgrammer Aug 15, 2025
f257c7f
fixed parallax effect optimization with new layer translation math
TheMaverickProgrammer Aug 15, 2025
bfc9641
Wrap math fixed.
TheMaverickProgrammer Aug 15, 2025
afd3f38
Parallax effects are now 1:1 with Tiled. WYSIWYG.
TheMaverickProgrammer Aug 16, 2025
344c77d
new tests WIP so far
TheMaverickProgrammer Sep 16, 2025
26c409b
Merge branch 'main' into feature/layer_component_ordering
TheMaverickProgrammer Sep 16, 2025
22ce55d
sync with upstream. TODO: update goldens for est_shifted.tmx. TODO: …
TheMaverickProgrammer Sep 16, 2025
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
10 changes: 10 additions & 0 deletions packages/flame_tiled/CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,3 +1,13 @@
## 3.0.6

- **FEAT**: Enable Tiled layers to respect component ordering for overlays and underlays. e.g. Foreground tiles obscure sprites.
- `RenderableLayer` is now a part of the public API.
- `RenderableLayer` is now a `Component` with `HasPaint` and `Position` traits. All render and update methods modified to integrate naturally into the Flame lifecycle.
- `RenderableTiledMap` has method `RenderableLayer? getRenderableLayer(String name)` to return the Flame component by name.
- e.g. `mapComponent.tileMap.getRenderableLayer('Ground')`
- Expanded the example map to be larger and placed coins beneath one of the layers to demonstrate this effect.
- Adjusted the camera move effect to better show-case this example map as the previous one poorly scrolled too far away.

## 3.0.5

- Update a dependency to the latest release.
Expand Down
51 changes: 42 additions & 9 deletions packages/flame_tiled/example/assets/tiles/map.tmx

Large diffs are not rendered by default.

10 changes: 7 additions & 3 deletions packages/flame_tiled/example/lib/main.dart
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@ class TiledGame extends FlameGame {
..anchor = Anchor.topLeft
..add(
MoveToEffect(
Vector2(1000, 0),
Vector2(320, 180),
EffectController(
duration: 10,
alternate: true,
Expand All @@ -37,19 +37,23 @@ 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<ObjectGroup>(
'AnimatedCoins',
);
final coins = await Flame.images.load('coins.png');

// Add sprites behind the ground decoration layer.
final groundLayer = mapComponent.tileMap.getRenderableLayer('Ground');

// We are 100% sure that an object layer named `AnimatedCoins`
// exists in the example `map.tmx`.
for (final object in objectGroup!.objects) {
world.add(
groundLayer?.add(
SpriteAnimationComponent(
size: Vector2.all(20.0),
anchor: Anchor.center,
position: Vector2(object.x, object.y),
animation: SpriteAnimation.fromFrameData(
coins,
Expand Down
1 change: 1 addition & 0 deletions packages/flame_tiled/lib/flame_tiled.dart
Original file line number Diff line number Diff line change
Expand Up @@ -4,6 +4,7 @@ export 'package:tiled/tiled.dart';

export 'src/extensions.dart';
export 'src/flame_tsx_provider.dart';
export 'src/renderable_layers/renderable_layer.dart';
export 'src/renderable_tile_map.dart';
export 'src/simple_flips.dart';
export 'src/tile_atlas.dart';
Expand Down
34 changes: 4 additions & 30 deletions packages/flame_tiled/lib/src/renderable_layers/group_layer.dart
Original file line number Diff line number Diff line change
@@ -1,50 +1,24 @@
import 'package:flame/components.dart';
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';

@internal
class GroupLayer extends RenderableLayer<Group> {
/// The child layers of this [Group] to be rendered recursively.
///
/// NOTE: This is set externally instead of via constructor params because
/// there are cyclic dependencies when loading the renderable layers.
late final List<RenderableLayer> children;

GroupLayer({
required super.layer,
required super.parent,
required super.camera,
required super.map,
required super.destTileSize,
required super.layerPaintFactory,
super.filterQuality,
});

@override
void refreshCache() {
for (final child in children) {
final childLayers = children.whereType<RenderableLayer>();
for (final child in childLayers) {
child.refreshCache();
}
}

@override
void handleResize(Vector2 canvasSize) {
for (final child in children) {
child.handleResize(canvasSize);
}
}

@override
void render(Canvas canvas, CameraComponent? camera) {
for (final child in children) {
child.render(canvas, camera);
}
}

@override
void update(double dt) {
for (final child in children) {
child.update(dt);
}
}
}
116 changes: 83 additions & 33 deletions packages/flame_tiled/lib/src/renderable_layers/image_layer.dart
Original file line number Diff line number Diff line change
Expand Up @@ -15,36 +15,30 @@ class FlameImageLayer extends RenderableLayer<ImageLayer> {
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,
required super.parent,
required super.camera,
required super.map,
required super.destTileSize,
required Image image,
required super.layerPaintFactory,
super.filterQuality,
}) : _image = image {
_initImageRepeat();
}

@override
void handleResize(Vector2 canvasSize) {
void onGameResize(Vector2 canvasSize) {
super.onGameResize(canvasSize);
_canvasSize.setFrom(canvasSize);
}

@override
void render(Canvas canvas, CameraComponent? camera) {
void render(Canvas canvas) {
canvas.save();

canvas.translate(offsetX, offsetY);

if (camera != null) {
applyParallaxOffset(canvas, camera);
}

_resizePaintArea(camera);

paintImage(
canvas: canvas,
rect: _paintArea,
Expand All @@ -59,40 +53,48 @@ class FlameImageLayer extends RenderableLayer<ImageLayer> {
}

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.abs() + camera.viewfinder.position.x.abs() * parallaxX;
_maxTranslation.y =
offsetY.abs() + camera.viewfinder.position.y.abs() * parallaxY;
} else {
_maxTranslation.x = offsetX.abs();
_maxTranslation.y = offsetY.abs();
}
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.

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(
destSize: destSize.x,
imageSideLen: imageW,
layerOffset: cachedLayerOffset.x,
) + // Apply camera left/right to range.
(visibleWorldRect.left, visibleWorldRect.right);

_paintArea
..left = left
..right = right;
} 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(
destSize: destSize.y,
imageSideLen: imageH,
layerOffset: cachedLayerOffset.y,
) + // Apply camera top/bottom to range.
(visibleWorldRect.top, visibleWorldRect.bottom);

_paintArea
..top = top
..bottom = bottom;
} else {
// Simply draw the full height of the image.
_paintArea.top = 0;
_paintArea.bottom = _canvasSize.y;
_paintArea.bottom = imageH;
}
}

Expand All @@ -108,28 +110,76 @@ class FlameImageLayer extends RenderableLayer<ImageLayer> {
}
}

// 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.
//
// The return tuple value is the range where its centroid is the wrap point
// 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 destSize,
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;

// 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 = layerOffset.ceil() % (destSize + unseen).toInt();

// Partition the _paintArea into two parts.
final part = (wrapPoint / imageSideLen).ceil();

return (
wrapPoint - (imageSideLen * part) - layerOffset,
wrapPoint + (imageSideLen * (imageCount - part)) - layerOffset,
);
}

static Future<FlameImageLayer> load({
required ImageLayer layer,
required GroupLayer? parent,
required CameraComponent? camera,
required TiledMap map,
required Vector2 destTileSize,
required Paint Function(double opacity) layerPaintFactory,
FilterQuality? filterQuality,
Images? images,
}) async {
return FlameImageLayer(
layer: layer,
parent: parent,
camera: camera,
map: map,
destTileSize: destTileSize,
filterQuality: filterQuality,
layerPaintFactory: layerPaintFactory,
image: await (images ?? Flame.images).load(layer.image.source!),
);
}

@override
void refreshCache() {}
}

@override
void update(double dt) {}
/// Provide tuples with addition.
extension _PrivRangeTupleHelper on (double, double) {
(double, double) operator +((double, double) other) =>
($1 + other.$1, $2 + other.$2);
}
25 changes: 8 additions & 17 deletions packages/flame_tiled/lib/src/renderable_layers/object_layer.dart
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
import 'dart:ui';

import 'package:flame/components.dart';
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';
Expand All @@ -11,41 +10,33 @@ class ObjectLayer extends RenderableLayer<ObjectGroup> {
ObjectLayer({
required super.layer,
required super.parent,
required super.camera,
required super.map,
required super.destTileSize,
required super.layerPaintFactory,
super.filterQuality,
});

@override
void render(Canvas canvas, CameraComponent? camera) {
// nothing to do
}

// ignore non-renderable layers when looping over the layers to render
@override
bool get visible => false;

static Future<ObjectLayer> load(
ObjectGroup layer,
Component? parent,
CameraComponent? camera,
TiledMap map,
Vector2 destTileSize,
Paint Function(double opacity) layerPaintFactory,
FilterQuality? filterQuality,
) async {
return ObjectLayer(
layer: layer,
parent: null,
parent: parent,
camera: camera,
map: map,
destTileSize: destTileSize,
layerPaintFactory: layerPaintFactory,
filterQuality: filterQuality,
);
}

@override
void handleResize(Vector2 canvasSize) {}

@override
void refreshCache() {}

@override
void update(double dt) {}
}
Loading
Loading