Ever wondered how you can create overlayed, auto-tileable, multi-layered, viewport-friendly, serializable, performant, and customizable tilemaps in libGDX with minimal effort using the dual-grid system?
The dual-grid system is a mechanism similar to the marching squares algorithm. It allows tile textures to be displayed based on their four cornering neighbors, using a 4-bit bitmask.
With traditional auto-tiling, you’d need 256 different tile combinations to achieve smooth edges and corners using an 8-neighbor system. However, with the dual-grid approach, you only need 16 tile combinations, significantly reducing the amount of artwork required while still achieving a polished result.
The first time I saw the dual-grid system was in an awesome dev-log by jess::codes. She is an incredibly talented game developer!
This library was largely inspired by these two videos:
Additionally, most assets in this repository are taken from her demos:
Thanks jess!
Each TileLayer
can have an optional overlay shader to render an overlay texture on top of the color mask
of the tile-set (default: red). This helps prevent repetition and allows drawing larger details that span multiple
tiles.
Each TileLayer
uses a bitmask system to automatically determine the correct tile variant based on its four
cornering neighbors. This means you don’t have to manually place tiles—the system will handle it for you!
Here's a little showcase of the example included (press to play the video):
Showcase.mp4
The library allows you to stack multiple tile layers, enabling independent auto-tiling for different tile types within the same map.
The example is managed using a viewport system, meaning that you work with world units instead of pixels. This ensures scalability across different screen sizes and resolutions.
For example, each tile in the example is 1x1 world units, making the system highly adaptable.
A viewport defines and manages the coordinate system of your game world, making your game more portable across platforms.
Every viewport manages a camera which may be freely positioned within the world, the camera is often what's visible on the screen.
This library consists of a single, configurable class: TileLayer
.
You can create a new TileLayer
instance using:
new TileLayer(
int tilesX, // World width in tiles
int tilesY, // World height in tiles
float tileWidth, // Tile width
float tileHeight, // Tile height
float unitScale, // Unit scale of the world
boolean fill // Should the layer start filled?
);
Once you have an instance, set the tile texture:
tileLayer.setTileSet(TextureRegion);
You can integrate an overlay texture and an overlay shader using:
tileLayer.setOverlay(Texture, ShaderProgram);
In your render pipeline:
tileLayer.setView(OrthographicCamera); // Set view bounds to the camera
tileLayer.render(Batch); // Render using a batch
You may also use the overloaded method setView(x, y, w, h)
if you don't have a camera.
If you experience texture bleeding, adjust the inset tolerance:
TileLayer#setInsetTolerance(float, float);
This allows you to handle texture bleeding properly at runtime, without ever modifying your texture.
Suggested values:
0.001
(for large textures, e.g.,>= 4096px
)0.05
(for small textures, e.g.,<= 16px
)
If your tile-set layout differs from the default, you can set a custom auto-tile configuration:
TileLayer#setAutoTileConfiguration(IntMap<Byte>);
Note that this is currently a static property, meaning it applies to all tile layers.
You may experiment with different IRenderStrategy
implementations for your tilemap, there are 4
rendering strategies integrated as of now:
RenderStrategy.ALL_TILES_ALL_QUADS
will render all tiles and all quads.RenderStrategy.ALL_TILES_VIEW_QUADS
will render all tiles but only visible quads.RenderStrategy.VIEW_TILES_ALL_QUADS
will render visible tiles but all quads.- (default)
RenderStrategy.VIEW_TILES_VIEW_QUADS
will render visible tiles and only visible quads.
Invisible quads are the ones associated with bitmask 0 in the auto-tile configuration.
You may also provide your own implementation of the IRenderStrategy
interface.
You may change the current tile layer rendering strategy like this:
tileLayer.setRenderStrategy(IRenderStrategy);
In case you want to serialize your tile layers, the TileLayer
class offers a couple of
convenient and efficient static methods that simplify the serialization process for you
using the UBJson
file format.
You may write your tile layer to a file handle or an output stream like this:
TileLayer#write(TileLayer, FileHandle); // write to a file handle.
TileLayer#write(TileLayer, OutputStream); // write to an output stream.
And you may read your tile layer from a file handle or an input stream like this:
TileLayer#read(FileHandle); // read from a file handle.
TileLayer#read(OutputStream); // read from an input stream.
You may also choose your desired compression strategy when serializing a tile layer. Either set the default compression strategy using
TileLayer#setDefaultCompressionStrategy(ICompressionStrategy);
or set it per tile layer, like so:
tileLayer.setCompressionStrategy(ICompressionStrategy);
Either way, you have access to 3 implemented compression strategies in this library:
Strategy | Suggestion | Bits per tile (Worst case) |
---|---|---|
BIT_COMPRESSION (raw) | Good for large layers with many randomly placed tiles. |
1 Bit |
SPARSE_COMPRESSION | Good for small layers with little tiles placed. |
32 Bit |
RUN_LENGTH_COMPRESSION | Good for layers of all sizes with row-placed tiles. |
~17 Bit |
These implementations can be found in the TileLayer.CompressionStrategy
enum. This library chooses the
RUN_LENGTH_COMPRESSION
strategy as the default compression strategy.
Well, here are some simple non-realistic benchmark metrics: How much space does it take to save different sizes of a tile layer?
Compression | Type | Tiles | Bytes |
---|---|---|---|
BIT_COMPRESSION | Empty | 4,096 | 665 |
Empty | 16,384 | 2,201 | |
Empty | 65,536 | 8,345 | |
Empty | 262,144 | 32,921 | |
Empty | 1,048,576 | 131,225 | |
Empty | 4,194,304 | 524,441 | |
Empty | 16,777,216 | 2,097,305 | |
SPARSE_COMPRESSION | Empty | 4,096 | 153 |
Empty | 16,384 | 153 | |
Empty | 65,536 | 153 | |
Empty | 262,144 | 153 | |
Empty | 1,048,576 | 153 | |
Empty | 4,194,304 | 153 | |
Empty | 16,777,216 | 153 | |
RUN_LENGTH_COMPRESSION | Empty | 4,096 | 156 |
Empty | 16,384 | 156 | |
Empty | 65,536 | 156 | |
Empty | 262,144 | 156 | |
Empty | 1,048,576 | 156 | |
Empty | 4,194,304 | 156 | |
Empty | 16,777,216 | 156 |
You may also provide your own implementation of the ICompressionStrategy
interface.
Note: If you serialize using your own compression, you'll have to set the custom compression strategy supplier before
reading your map, you may do that using
the TileLayer#setCustomCompressionStrategySupplier(Supplier<ICompressionStrategy>);
method.
This repository contains both the library and an example project:
-
Library (
:library
Gradle submodule)- Contains everything related to the
TileLayer
class. - Located in the
library
folder. - This is the core functionality meant for integration into your own projects.
- Contains everything related to the
-
Example (
:example
Gradle submodule)- A demonstration of how to use
TileLayer
. - Located in the
example
folder. - Provides a working implementation showcasing auto-tiling, overlays, and viewport management.
- A demonstration of how to use
Clone the repository and run:
./gradlew clean :example:run
To build the library JAR file:
./gradlew clean :library:jar
Alternatively, since the entire library is a single class, you can simply copy
TileLayer.java
into your project.
I will do my best to maintain this library, fix bugs, and possibly add new features and optimizations.
Pull requests are highly welcome!
Currently, TileLayer
is well-optimized and can efficiently handle large worlds.
This library (excluding example assets) is public domain.
Everything inside the library
folder is free to use without restrictions.