Skip to content

Commit

Permalink
feat: Adding support for Group layer nesting for RenderableTileMap (#…
Browse files Browse the repository at this point in the history
…1886)

Changes the underling implementation of RenderableTileMap to handle Group layers. Each Group turns into a _RenderableGroupLayer which contains a list of renderable children, forming a tree of renderable layers.

Also, each _RenderableLayer has getter methods for the values which depend on their parent:

    opacity
    offsetX
    offsetY
    parallaxX
    parallaxY
  • Loading branch information
kurtome authored Sep 11, 2022
1 parent 793c843 commit 5ed3454
Show file tree
Hide file tree
Showing 6 changed files with 207 additions and 47 deletions.
177 changes: 138 additions & 39 deletions packages/flame_tiled/lib/src/renderable_tile_map.dart
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
import 'dart:async';
import 'dart:ui' as ui;

import 'package:collection/collection.dart';
import 'package:flame/extensions.dart';
import 'package:flame/flame.dart';
import 'package:flame/game.dart';
Expand Down Expand Up @@ -138,44 +138,76 @@ class RenderableTiledMap {
Vector2 destTileSize, {
Camera? camera,
}) async {
final renderableLayers = await Future.wait(
map.layers.where((layer) => layer.visible).toList().map((layer) {
final renderableLayers =
await _renderableLayers(map.layers, null, map, destTileSize, camera);

return RenderableTiledMap(
map,
renderableLayers,
destTileSize,
camera: camera,
);
}

static Future<List<_RenderableLayer<Layer>>> _renderableLayers(
List<Layer> layers,
_RenderableGroupLayer? parent,
TiledMap map,
Vector2 destTileSize,
Camera? camera,
) async {
return Future.wait(
layers.where((layer) => layer.visible).toList().map((layer) async {
switch (layer.runtimeType) {
case TileLayer:
return _RenderableTileLayer.load(
layer as TileLayer,
parent,
map,
destTileSize,
);
case ImageLayer:
return _RenderableImageLayer.load(layer as ImageLayer, camera);
return _RenderableImageLayer.load(
layer as ImageLayer,
parent,
camera,
);

case Group:
final groupLayer = layer as Group;
final renderableGroup = _RenderableGroupLayer(
groupLayer,
parent,
);
final children = _renderableLayers(
groupLayer.layers,
renderableGroup,
map,
destTileSize,
camera,
);
renderableGroup.children = await children;
return renderableGroup;

default:
return _UnrenderableLayer.load(layer);
}
}),
);

return RenderableTiledMap(
map,
renderableLayers,
destTileSize,
camera: camera,
);
}

/// Handle game resize and propagate it to renderable layers
void handleResize(Vector2 canvasSize) {
renderableLayers.forEach((rl) {
rl.handleResize(canvasSize);
});
for (final layer in renderableLayers) {
layer.handleResize(canvasSize);
}
}

/// Rebuilds the cache for rendering
void _refreshCache() {
renderableLayers.forEach((rl) {
rl.refreshCache();
});
for (final layer in renderableLayers) {
layer.refreshCache();
}
}

/// Renders each renderable layer in the same order specified by the Tiled map
Expand All @@ -184,25 +216,32 @@ class RenderableTiledMap {
c.drawPaint(_backgroundPaint!);
}

// paint each layer in reverse order, because the last layers should be
// Paint each layer in reverse order, because the last layers should be
// rendered beneath the first layers
renderableLayers.where((l) => l.visible).forEach((renderableLayer) {
renderableLayer.render(c, camera);
});
for (final layer in renderableLayers.where((l) => l.visible)) {
layer.render(c, camera);
}
}

/// Returns a layer of type [T] with given [name] from all the layers
/// of this map. If no such layer is found, null is returned.
T? getLayer<T extends Layer>(String name) {
return map.layers
.firstWhereOrNull((layer) => layer is T && layer.name == name) as T?;
try {
// layerByName will searches recursively starting with tiled.dart v0.8.5
return map.layerByName(name) as T;
} on ArgumentError {
return null;
}
}
}

abstract class _RenderableLayer<T extends Layer> {
final T layer;

_RenderableLayer(this.layer);
/// The parent [Group] layer (if it exists)
final _RenderableGroupLayer? parent;

_RenderableLayer(this.layer, this.parent);

bool get visible => layer.visible;

Expand All @@ -212,11 +251,43 @@ abstract class _RenderableLayer<T extends Layer> {

void refreshCache() {}

double get offsetX {
return layer.offsetX + (parent?.offsetX ?? 0);
}

double get offsetY {
return layer.offsetY + (parent?.offsetY ?? 0);
}

double get opacity {
if (parent != null) {
return parent!.opacity * layer.opacity;
} else {
return layer.opacity;
}
}

double get parallaxX {
if (parent != null) {
return parent!.parallaxX * layer.parallaxX;
} else {
return layer.parallaxX;
}
}

double get parallaxY {
if (parent != null) {
return parent!.parallaxY * layer.parallaxY;
} else {
return layer.parallaxY;
}
}

/// Calculates the offset we need to apply to the canvas to compensate for
/// parallax positioning and scroll for the layer and the current camera
/// position
/// https://doc.mapeditor.org/en/latest/manual/layers/#parallax-scrolling-factor
void _applyParallaxOffset(Canvas canvas, Camera camera, Layer layer) {
void _applyParallaxOffset(Canvas canvas, Camera camera) {
final cameraX = camera.position.x;
final cameraY = camera.position.y;
final vpCenterX = camera.viewport.effectiveSize.x / 2;
Expand All @@ -225,15 +296,15 @@ abstract class _RenderableLayer<T extends Layer> {
// 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 - layer.parallaxX) * vpCenterX;
var y = (1 - layer.parallaxY) * vpCenterY;
var x = (1 - parallaxX) * vpCenterX;
var y = (1 - parallaxY) * vpCenterY;
// compensate the offset for zoom
x /= camera.zoom;
y /= camera.zoom;

// Now add the scroll for the current camera position
x += cameraX - (cameraX * layer.parallaxX);
y += cameraY - (cameraY * layer.parallaxY);
x += cameraX - (cameraX * parallaxX);
y += cameraY - (cameraY * parallaxY);

canvas.translate(x, y);
}
Expand All @@ -247,11 +318,12 @@ class _RenderableTileLayer extends _RenderableLayer<TileLayer> {

_RenderableTileLayer(
super.layer,
super.parent,
this._map,
this._destTileSize,
this._cachedSpriteBatches,
) {
_layerPaint.color = Color.fromRGBO(255, 255, 255, layer.opacity);
_layerPaint.color = Color.fromRGBO(255, 255, 255, opacity);
_cacheLayerTiles();
}

Expand Down Expand Up @@ -287,6 +359,7 @@ class _RenderableTileLayer extends _RenderableLayer<TileLayer> {
final tile = _map.tileByGid(tileGid.tile);
final tileset = _map.tilesetByTileGId(tileGid.tile);
final img = tile.image ?? tileset.image;

if (img == null) {
continue;
}
Expand Down Expand Up @@ -487,24 +560,28 @@ class _RenderableTileLayer extends _RenderableLayer<TileLayer> {
void render(Canvas canvas, Camera? camera) {
canvas.save();

canvas.translate(offsetX, offsetY);

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

_cachedSpriteBatches.values.forEach((batch) {
for (final batch in _cachedSpriteBatches.values) {
batch.render(canvas, paint: _layerPaint);
});
}

canvas.restore();
}

static Future<_RenderableLayer> load(
TileLayer layer,
_RenderableGroupLayer? parent,
TiledMap map,
Vector2 destTileSize,
) async {
return _RenderableTileLayer(
layer,
parent,
map,
destTileSize,
await _loadImages(map),
Expand All @@ -530,7 +607,7 @@ class _RenderableImageLayer extends _RenderableLayer<ImageLayer> {
late final ImageRepeat _repeat;
Rect _paintArea = Rect.zero;

_RenderableImageLayer(super.layer, this._image) {
_RenderableImageLayer(super.layer, super.parent, this._image) {
_initImageRepeat();
}

Expand All @@ -543,17 +620,17 @@ class _RenderableImageLayer extends _RenderableLayer<ImageLayer> {
void render(Canvas canvas, Camera? camera) {
canvas.save();

canvas.translate(layer.offsetX, layer.offsetY);
canvas.translate(offsetX, offsetY);

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

paintImage(
canvas: canvas,
rect: _paintArea,
image: _image,
opacity: layer.opacity,
opacity: opacity,
alignment: Alignment.topLeft,
repeat: _repeat,
);
Expand All @@ -575,17 +652,39 @@ class _RenderableImageLayer extends _RenderableLayer<ImageLayer> {

static Future<_RenderableLayer> load(
ImageLayer layer,
_RenderableGroupLayer? parent,
Camera? camera,
) async {
return _RenderableImageLayer(
layer,
parent,
await Flame.images.load(layer.image.source!),
);
}
}

class _RenderableGroupLayer 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;

_RenderableGroupLayer(
super.layer,
super.parent,
);

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

class _UnrenderableLayer extends _RenderableLayer {
_UnrenderableLayer(super.layer);
_UnrenderableLayer(super.layer, super.parent);

@override
void render(Canvas canvas, Camera? camera) {
Expand All @@ -597,7 +696,7 @@ class _UnrenderableLayer extends _RenderableLayer {
bool get visible => false;

static Future<_RenderableLayer> load(Layer layer) async {
return _UnrenderableLayer(layer);
return _UnrenderableLayer(layer, null);
}
}

Expand Down
9 changes: 5 additions & 4 deletions packages/flame_tiled/test/README.md
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
The following test assets were retrieved
from [open game art](https://opengameart.org/content/minimalistic-hexagonal-tilesets-both-orientations).
They were released as Public domain.

The following test assets were retrieved from [open game art](https://opengameart.org/content/minimalistic-hexagonal-tilesets-both-orientations). They were released as Public domain.

* Tileset_Hexagonal_FlatTop_60x39_60x60.png
* Tileset_Hexagonal_PointyTop_60x52_60x80.png
* Tileset_Hexagonal_FlatTop_60x39_60x60.png
* Tileset_Hexagonal_PointyTop_60x52_60x80.png

Binary file added packages/flame_tiled/test/assets/image1.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
14 changes: 12 additions & 2 deletions packages/flame_tiled/test/assets/map.tmx

Large diffs are not rendered by default.

Binary file added packages/flame_tiled/test/goldens/orthogonal.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading

0 comments on commit 5ed3454

Please sign in to comment.