SpriteForge.tech
Blog Pricing Open the studio

How to use a 16-tile Wang tileset in Unity: RuleTile and the dual grid

← All posts · · 8 min read
TL;DR
  • 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.

A tile split into four corners worth 1, 2, 4 and 8, and the sixteen tiles of the sheet laid out in mask order NW 1 NE 2 SW 4 SE 8 one tile, four corners NW + SE = 1 + 8 = 9 so this is tile 9 the 64×64 sheet: index = mask 0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15
Corner values NW=1, NE=2, SW=4, SE=8. Tile 0 is empty, tile 15 is solid snow.

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:

SettingValue and why
Texture TypeSprite (2D and UI)
Sprite ModeMultiple, because the sheet holds sixteen sprites
Pixels Per Unit16, so one tile is one world unit, the size of a default Grid cell
Filter ModePoint (no filter), which keeps pixels hard
CompressionNone (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

  1. Create a palette with Assets → Create → 2D → Tile Palette (Rectangular), and open Window → 2D → Tile Palette.
  2. 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.
  3. 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:

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:

OrderRules for tiles
1st15 (all eight arrows)
2nd7, 11, 13, 14 (three corners)
3rd3, 5, 6, 9, 10, 12 (two corners)
4th1, 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.

The display grid sits half a tile off the data grid, so each display tile covers the corners of four data cells filled: snow cells (data) · dashed: display tiles the outlined display tile (x, y) reads: NW (x-1, y) = 1 NE (x, y) = 2 SW (x-1, y-1) = 4 SE (x, y-1) = 8 y points up in Unity; here only SE is snow, so it gets tile 8
The display grid (dashed) is offset by half a cell. Unity's y points up, so display cell (x, y) is drawn over the point where data cells (x−1, y), (x, y), (x−1, y−1) and (x, y−1) meet.

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:

  1. Blurry or slightly wrong colours: Filter Mode isn't Point, or Compression isn't None.
  2. Fifteen sprites instead of sixteen: re-slice with Keep Empty Rects ticked.
  3. Snow shifted by half a tile: the Display Tilemap's Position isn't (-0.5, -0.5, 0).
  4. 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.