# **Tilemaps and Tilesets in Unity 6 (6000.5+)**
> **Target:** Unity 6000.5 and above. Packages: **2D Tilemap Editor** (`com.unity.2d.tilemap`, built in to the 2D templates) and **2D Tilemap Extras** (`com.unity.2d.tilemap.extras`, 6.x line for Unity 6). Input uses the **Input System** package (the default active input handler in new Unity 6 projects).
What we'll build: a small side-on level painted from a sliced sprite sheet, with rule tiles for terrain that auto-picks edges/corners, an animated water tile, a custom C# "ore" tile that randomizes its sprite, a single merged collider for the whole ground, and a runtime digger that removes and places tiles with the mouse.
---
## 1. Project setup
1. Unity Hub > **New Project** > **Universal 2D** template (URP 2D renderer) > name it `TilemapLab`.
2. **Window > Package Management > Package Manager** > Unity Registry. Confirm **2D Tilemap Editor** is installed and install **2D Tilemap Extras**.
- *Why:* the editor package provides the Tilemap, Tile Palette and `Tile` classes; Extras adds `RuleTile`, `AnimatedTile`, `RuleOverrideTile`, Random brushes, GameObject brush, etc.
3. Make sure **Edit > Project Settings > Player > Active Input Handling** is *Input System Package (New)* or *Both*.
---
## 2. Importing and slicing a sprite sheet (the tileset)
1. Drop your sheet (e.g. `terrain.png`, 16×16 px tiles) into `Assets/Art/Tiles/`.
2. Select it; in the Inspector set:
- **Texture Type:** Sprite (2D and UI)
- **Sprite Mode:** Multiple
- **Pixels Per Unit:** `16` (one tile = one world unit = one grid cell)
- **Filter Mode:** Point (no filter); **Compression:** None (pixel art)
- Apply.
3. Click **Open Sprite Editor** > **Slice** dropdown > Type **Grid By Cell Size**, Pixel Size `16 × 16`, add **Padding/Offset** if the sheet has gutters > **Slice** > **Apply**.
4. *Tip — seams:* if you see hairline gaps between tiles at runtime, add a **Sprite Atlas** (Assets > Create > 2D > Sprite Atlas) containing the sheet with padding 2–4 px, or use the **Pixel Perfect Camera** component on the camera.
**Why PPU matters:** the Grid's cell size defaults to 1×1 unit. If PPU ≠ tile pixel size, tiles render too big/small relative to cells.
### What to draw in `terrain.png`
The rest of this tutorial expects these pieces, so plan the sheet before you open your pixel editor. Every cell is 16x16 px on a transparent background, pixel-aligned with no gutters. A **128 x 64 px sheet (8 columns x 4 rows)** holds everything:
```
col: 0 1 2 3 4 5 6 7
row 0 [TL ] [T ] [TR ] [ICtl] [ICtr] [PL ] [PM ] [PR ]
row 1 [L ] [C ] [R ] [ICbl] [ICbr] [SOLO] [F2 ] [F3 ]
row 2 [BL ] [B ] [BR ] [ORE1] [ORE2] [ORE3] [GEM ] [ -- ]
row 3 [W1 ] [W2 ] [W3 ] [W4 ] [GR1 ] [GR2 ] [FLW1] [FLW2]
```
| Group | Cells | What to draw | Used by |
|---|---|---|---|
| **Ground 3x3 block** | `TL T TR / L C R / BL B BR` | Outer edges and corners of a solid dirt mass. The top row carries the grass lip. `C` is the plain center fill. | `GroundRule` (section 5) |
| **Inner corners** | `ICtl ICtr ICbl ICbr` | Small notches of grass or edge in one corner of an otherwise filled tile, for concave shapes. | `GroundRule`, so holes from the digger re-tile cleanly |
| **Thin platforms** | `PL PM PR`, `SOLO` | Left cap, middle and right cap for 1-tile-high ledges, plus a lone single block. Without these, 1-wide shapes get the wrong sprite. | `GroundRule` |
| **Fill variants** | `F2 F3` | Two more versions of `C` with small differences (pebbles, roots). They must tile seamlessly on all four sides. | Random sprite output on the center rule |
| **Ore** | `ORE1-3`, `GEM` | Ore specks drawn on top of the `C` dirt so they blend. `GEM` is the rare version that `OreTile` tints. | `OreTile` (section 7) |
| **Water** | `W1-W4` | A 4-frame loop. Make frame 4 lead smoothly back into frame 1. | `Water` Animated Tile (section 6) |
| **Decor** | `GR1 GR2 FLW1 FLW2` | Grass tufts and flowers that sit on top of the ground, with mostly transparent pixels. | `Decor` tilemap and Random Brush, no collider |
**Drawing tips**
- Draw the center fill `C` first and test it tiled 4x4 before you draw any edges. Every other ground tile borrows its texture.
- Keep the edge pixels of `T` identical across `TL`, `T` and `TR` where they meet, so a long row reads as one surface.
- Add 1 px of shading under the grass lip for depth. It reads well at 16 px.
- You don't need gutters between cells. If you add them, set the matching **Padding** when you slice.
- Leave unused cells (`--`) fully transparent. Delete their empty sprites after slicing, or just ignore them.
**Example sheets to study**
- [Pixel Adventure 1 by Pixel Frog](https://pixelfrog-assets.itch.io/pixel-adventure-1) is free, 16x16, and its terrain blocks follow almost exactly this layout.
- [Sunny Land by ansimuz](https://ansimuz.itch.io/sunny-land-pixel-game-art) is a free 16x16 side-view set with grass-topped terrain, platforms and decor.
- [Kenney Pixel Platformer](https://kenney.nl/assets/pixel-platformer) is CC0 and good for class handouts. Its tiles are **18x18**, so use PPU 18 and an 18 x 18 slice.
- [Unity 2D Tilemap Extras samples (GitHub)](https://github.com/Unity-Technologies/2d-extras) include rule tile examples with matching sprites.
- The [Rule Tile manual page](https://docs.unity3d.com/Packages/
[email protected]/manual/RuleTile.html) shows how neighbor rules map onto these edge and corner tiles.
---
## 3. Grid, Tilemaps and layers
1. Hierarchy > **+ > 2D Object > Tilemap > Rectangular**. This creates `Grid` (with a `Grid` component) and a child `Tilemap` (with `Tilemap` + `TilemapRenderer`).
2. Rename the child `Ground`. Add two more tilemaps under the same Grid: `Background` and `Decor`.
3. Sort them: on each `TilemapRenderer` set **Order in Layer** (Background = -10, Ground = 0, Decor = 10) or use Sorting Layers.
4. **TilemapRenderer Mode:**
- **Chunk** (default) — batches tiles per chunk; fastest, but all sprites in a tilemap should share a texture/atlas.
- **Individual** — sorts each tile separately (needed for isometric/Y-sorted tiles that overlap sprites).
- **SRP Batch** — available in Unity 6 for URP; lets tiles batch through the SRP batcher.
Other grid layouts: Hexagonal (Point/Flat Top) and Isometric / Isometric Z as Y — same API, different `Grid.cellLayout`.
---
## 4. Tile Palette: painting
1. **Window > 2D > Tile Palette**. Click **Create New Palette**, name `TerrainPalette`, Grid = Rectangle, save in `Assets/Tiles/Palettes/`.
2. Drag the sliced sheet (or selected sprites) into the palette window and choose a folder (`Assets/Tiles/Generated/`). Unity creates one `Tile` asset per sprite.
3. Choose the **Active Tilemap** dropdown at the top of the palette (e.g. `Ground`) and paint.
4. Tools and shortcuts: Select (S), Move (M), Paint (B), Box Fill (U), Pick (I), Erase (D), Flood Fill (G). `[` / `]` rotate the brush, Shift+`[` flips. Hold Ctrl/Alt for pick/erase shortcuts depending on tool.
5. Use the **Tile Palette's Brush** dropdown for Extras brushes: **Random Brush** (scatter grass), **Line Brush**, **GameObject Brush** (paint prefabs onto the grid, e.g. coins).
*Explanation:* the palette is itself a tilemap prefab; tiles are assets referenced by your scene tilemaps, so editing a Tile asset (e.g. changing its collider type) updates every placed instance.
---
## 5. Rule Tiles (auto-tiling)
1. Project window > **Create > 2D > Tiles > Rule Tile**. Name `GroundRule`.
2. Set **Default Sprite** to the center-fill sprite and **Default Collider** to *Sprite* (or *Grid*).
3. Add rules (+). Each rule has a 3×3 neighbor grid: click a cell to cycle **green arrow = This** (same tile must be there), **red X = Not This**, empty = don't care.
- Rule for "top edge": up = X, left/right/down = arrow → top-surface sprite.
- Rules for left/right edges, outer corners, inner corners (diagonal X with orthogonal arrows).
4. **Order matters** — rules are evaluated top to bottom; first match wins. Put specific rules (corners) above general ones (edges).
5. Per-rule options: **Output** = Single / Random (with Perlin noise scale) / Animation; **Transform** = Fixed / Rotated / MirrorX / MirrorY (lets one corner rule cover all four orientations).
6. Drag `GroundRule` into the palette and paint — edges resolve automatically.
7. **Rule Override Tile** (Create > 2D > Tiles > Rule Override Tile): reuse `GroundRule`'s rules but swap sprites for a snow/desert variant.
---
## 6. Animated Tiles
1. **Create > 2D > Tiles > Animated Tile** → `Water`.
2. Set **Number of Animated Sprites** and drag frames in order.
3. **Minimum/Maximum Speed** (frames per second multiplier) — a random value in range per tile desyncs neighbours; **Start Time** and **Start Frame** offset the cycle.
4. **Tile Collider**: None for decorative water. Paint it on `Background`.
Animated tiles are driven by the TilemapRenderer — zero scripts, and they work in Edit Mode preview from the palette.
---
## 7. Custom scriptable tile in C#
Any class deriving from `TileBase` can be a tile. Override `GetTileData` to supply sprite/color/transform/collider per cell, and `RefreshTile` if neighbours must update when this tile changes. This `OreTile` picks a deterministic random sprite from its position (stable across reloads), tints rare variants, and refreshes neighbours so adjacent ore tiles can react.
```csharp
// Assets/Scripts/Tiles/OreTile.cs
using UnityEngine;
using UnityEngine.Tilemaps;
[CreateAssetMenu(fileName = "OreTile", menuName = "2D/Tiles/Ore Tile")]
public class OreTile : TileBase
{
[Tooltip("Possible sprites; one is chosen per cell from a position hash.")]
public Sprite[] sprites;
[Range(0f, 1f)] public float rareChance = 0.1f;
public Color rareTint = new Color(1f, 0.85f, 0.3f);
public Tile.ColliderType colliderType = Tile.ColliderType.Grid;
// How much "ore" a cell yields when mined (read by TileDigger).
public int yield = 1;
public override void GetTileData(Vector3Int position, ITilemap tilemap, ref TileData tileData)
{
if (sprites == null || sprites.Length == 0) return;
uint h = Hash(position);
tileData.sprite = sprites[h % (uint)sprites.Length];
bool rare = (h % 1000u) / 1000f < rareChance;
tileData.color = rare ? rareTint : Color.white;
tileData.transform = Matrix4x4.identity;
tileData.flags = TileFlags.LockTransform; // allow runtime color changes (no LockColor)
tileData.colliderType = colliderType;
}
public override void RefreshTile(Vector3Int position, ITilemap tilemap)
{
// Refresh this cell and the 8 neighbours (useful if visuals depend on neighbours).
for (int y = -1; y <= 1; y++)
for (int x = -1; x <= 1; x++)
{
var p = new Vector3Int(position.x + x, position.y + y, position.z);
if (tilemap.GetTile(p) == this) tilemap.RefreshTile(p);
}
}
static uint Hash(Vector3Int p)
{
unchecked
{
uint h = 2166136261u;
h = (h ^ (uint)p.x) * 16777619u;
h = (h ^ (uint)p.y) * 16777619u;
h = (h ^ (uint)p.z) * 16777619u;
h ^= h >> 13; h *= 0x5bd1e995; h ^= h >> 15;
return h;
}
}
}
```
**Use it:** Create > 2D > Tiles > Ore Tile, assign 3–4 rock sprites, drag the asset into the palette, paint.
*Explanation:* `TileBase` assets are shared — never store per-cell state in fields. Derive per-cell variety from `position` (as above) or keep state in a separate component/dictionary keyed by cell.
You can also subclass the concrete `Tile` class when you just need to tweak `GetTileData` while keeping its sprite/color/collider fields, or override `GetTileAnimationData` for code-driven animation and `StartUp` to spawn a GameObject per tile.
---
## 8. Collisions: TilemapCollider2D + CompositeCollider2D
1. Select `Ground` > Add Component **Tilemap Collider 2D**. Every tile whose collider type is Sprite/Grid gets a shape — many small boxes, which cause "ghost collisions" where characters snag on seams.
2. Add **Composite Collider 2D** to the same object. Unity adds a **Rigidbody 2D** — set **Body Type: Static**.
3. On the Tilemap Collider 2D set **Composite Operation: Merge** (Unity 6 replaced the old *Used By Composite* checkbox with `compositeOperation`).
4. On the Composite Collider 2D choose **Geometry Type: Outlines** (edge chains, best for platformer ground) or **Polygons** (needed if you want `OverlapPoint` inside solid areas / trigger volumes).
5. Optional: **Tilemap Collider 2D > Max Tile Change Count** controls when a full rebuild happens instead of incremental updates — relevant for runtime digging.
For decoration tilemaps, set tiles' collider type to None or just don't add a collider.
**Player for testing** (dynamic Rigidbody2D + CapsuleCollider2D, Input System):
```csharp
// Assets/Scripts/SimplePlatformer.cs
using UnityEngine;
using UnityEngine.InputSystem;
[RequireComponent(typeof(Rigidbody2D))]
public class SimplePlatformer : MonoBehaviour
{
public float moveSpeed = 6f;
public float jumpVelocity = 11f;
public LayerMask groundMask;
public Transform groundCheck;
public float groundRadius = 0.15f;
Rigidbody2D rb;
void Awake() => rb = GetComponent<Rigidbody2D>();
void Update()
{
var kb = Keyboard.current;
if (kb == null) return;
float x = 0f;
if (kb.aKey.isPressed || kb.leftArrowKey.isPressed) x -= 1f;
if (kb.dKey.isPressed || kb.rightArrowKey.isPressed) x += 1f;
Vector2 v = rb.linearVelocity; // Unity 6 name (was velocity)
v.x = x * moveSpeed;
bool grounded = Physics2D.OverlapCircle(groundCheck.position, groundRadius, groundMask);
if (grounded && kb.spaceKey.wasPressedThisFrame) v.y = jumpVelocity;
rb.linearVelocity = v;
}
}
```
Put `Ground` on a `Ground` layer and assign it to `groundMask`; set the player's Rigidbody2D **Interpolate** and **Collision Detection: Continuous**.
---
## 9. Runtime tile editing
Key `Tilemap` API: `WorldToCell`, `CellToWorld`, `GetCellCenterWorld`, `GetTile<T>`, `HasTile`, `SetTile`, `SetTiles` (batch), `SetTilesBlock`, `BoxFill`, `RefreshTile`, `SetColor` (requires the tile's `TileFlags.LockColor` cleared), `SetTileFlags`, `CompressBounds`, and the static `Tilemap.tilemapTileChanged` event.
```csharp
// Assets/Scripts/TileDigger.cs
using UnityEngine;
using UnityEngine.InputSystem;
using UnityEngine.Tilemaps;
public class TileDigger : MonoBehaviour
{
public Camera cam;
public Tilemap ground;
public TileBase placeTile; // e.g. GroundRule
public float maxReach = 6f;
public Transform player;
public int OreCollected { get; private set; }
void Update()
{
var mouse = Mouse.current;
if (mouse == null) return;
Vector3 world = cam.ScreenToWorldPoint(mouse.position.ReadValue());
world.z = 0f;
Vector3Int cell = ground.WorldToCell(world);
if (player && Vector2.Distance(player.position, ground.GetCellCenterWorld(cell)) > maxReach)
return;
if (mouse.leftButton.wasPressedThisFrame) Dig(cell);
else if (mouse.rightButton.wasPressedThisFrame) Place(cell);
}
void Dig(Vector3Int cell)
{
TileBase tile = ground.GetTile(cell);
if (tile == null) return;
if (tile is OreTile ore) OreCollected += ore.yield;
ground.SetTile(cell, null); // rule tiles around it re-evaluate automatically
Debug.Log(