# **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.
---
## 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(
quot;Dug {tile.name} at {cell}. Ore: {OreCollected}");
}
void Place(Vector3Int cell)
{
if (ground.HasTile(cell)) return;
// Don't bury the player.
if (player)
{
Vector3 c = ground.GetCellCenterWorld(cell);
if (Physics2D.OverlapBox(c, Vector2.one * 0.9f, 0f, 1 << player.gameObject.layer))
return;
}
ground.SetTile(cell, placeTile);
}
// Batch example: fill a rectangle in one call (far cheaper than many SetTile calls).
public void FillRect(Vector3Int min, Vector3Int size, TileBase tile)
{
var bounds = new BoundsInt(min, new Vector3Int(size.x, size.y, 1));
var tiles = new TileBase[size.x * size.y];
for (int i = 0; i < tiles.Length; i++) tiles[i] = tile;
ground.SetTilesBlock(bounds, tiles);
}
// Flash a cell red (works because OreTile doesn't set LockColor).
public void Highlight(Vector3Int cell)
{
ground.SetTileFlags(cell, TileFlags.None);
ground.SetColor(cell, Color.red);
}
}
```
The `TilemapCollider2D` + composite update automatically after `SetTile`; Rule Tiles refresh their neighbours so edges re-form around holes.
**Procedural generation sketch** — fill from noise using `SetTiles` with `TileChangeData` or position/tile arrays:
```csharp
// Assets/Scripts/NoiseTerrain.cs
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Tilemaps;
public class NoiseTerrain : MonoBehaviour
{
public Tilemap ground;
public TileBase dirt; // GroundRule
public TileBase ore; // OreTile
public int width = 120, depth = 30, surfaceHeight = 8;
public float scale = 0.08f, oreThreshold = 0.78f;
public int seed = 1234;
void Start() => Generate();
[ContextMenu("Generate")]
public void Generate()
{
ground.ClearAllTiles();
var positions = new List<Vector3Int>();
var tiles = new List<TileBase>();
float off = seed * 0.137f;
for (int x = 0; x < width; x++)
{
int top = surfaceHeight + Mathf.RoundToInt(Mathf.PerlinNoise(x * scale + off, off) * 6f);
for (int y = -depth; y < top; y++)
{
float n = Mathf.PerlinNoise(x * 0.2f + off, y * 0.2f - off);
positions.Add(new Vector3Int(x, y, 0));
tiles.Add(n > oreThreshold && y < top - 3 ? ore : dirt);
}
}
ground.SetTiles(positions.ToArray(), tiles.ToArray());
ground.CompressBounds();
}
}
```
---
## 10. Worked example: put it together
1. Grid with `Background` (water AnimatedTile), `Ground` (GroundRule + OreTile; TilemapCollider2D Merge → CompositeCollider2D Outlines, static Rigidbody2D, layer Ground), `Decor` (grass via Random Brush, no collider).
2. Player: Sprite + Rigidbody2D (Dynamic, freeze rotation Z) + CapsuleCollider2D + `SimplePlatformer`; child `GroundCheck` at the feet.
3. Empty `Systems` object with `TileDigger` (cam, ground, placeTile = GroundRule, player) and optionally `NoiseTerrain` (or paint by hand).
4. Camera: add **Pixel Perfect Camera** (Assets PPU 16) for crisp pixel art.
5. Play: run/jump; left-click digs (edges re-tile, colliders update, ore counter increments in Console); right-click places ground.
**Checkpoints / troubleshooting**
- Tiles look offset by half a cell → sprite pivot isn't Center, or Tile Anchor on the Tilemap isn't (0.5, 0.5, 0).
- Player snags on seams → composite not merging (check Composite Operation = Merge and Rigidbody2D static).
- `SetColor` does nothing → tile has `LockColor`; clear flags with `SetTileFlags(cell, TileFlags.None)`.
- Rule tile picks wrong sprite → reorder rules; specific before general.
**Docs:** Unity Manual › 2D › Tilemaps; 2D Tilemap Extras package docs (Rule Tile, Animated Tile, Scriptable Tiles); Scripting API `Tilemap`, `TileBase`, `TilemapCollider2D.compositeOperation`.