Skip to content

feat: Add culling support for out of view tiles in TiledComponent - #4124

Merged
ufrshubham merged 11 commits into
mainfrom
devkage/tiled-chunk-culling
Oct 23, 2026
Merged

ufrshubham merged 11 commits into
mainfrom
devkage/tiled-chunk-culling

Conversation

@ufrshubham

@ufrshubham ufrshubham commented Oct 19, 2026 •

Copy link
Copy Markdown
Member

Description

Right now every tile of a Tiled layer is part of a single SpriteBatch, which is drawn on every
frame, even when most of the map is outside of the screen. For big maps this is the main cost of
rendering.

This PR splits a layer into chunks of tiles, each with its own SpriteBatch, and draws only the
chunks that overlap the area the camera sees. It works for all four orientations: orthogonal,
isometric, staggered and hexagonal.

How to use it

  • Infinite maps are split automatically. They are already stored in chunks, and they are the
    maps that get really big.
  • Finite maps are not split unless you ask for it with the new chunkSize option (in tiles per
    side, for example 16). A map that doesn't use it renders exactly as before.
  • Without a camera, everything is drawn, as before.
final map = await TiledComponent.load(
  'map.tmx',
  Vector2.all(16),
  chunkSize: 16,
);

Things to be aware of

  • A layer is only split when this cannot change how it looks. The tiles of the layer need the same
    size as the cells of the map, no tileOffset, and an image tileset. Tiles are drawn in the order
    of the rows, so for tiles that are bigger than a cell, drawing them chunk by chunk could change
    which one is on top. Layers that don't pass this check are drawn as one batch, like before. This is
    the case for the maps with taller tiles.
  • The split assumes that a tile's art stays inside its cell.
  • Rotated tiles (a quarter turn) in maps with non-square cells are not split either, because the
    art does not stay in its cell.
  • tileStack on a tile keeps its chunk drawn, so a tile that is handed out for changes does not
    disappear.

Tests and examples

  • Tests for every orientation compare the culled render with the unsplit one, pixel for pixel, with
    and without a camera. All the maps they use are .tmx files, so they can be opened in Tiled.
  • Two examples, like the ones for CullWhenOffscreen: one shows the speed with culling on and off,
    and one draws the chunk grid. Both have an orientation knob.
  • tile_layer_benchmark.dart benchmarks all orientations.

Demo (Windows)

tiled_culling_visualized_windows.mp4
tiled_culling_visualized_isometric_windows.mp4
tiled_culling_visualized_staggered_windows.mp4
tiled_culling_visualized_hexagonal_windows.mp4

Checklist

  • I have followed the Contributor Guide when preparing my PR.
  • I have updated/added tests for ALL new/updated/fixed functionality.
  • I have updated/added relevant documentation in docs and added dartdoc comments with ///.
  • I have updated/added relevant examples in examples or docs.

Breaking Change?

  • Yes, this PR is a breaking change.
  • No, this PR is not a breaking change.

Related Issues

Orthogonal tile layers of infinite maps are split into chunks, each with its
own SpriteBatch, and only the chunks that touch the view of the camera are
rendered. Finite maps can opt in with the new chunkSize parameter of
TiledComponent.load and the RenderableTiledMap factories.

Chunks are only used when every tile fits in its cell, since tiles that stick
out of their cell would be drawn in another order. A chunk is always rendered
once a transform of one of its tiles was handed out by tileStack, because the
tile can be moved. A map that is rendered without a camera renders everything.

With the atlas, a 496 x 496 tile map takes about 0.15 ms to render instead of
15 ms, and without the atlas about 4 ms instead of 230 ms (debug mode, CPU
time of recording the frame).

Adds a benchmark, tests, and two examples that show the difference.
The Tiled map files that the tests and the benchmark write have attributes
like tilewidth and staggeraxis, which the spell checker does not know.
…d maps

Isometric, staggered and hexagonal layers now use the chunks of the orthogonal
layers too. The chunk size can be set for every orientation, and infinite maps
are split in the chunks that they have in Tiled.

A layer is only split when its tiles are as big as the cells of the map and
have no offset. That used to need square cells, but a cell that is not square,
like the 2:1 cells of an isometric map, is fine as long as no tile is turned a
quarter, since that swaps the width and the height of a tile.

The tiles of the layers of staggered and hexagonal maps that are painted after
their row keep their coordinates in TileTransform, so they end up in the right
chunk.
The maps that the culling tests render are Tiled files in test/assets now, so
that they can be opened in Tiled, instead of being written by the tests.

Adds tests for the layers of isometric, staggered and hexagonal maps. Each
map is rendered with and without chunks, with and without a camera, and the
pixels have to be the same. They also cover maps with cells that are not
square, infinite maps with other chunks and chunks at negative coordinates,
the real dungeon maps, and the maps that have to stay in one batch.
Renders isometric, staggered and hexagonal maps of 208 and 496 tiles per side,
with and without chunks, with and without the atlas.
…mples

The Tiled culling examples have an Orientation knob. They show an orthogonal,
isometric, staggered or hexagonal map of 400 x 400 tiles. The new maps are
terrain made of diamonds and hexagons, as big as the cells of their maps, which
is what lets them be split in chunks.

The visualized example draws the chunks of an isometric map as diamonds, and
the others as the box that Flame compares with the view.
@ufrshubham ufrshubham changed the title feat: Skip the tiles that are out of view in Tiled maps feat: Add culling support for out of view tiles in TiledComponent Oct 19, 2026
@ufrshubham
ufrshubham marked this pull request as ready for review October 21, 2026 15:05
@ufrshubham
ufrshubham requested a balanced review from Copilot October 21, 2026 15:07

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Copilot was unable to review this pull request because the user who requested the review has reached their quota limit.

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

🟡 Changes recommended

Camera-context handling and stale chunk eligibility can hide visible tiles or alter their drawing order.

3 open issues

Review effort: Balanced

Comment thread packages/flame_tiled/lib/src/renderable_layers/tile_layers/tile_layer.dart Outdated
Comment thread packages/flame_tiled/lib/src/renderable_layers/tile_layers/tile_layer.dart Outdated
Comment thread packages/flame_tiled/lib/src/renderable_tile_map.dart Outdated
- A map in the viewport of a camera, or added to the game directly, is
  not rendered through the camera that its view comes from, so it is no
  longer culled.
- A layer is only kept whole because of the tilesets that it uses, not
  because of the other tilesets of the map.
- Whether a layer is split is decided again when its cache is refreshed,
  so a tile that is turned a quarter later keeps the layer whole.

@spydon spydon left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, lgtm!

@ufrshubham
ufrshubham merged commit b9c1c91 into main Oct 23, 2026
8 checks passed
@ufrshubham
ufrshubham deleted the devkage/tiled-chunk-culling branch October 23, 2026 18:29
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants