Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

feat: Add support for isometric staggered maps #1895

Merged
merged 11 commits into from
Sep 13, 2022
7 changes: 7 additions & 0 deletions packages/flame/lib/src/sprite_batch.dart
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,9 @@ class SpriteBatch {
/// Whether to use [Canvas.drawAtlas] or not.
final bool useAtlas;

/// Does this batch contain any operatiosn?
bool get isEmpty => _batchItems.isEmpty;

Future<void> _makeFlippedAtlas() async {
_hasFlips = true;
_atlasReady = false;
Expand Down Expand Up @@ -353,6 +356,10 @@ class SpriteBatch {
Rect? cullRect,
Paint? paint,
}) {
if (isEmpty) {
return;
}

paint ??= _emptyPaint;

if (useAtlas && _atlasReady) {
Expand Down
235 changes: 208 additions & 27 deletions packages/flame_tiled/lib/src/renderable_tile_map.dart
Original file line number Diff line number Diff line change
Expand Up @@ -324,7 +324,6 @@ class _RenderableTileLayer extends _RenderableLayer<TileLayer> {
this._cachedSpriteBatches,
) {
_layerPaint.color = Color.fromRGBO(255, 255, 255, opacity);
_cacheLayerTiles();
}

@override
Expand All @@ -333,12 +332,27 @@ class _RenderableTileLayer extends _RenderableLayer<TileLayer> {
}

void _cacheLayerTiles() {
if (_map.orientation == MapOrientation.isometric) {
_cacheIsometricTiles();
} else if (_map.orientation == MapOrientation.hexagonal) {
_cacheHexagonalTiles();
} else {
_cacheOrthogonalLayerTiles();
for (final batch in _cachedSpriteBatches.values) {
batch.clear();
}

if (_map.orientation == null) {
return;
}

switch (_map.orientation!) {
case MapOrientation.isometric:
_cacheIsometricTiles();
break;
case MapOrientation.staggered:
_cacheIsometricStaggeredTiles();
break;
case MapOrientation.hexagonal:
_cacheHexagonalTiles();
break;
case MapOrientation.orthogonal:
_cacheOrthogonalLayerTiles();
break;
}
}

Expand Down Expand Up @@ -454,7 +468,7 @@ class _RenderableTileLayer extends _RenderableLayer<TileLayer> {
}
}

void _cacheHexagonalTiles() {
void _cacheIsometricStaggeredTiles() {
jtmcdole marked this conversation as resolved.
Show resolved Hide resolved
final tileData = layer.tileData!;
final batchMap = _cachedSpriteBatches;
final halfDestinationTile = _destTileSize / 2;
Expand All @@ -463,16 +477,134 @@ class _RenderableTileLayer extends _RenderableLayer<TileLayer> {

var staggerY = 0.0;
var staggerX = 0.0;
if (_map.orientation == MapOrientation.hexagonal) {
// Hexagonal Ponity Tiles move down by a fractional amount.
// Hexagonal Ponity Tiles move down by a fractional amount.
if (_map.staggerAxis == StaggerAxis.y) {
staggerY = size.y * 0.5;
} else
// Hexagonal Flat Tiles move right by a fractional amount.
if (_map.staggerAxis == StaggerAxis.x) {
staggerX = size.x * 0.5;
}

for (var ty = 0; ty < tileData.length; ty++) {
final tileRow = tileData[ty];

// Hexagonal Pointy Tiles shift left and right depending on the row
if (_map.staggerAxis == StaggerAxis.y) {
staggerY = _map.tileHeight * 0.75;
if ((ty.isOdd && _map.staggerIndex == StaggerIndex.odd) ||
(ty.isEven && _map.staggerIndex == StaggerIndex.even)) {
staggerX = halfDestinationTile.x;
} else {
staggerX = 0.0;
}
}

// When staggering in the X axis, we need to hold painting of "lower"
// tiles (those with staggerY adjustments) otherwise they'll just get
// painted over. See the second pass loop after tx.
final xSecondPass = <_Transform>[];

for (var tx = 0; tx < tileRow.length; tx++) {
final tileGid = tileRow[tx];
if (tileGid.tile == 0) {
continue;
}

final tile = _map.tileByGid(tileGid.tile);
jtmcdole marked this conversation as resolved.
Show resolved Hide resolved
final tileset = _map.tilesetByTileGId(tileGid.tile);
final img = tile.image ?? tileset.image;
if (img == null) {
continue;
}

final batch = batchMap[img.source];
if (batch == null) {
continue;
}

// Tiles shift up and down as we move across the row.
if (_map.staggerAxis == StaggerAxis.x) {
if ((tx.isOdd && _map.staggerIndex == StaggerIndex.odd) ||
(tx.isEven && _map.staggerIndex == StaggerIndex.even)) {
staggerY = halfDestinationTile.y;
} else {
staggerY = 0.0;
}
}

final src = tileset.computeDrawRect(tile).toRect();
final flips = SimpleFlips.fromFlips(tileGid.flips);
final scale = size.x / src.width;
final anchorX = src.width - halfMapTile.x;
final anchorY = src.height - halfMapTile.y;

late double offsetX;
late double offsetY;

// halfTile.x: shifts the map half a tile forward rather than
// lining up on at the center.
// halfTile.y: shifts the map half a tile down rather than
// lining up on at the center.
// StaggerX/Y: Moves the tile forward/down depending on orientation.
// * stagger: Isometric tiles move down or right by only a fraction,
// specifically 1/2 the width or height, for packing.
if (_map.staggerAxis == StaggerAxis.y) {
offsetX = tx * size.x + staggerX + halfDestinationTile.x;
offsetY = ty * staggerY + halfDestinationTile.y;
} else {
offsetX = tx * staggerX + halfDestinationTile.x;
offsetY = ty * size.y + staggerY + halfDestinationTile.y;
}

final scos = flips.cos * scale;
final ssin = flips.sin * scale;

final transform = ui.RSTransform(
scos,
ssin,
offsetX + -scos * anchorX + ssin * anchorY,
offsetY + -ssin * anchorX - scos * anchorY,
);

// A second pass is only needed in the case of staggery.
if (_map.staggerAxis == StaggerAxis.x && staggerY > 0) {
xSecondPass.add(_Transform(src, transform, flips.flip, batch));
} else {
batch.addTransform(
source: src,
transform: transform,
flip: flips.flip,
);
}
}
// Hexagonal Flat Tiles move right by a fractional amount.
if (_map.staggerAxis == StaggerAxis.x) {
staggerX = _map.tileWidth * 0.75;

for (final tile in xSecondPass) {
tile.batch.addTransform(
source: tile.source,
transform: tile.transform,
flip: tile.flip,
);
}
}
}

void _cacheHexagonalTiles() {
final tileData = layer.tileData!;
final batchMap = _cachedSpriteBatches;
final halfDestinationTile = _destTileSize / 2;
final size = _destTileSize;
final halfMapTile = Vector2(_map.tileWidth / 2, _map.tileHeight / 2);

var staggerY = 0.0;
var staggerX = 0.0;
// Hexagonal Ponity Tiles move down by a fractional amount.
if (_map.staggerAxis == StaggerAxis.y) {
staggerY = size.y * 0.75;
} else
// Hexagonal Flat Tiles move right by a fractional amount.
if (_map.staggerAxis == StaggerAxis.x) {
staggerX = size.x * 0.75;
}

for (var ty = 0; ty < tileData.length; ty++) {
final tileRow = tileData[ty];
Expand All @@ -487,6 +619,11 @@ class _RenderableTileLayer extends _RenderableLayer<TileLayer> {
}
}

// When staggering in the X axis, we need to hold painting of "lower"
// tiles (those with staggerY adjustments) otherwise they'll just get
// painted over. See the second pass loop after tx.
final xSecondPass = <_Transform>[];

for (var tx = 0; tx < tileRow.length; tx++) {
final tileGid = tileRow[tx];
if (tileGid.tile == 0) {
Expand Down Expand Up @@ -524,33 +661,48 @@ class _RenderableTileLayer extends _RenderableLayer<TileLayer> {
late double offsetX;
late double offsetY;

// halfTile.x: shfits the map half a tile forward rather than
// halfTile.x: shifts the map half a tile forward rather than
// lining up on at the center.
// halfTile.y: shfits the map half a tile down rather than
// halfTile.y: shifts the map half a tile down rather than
// lining up on at the center.
// StaggerX/Y: Moves the tile forward/down depending on orientation.
// * stagger: Hexagonal tiles move down or right by only a fraction,
// specifically 3/4 the width or height, for packing.
if (_map.staggerAxis == StaggerAxis.y) {
offsetX = tx * _map.tileWidth + staggerX + halfDestinationTile.x;
jtmcdole marked this conversation as resolved.
Show resolved Hide resolved
offsetX = tx * size.x + staggerX + halfDestinationTile.x;
offsetY = ty * staggerY + halfDestinationTile.y;
} else {
offsetX = tx * staggerX + halfDestinationTile.x;
offsetY = ty * _map.tileHeight + staggerY + halfDestinationTile.y;
offsetY = ty * size.y + staggerY + halfDestinationTile.y;
}

final scos = flips.cos * scale;
final ssin = flips.sin * scale;

batch.addTransform(
source: src,
transform: ui.RSTransform(
scos,
ssin,
offsetX + -scos * anchorX + ssin * anchorY,
offsetY + -ssin * anchorX - scos * anchorY,
),
flip: flips.flip,
final transform = ui.RSTransform(
scos,
ssin,
offsetX + -scos * anchorX + ssin * anchorY,
offsetY + -ssin * anchorX - scos * anchorY,
);

// A second pass is only needed in the case of staggery.
if (_map.staggerAxis == StaggerAxis.x && staggerY > 0) {
xSecondPass.add(_Transform(src, transform, flips.flip, batch));
spydon marked this conversation as resolved.
Show resolved Hide resolved
} else {
batch.addTransform(
source: src,
transform: transform,
flip: flips.flip,
);
}
}

for (final tile in xSecondPass) {
tile.batch.addTransform(
source: tile.source,
transform: tile.transform,
flip: tile.flip,
);
}
}
Expand Down Expand Up @@ -675,6 +827,20 @@ class _RenderableGroupLayer extends _RenderableLayer<Group> {
super.parent,
);

@override
void refreshCache() {
for (final child in children) {
child.refreshCache();
}
}

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

@override
void render(ui.Canvas canvas, Camera? camera) {
for (final child in children) {
Expand Down Expand Up @@ -715,3 +881,18 @@ Color? _parseTiledColor(String? tiledColor) {
return null;
}
}

/// Caches transforms for staggered maps as the row/col are switched.
class _Transform {
final Rect source;
final ui.RSTransform transform;
final bool flip;
final SpriteBatch batch;

_Transform(
this.source,
this.transform,
this.flip,
this.batch,
);
}
53 changes: 31 additions & 22 deletions packages/flame_tiled/lib/src/tiled_component.dart
Original file line number Diff line number Diff line change
Expand Up @@ -63,35 +63,44 @@ class TiledComponent<T extends FlameGame> extends Component with HasGameRef<T> {
final xScale = tileMap.destTileSize.x / tMap.tileWidth;
final yScale = tileMap.destTileSize.y / tMap.tileHeight;

late Vector2 size;

final tileScaled = Vector2(
tileMap.map.tileWidth * xScale,
tileMap.map.tileHeight * yScale,
);

if (tMap.orientation == MapOrientation.hexagonal) {
if (tMap.staggerAxis == StaggerAxis.y) {
size = Vector2(
tileMap.map.width * tileScaled.x,
tileScaled.y + ((tileMap.map.height - 1) * tileScaled.y * 0.75),
);
} else {
size = Vector2(
tileScaled.x + ((tileMap.map.width - 1) * tileScaled.x * 0.75),
(tileMap.map.height * tileScaled.y) + tileScaled.y / 2,
);
}
} else {
size = Vector2(
tileMap.map.width * tileScaled.x,
tileMap.map.height * tileScaled.y,
);
if (tMap.orientation == null) {
return Vector2.zero();
}

if (tMap.staggerAxis == StaggerAxis.y) {
size.x += tMap.tileWidth / 2;
switch (tMap.orientation!) {
case MapOrientation.staggered:
return tMap.staggerAxis == StaggerAxis.y
? Vector2(
tileScaled.x * tileMap.map.width + tileScaled.x / 2,
tileScaled.y + ((tileMap.map.height - 1) * tileScaled.y / 2),
)
: Vector2(
tileScaled.x + ((tileMap.map.width - 1) * tileScaled.x / 2),
tileScaled.y * tileMap.map.height + tileScaled.y / 2,
);

case MapOrientation.hexagonal:
return tMap.staggerAxis == StaggerAxis.y
? Vector2(
tileMap.map.width * tileScaled.x + tileScaled.x / 2,
tileScaled.y + ((tileMap.map.height - 1) * tileScaled.y * 0.75),
)
: Vector2(
tileScaled.x + ((tileMap.map.width - 1) * tileScaled.x * 0.75),
(tileMap.map.height * tileScaled.y) + tileScaled.y / 2,
);

case MapOrientation.isometric:
case MapOrientation.orthogonal:
return Vector2(
tileMap.map.width * tileScaled.x,
tileMap.map.height * tileScaled.y,
);
}
return size;
}
}
Binary file added packages/flame_tiled/test/assets/dirt_atlas.png
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.
Loading