How to use a 16-tile Wang tileset in Unity: RuleTile and the dual grid
- A 16-tile corner set has one tile per combination of four corners. The tile's index is its corner mask: NW=1, NE=2, SW=4, SE=8.
- Import: Sprite (2D and UI), Multiple, 16 Pixels Per Unit, Point (no filter), Compression None. Slice a 16×16 grid with Keep Empty Rects on.
- Two ways to paint: a RuleTile from 2D Tilemap Extras with 15 corner rules, or a dual grid: a Tilemap offset by half a cell, filled by a short C# component from a bool grid.
A Wang tileset for one terrain on empty ground (snow on nothing, path on grass) is sixteen tiles, one for every way the four corners of a tile can be filled. This tutorial puts one into Unity twice: with a RuleTile from the 2D Tilemap Extras package, then with the dual-grid approach many 2D games use instead. Menu paths and property names follow the current Unity Manual and the 2D Tilemap Extras 6.0 docs.
To follow along, grab SpriteForge's free Snow Starter Pack on itch.io. You need snow_autotile.png, a 64×64 sheet of sixteen 16×16 tiles, and snow_autotile.json, which lists each tile's rect and corners.
The set: one tile per corner combination
Each tile is split into four corners, and each corner is either snow or empty. Give the corners the values 1, 2, 4 and 8, add up the snowy ones, and you get a number from 0 to 15. That number is the tile's corner mask, and in this sheet it is also the tile's position: the tiles are laid out in mask order, left to right, top to bottom.
So tile m sits in column m % 4, row m / 4, counted from the top left. The JSON says the same thing in data form, with y measured from the top of the sheet:
{ "mask": 9, "x": 16, "y": 32, "w": 16, "h": 16,
"corners": { "NW": true, "NE": false, "SW": false, "SE": true } }
This is the dual-grid convention described in why AI tilesets don't tile: the corners of a tile are the points between map cells, which is why the snow pattern can carry across tile edges instead of repeating as a stamp.
1. Import settings
Drop snow_autotile.png into your Assets folder, select it, and set these in the Inspector:
| Setting | Value and why |
|---|---|
| Texture Type | Sprite (2D and UI) |
| Sprite Mode | Multiple, because the sheet holds sixteen sprites |
| Pixels Per Unit | 16, so one tile is one world unit, the size of a default Grid cell |
| Filter Mode | Point (no filter), which keeps pixels hard |
| Compression | None (in the Default platform tab), so no block artifacts or colour shifts |
Click Apply.
2. Slice the sheet
With the texture selected, click Sprite Editor (part of the 2D Sprite package, which the 2D templates include). Open the Slice dropdown, set Type to Grid By Cell Size and Pixel Size to 16×16, and tick Keep Empty Rects. Tile 0 is fully transparent, and without that option Unity drops it, so the sheet would no longer have sixteen sprites. Click Slice, then Apply.
The JSON's rects are exactly this grid, so there is nothing else to import from it. Use it as the answer key: click a few sprites and check that the one you'll treat as tile m sits at column m % 4, row m / 4 from the top left.
3. Tiles, a palette and a Tilemap
- Create a palette with Assets → Create → 2D → Tile Palette (Rectangular), and open Window → 2D → Tile Palette.
- Drag the sliced texture into the Tile Palette window. For a Multiple sprite, Unity creates one tile asset per sprite and asks where to save them.
- In the Hierarchy, right-click and choose 2D Object → Tilemap → Rectangular. You get a Grid with a child Tilemap.
You can paint the sixteen tiles by hand now, but the point of the set is that you shouldn't have to.
4. A corner-based RuleTile
Install 2D Tilemap Extras (Window → Package Manager → Unity Registry), then right-click in the Project window and choose Create → 2D → Tiles → Rule Tile. Set its Default Sprite to tile 0.
A RuleTile looks at the 3×3 neighbourhood of the cell it's painted in. A green arrow passes if that neighbour is this same Rule Tile, an empty square ignores the neighbour, and Unity checks the rules from the top, using the first one whose arrows all pass. So define a corner as snow when the three neighbours that share it are painted too:
- NW corner: arrows on up, left and up-left
- NE corner: up, right and up-right
- SW corner: down, left and down-left
- SE corner: down, right and down-right
Add one rule per tile from 1 to 15 with the arrows of its snowy corners (leave every other square empty, Output Single, Sprite = that tile), in this order:
| Order | Rules for tiles |
|---|---|
| 1st | 15 (all eight arrows) |
| 2nd | 7, 11, 13, 14 (three corners) |
| 3rd | 3, 5, 6, 9, 10, 12 (two corners) |
| 4th | 1, 2, 4, 8 (one corner) |
The order is what makes it correct. A rule with empty squares also passes when more corners are snowy than it asks for, so the rules with more corners have to come first. Within a group the order doesn't matter, and if no corner is snowy, the Default Sprite (tile 0) is used. Drag the Rule Tile into your palette and paint.
The trade-off: the snow is drawn inside the cells you paint, so its edge sits half a cell in from where you painted, and an area must be at least two cells wide to show anything. A single painted cell or a one-cell line comes out as tile 0. If that's a problem, use the dual grid below.
(The 6.0 version of the package also has an Auto Tile with a Mask_2x2 type, which covers 16 floor layouts and is set up by drawing masks over the texture. This post sticks to RuleTile, which works on older versions too.)
5. The dual-grid alternative
The dual grid separates data from pictures. You keep a plain bool[,] of which cells are snow, and a display Tilemap moved down and left by half a cell. Every display cell then sits on a corner where four data cells meet, and it shows the tile whose mask those four cells make. Nothing is guessed, single cells and one-cell lines work, and it's exactly how SpriteForge composes its own previews.
Scene setup: under the Grid, add a second Tilemap named Display and set its Transform Position to (-0.5, -0.5, 0), half a cell at 16 PPU. Add this component to any GameObject, assign Display, and fill the 16-element array with the tile assets in order, element i = tile i (tile 0 included):
using UnityEngine;
using UnityEngine.Tilemaps;
public class DualGridTilemap : MonoBehaviour
{
[SerializeField] Tilemap display; // Position (-0.5, -0.5, 0)
[SerializeField] TileBase[] tilesByMask = new TileBase[16]; // element i = tile i
bool[,] snow; // snow[x, y]: the data cells, y up
public void Build(bool[,] cells)
{
snow = cells;
display.ClearAllTiles();
for (int x = 0; x <= cells.GetLength(0); x++)
for (int y = 0; y <= cells.GetLength(1); y++)
UpdateDisplayCell(x, y);
}
// Change one data cell and fix the four display cells it touches.
public void Set(int x, int y, bool on)
{
snow[x, y] = on;
UpdateDisplayCell(x, y);
UpdateDisplayCell(x + 1, y);
UpdateDisplayCell(x, y + 1);
UpdateDisplayCell(x + 1, y + 1);
}
void UpdateDisplayCell(int x, int y)
{
int mask = (Has(x - 1, y) ? 1 : 0) // NW
| (Has(x, y) ? 2 : 0) // NE
| (Has(x - 1, y - 1) ? 4 : 0) // SW
| (Has(x, y - 1) ? 8 : 0); // SE
display.SetTile(new Vector3Int(x, y, 0), tilesByMask[mask]);
}
bool Has(int x, int y) =>
x >= 0 && y >= 0 && x < snow.GetLength(0) && y < snow.GetLength(1) && snow[x, y];
}
Call Build() once with your level (from a generator, a text file, or by reading a hidden data Tilemap with HasTile), then Set() whenever a cell changes: it touches exactly four tiles. The one thing to get right is the y axis. In Unity it points up, so the data cells above a display cell have the same y and the ones below have y − 1. Code ported from Godot or from an image library, where y points down, will swap the top and bottom corners.
Gameplay and collision should read the data, not the display Tilemap: the display layer is only pictures, and it is offset by half a cell.
Quick check if something looks off:
- Blurry or slightly wrong colours: Filter Mode isn't Point, or Compression isn't None.
- Fifteen sprites instead of sixteen: re-slice with Keep Empty Rects ticked.
- Snow shifted by half a tile: the Display Tilemap's Position isn't
(-0.5, -0.5, 0). - Corners swapped top to bottom: NW/NE and SW/SE read the wrong rows (the y-up rule above).
SpriteForge generates tilesets in exactly this format: sixteen tiles in mask order with the JSON beside them. See the tileset generator.