Scripting¶
This page covers reading and writing tiles from your own C# code: procedural levels, runtime editing, editor tools. The pages under it go deeper into the cell format, coordinates, collisions, tile prefabs and pathfinding.
Concept¶
The asset's types live in one namespace. Add this line at the top of any script that touches it:
Without it the compiler reports CS0246: The type or namespace name 'STETilemap' could not be
found. The pathfinding internals sit in a sub-namespace, CreativeSpore.SuperTilemapEditor.PathFindingLib,
which you only need for Pathfinding.
The component you talk to is STETilemap. It holds the settings and the coordinate system, and
it owns a set of hidden child objects called chunks, 60 by 60 cells each, which hold the tiles,
the mesh and the colliders. You never touch a chunk directly. How It Works
explains why they exist.
Each cell stores one uint that packs a tile id, a brush id and four flag bits. Most methods take
either that packed value (SetTileData, GetTileData) or the three parts separately (SetTile).
Tile Data describes the format.
Writes are deferred. SetTile and SetTileData change the stored data and mark the chunk dirty,
and nothing on screen changes until the mesh is rebuilt. Filling a whole map from code therefore
costs one rebuild at the end, however many tiles you wrote.
Getting a tilemap¶
Reference it from a serialized field, which is the usual way:
using UnityEngine;
using CreativeSpore.SuperTilemapEditor;
public class LevelBuilder : MonoBehaviour
{
[SerializeField] private STETilemap m_ground;
public STETilemap Ground => m_ground;
}
A tilemap inside a TilemapGroup can also be reached through the group, by index or by
GameObject name:
STETilemap first = group[0];
STETilemap walls = group["Walls"]; // null when no tilemap has that name
From a collider, a chunk or anything else under the tilemap in the hierarchy, use
GetComponentInParent<STETilemap>(). Collisions shows it.
Writing tiles¶
SetTile takes grid coordinates and a tile id. The brush id and the flags are optional.
// Tile 5 of the tileset at grid cell (3, 4).
m_ground.SetTile(3, 4, 5);
// The same tile mirrored left to right.
m_ground.SetTile(4, 4, 5, Tileset.k_BrushId_Default, eTileFlags.FlipH);
// Remove a tile.
m_ground.Erase(5, 4);
// Nothing above is visible until the mesh is rebuilt.
m_ground.UpdateMesh();
SetTileData writes a packed value, which is what you want when copying a cell or restoring
one you read earlier:
SetTile does nothing more than pack its arguments and call SetTileData, and in a measured fill
of a 512x512 map the two cost the same. Pick whichever reads better.
Painting with a brush¶
A brush tile is written with the brush id and an empty tile id. The brush chooses the real tile the next time the mesh is rebuilt, taking its neighbours into account. That is how autotiling works from code.
// Brush ids are stored in the tileset, next to the brush asset.
static int FindBrushId(Tileset tileset, TilesetBrush brush)
{
foreach (Tileset.BrushContainer container in tileset.Brushes)
{
if (container.BrushAsset == brush)
return container.Id;
}
return -1;
}
// ...
int roadId = FindBrushId(m_ground.Tileset, m_roadBrush);
if (roadId > 0)
{
for (int x = 0; x < 20; ++x)
m_ground.SetTile(x, 0, Tileset.k_TileId_Empty, roadId);
m_ground.UpdateMesh();
}
Tileset.FindBrushId(string) also exists and matches by asset name, so two brushes with the same
name cannot be told apart. Comparing the asset reference, as above, avoids that.
Grid positions and local positions¶
Most methods have two overloads, and the difference matters.
| Overload | Coordinates |
|---|---|
SetTile(int gridX, int gridY, ...), SetTileData(int, int, uint), SetTileData(Vector2Int, uint), Erase(int, int), GetTileData(int, int), GetTileData(Vector2Int), GetTile(int, int), GetBrush(int, int) |
Grid cells. (0, 0) is the cell whose bottom left corner sits on the tilemap's origin. Negative cells are valid. |
SetTile(Vector2, ...), SetTileData(Vector2, uint), Erase(Vector2), GetTileData(Vector2), GetTile(Vector2), GetBrush(Vector2) |
A position in the tilemap's local space, in units. It is divided by CellSize to find the cell. |
The Vector2 overloads take a local position, not a world one. Passing transform.position of
some object only works while the tilemap sits at the origin with no rotation or scale. Convert
first:
Vector2 local = m_ground.transform.InverseTransformPoint(worldPosition);
Tile tile = m_ground.GetTile(local);
Positions and the Mouse has the conversion helpers.
The indexer¶
STETilemap has three indexers that read and write packed tile data:
uint a = m_ground[3, 4]; // grid cell
uint b = m_ground[new Vector2Int(3, 4)]; // grid cell
uint c = m_ground[new Vector2(0.48f, 0.64f)]; // local position
m_ground[3, 5] = a; // writes like SetTileData, so call UpdateMesh afterwards
Reading tiles¶
| Method | Returns |
|---|---|
GetTileData(x, y) |
The packed uint. Tileset.k_TileData_Empty for an empty cell or a cell outside every chunk. |
GetTile(x, y) |
The Tile object from the tileset, with its collider, parameters and prefab data, or null for an empty cell. |
GetBrush(x, y) |
The TilesetBrush that painted the cell, or null if it was painted without one. |
GetTileObject(x, y) |
The GameObject created from the tile's prefab, or null. See Tile Prefabs. |
GetTileColor(x, y) |
The TileColor32 painted on the cell. See Color. |
For a brush cell, GetTile returns the tile the brush picked at the last rebuild, and before
the first rebuild the tile id is still empty. Reading a brush cell right after writing it gives
you back what you wrote.
When the tilemap updates¶
| Call | What happens |
|---|---|
UpdateMesh() |
Sets a flag. The tilemap's own Update sees it and runs UpdateMeshImmediate. That is this frame if your script's Update ran first, otherwise the next one. |
UpdateMeshImmediate() |
Rebuilds now. First the mesh of every dirty chunk, then the colliders of every dirty chunk, then raises OnMeshUpdated. |
Refresh(refreshMesh, refreshMeshCollider, refreshTileObjects, invalidateBrushes) |
Marks every chunk dirty for the parts you ask for, then calls UpdateMesh(). All four parameters are optional, and the defaults rebuild the mesh and the colliders. |
Only chunks that were marked dirty rebuild their mesh or colliders. A write near a chunk edge also marks the neighbouring chunk, because autotiling and collider edges look across the seam.
Use UpdateMeshImmediate when the result has to exist before your code continues. The usual case
is placing the player on a map you have just generated, where the colliders must be there before
the first physics step. Otherwise call UpdateMesh and let the rebuild happen once, however many
places wrote tiles in between.
Note
In edit mode Unity only runs Update when something in the scene changes, so a flag set by
UpdateMesh from an editor script can sit there until you move the mouse over the scene
view. Editor tools should call UpdateMeshImmediate.
The tilemap also rebuilds by itself in a few cases. A chunk that enters play mode or loads with a
scene builds its mesh in OnEnable, because meshes are never saved. Changing IsVisible or
Material from code calls UpdateMesh for you. In the editor, undo and redo rebuild the chunks
they restore.
Settings that are plain fields or simple properties do not rebuild anything. After changing
CellSize, InnerPadding, ColliderType, Collider2DType, ColliderDepth or IsTrigger from
code, call Refresh().
Warning
Assign Tileset before writing tiles, and give the tileset its atlas texture. Writing to a
tilemap with no tileset throws a NullReferenceException. A rebuild while the tileset has no
atlas texture treats each chunk it rebuilds as empty and removes it, tiles included.
Filling a map from code¶
The pattern for a procedural level is: write everything, then rebuild once.
using UnityEngine;
using CreativeSpore.SuperTilemapEditor;
public class NoiseMapGenerator : MonoBehaviour
{
[SerializeField] private STETilemap m_ground;
[SerializeField] private int m_width = 200;
[SerializeField] private int m_height = 200;
[SerializeField] private int m_waterTileId = 0;
[SerializeField] private int m_grassTileId = 1;
public void Generate(int seed)
{
m_ground.ClearMap();
float offsetX = seed * 13.1f;
float offsetY = seed * 7.7f;
for (int y = 0; y < m_height; ++y)
{
for (int x = 0; x < m_width; ++x)
{
float noise = Mathf.PerlinNoise((x + offsetX) / 25f, (y + offsetY) / 25f);
m_ground.SetTile(x, y, noise < 0.35f ? m_waterTileId : m_grassTileId);
}
}
// One rebuild for the whole map, and colliders are ready when this returns.
m_ground.UpdateMeshImmediate();
}
}
What the measurements say, taken on 2026-09-23 with an i7-10700K in the Unity 2022.3.62f2 editor on version 1.7.7:
- Writing every cell of a 512x512 map took 305 ms, and a 128x128 map took 19.7 ms. That is only the write; the mesh rebuild comes on top.
- Writing allocates no garbage beyond the tile arrays of the chunks it creates. Versions before 1.7.7 left about 19 MB behind on the same 512x512 fill.
- Rebuilding one chunk's mesh costs about 1.7 ms, and a chunk rebuilds whole even when one cell changed. A 512x512 map is 81 chunks.
So a few habits pay off on big maps:
- Call
UpdateMeshorUpdateMeshImmediateonce, after the loop, never inside it. - If you generate a large world in pieces, write one region and rebuild per frame. Only the chunks you touched rebuild.
- Tiles with prefabs instantiate their GameObject the moment you write them. When a generator overwrites cells several times, turn that off until the map is final, as shown in Tile Prefabs.
- Layers that nobody collides with should have
Collider Typeset toNone, so their chunks skip the collider pass.
TilemapDrawingUtils has the shape routines the paint tools use, and they work at runtime too.
Each takes a uint[,] pattern indexed [x, y], which it repeats across the shape:
uint wall = Tileset.k_TileData_Empty;
uint floor = Tileset.k_TileData_Empty;
// ... fill wall and floor with tile data ...
// A filled 10x6 room with its bottom left corner at (2, 2).
TilemapDrawingUtils.DrawRect(m_ground, 2, 2, 11, 7, new uint[,] { { floor } }, true);
// A one tile outline around it.
TilemapDrawingUtils.DrawRect(m_ground, 1, 1, 12, 8, new uint[,] { { wall } }, false);
m_ground.UpdateMesh();
DrawLine, DrawEllipse, DrawDot and FloodFill take the same kind of pattern. The flood
fill is limited to the map bounds.
Clearing¶
| Call | Effect |
|---|---|
ClearMap() |
Destroys every child GameObject of the tilemap and resets the map bounds to zero. |
ClearMap(true) |
The same, but keeps the map bounds. This is the inspector's Clear Tiles button. |
Erase(x, y) |
Empties one cell. Needs a rebuild like any other write. |
ClearMap takes effect at once, with no rebuild needed. It removes everything parented to the
tilemap, which includes tile prefab instances and any object of yours you placed under it.
Map bounds¶
A tilemap tracks the rectangle of cells that has been written to.
| Member | Meaning |
|---|---|
MinGridX, MinGridY, MaxGridX, MaxGridY |
The bounds in grid cells, inclusive. |
GridWidth, GridHeight |
Cells across and up. |
MapBounds |
The same rectangle as a Bounds in local units. |
SetMapBounds(minX, minY, maxX, maxY) |
Sets the bounds. |
Trim() |
Shrinks the bounds to the tiles that exist. AutoTrim does it on every rebuild. |
AllowPaintingOutOfBounds |
When false, writes outside the bounds are ignored. |
IsGridPositionInsideTilemap(x, y) |
True inside the bounds. |
The bounds always contain cell (0, 0). SetMapBounds clamps the minimum to at most 0 and the
maximum to at least 0, and so does every rebuild. Writing outside the bounds grows them when
AllowPaintingOutOfBounds is on. Erasing never shrinks them; call Trim() for that.
To visit every cell in the bounds:
TilemapUtils.IterateTilemapWithAction(m_ground, (STETilemap tilemap, int x, int y, uint data) =>
{
if (data != Tileset.k_TileData_Empty)
Debug.Log("Tile at " + x + "," + y);
});
Tilemap events¶
STETilemap exposes two delegate fields. Subscribe with += and unsubscribe in OnDisable.
| Delegate | Raised |
|---|---|
OnTileChanged(STETilemap tilemap, int gridX, int gridY, uint tileData) |
From SetTileData for every cell it writes, which covers SetTile, Erase, the indexer and the paint tools. |
OnMeshUpdated(STETilemap source) |
At the end of UpdateMeshImmediate, after meshes and colliders are rebuilt. |
void OnEnable()
{
m_ground.OnTileChanged += HandleTileChanged;
m_ground.OnMeshUpdated += HandleMeshUpdated;
}
void OnDisable()
{
m_ground.OnTileChanged -= HandleTileChanged;
m_ground.OnMeshUpdated -= HandleMeshUpdated;
}
void HandleTileChanged(STETilemap tilemap, int gridX, int gridY, uint tileData) { /* ... */ }
void HandleMeshUpdated(STETilemap source) { /* ... */ }
OnTileChanged can fire for cells you did not write. When a brush tile sits next to your cell
across a chunk edge, the tilemap rewrites that neighbour to schedule its refresh, and the event
reports it. ClearMap raises neither event.
The tilemap also forwards Unity's collision and trigger messages from its chunks to its own GameObject. See Collisions.
Tile parameters¶
Tiles and brushes can carry named parameters, set in the
Tile Properties window: bool, int, float, string or an
object reference. Read them per cell with TilemapUtils.GetTileParameter:
float speed = TilemapUtils.GetTileParameter(m_ground, x, y, "speed", 1f);
bool isWater = TilemapUtils.GetTileParameter(m_ground, x, y, "water", false);
It looks in the cell's brush first. If the brush does not have the parameter, or its value equals the default you passed, it falls back to the tile's own value. So a brush cannot override a tile's value back to your default.
For direct access, the containers are Tile.paramContainer and TilesetBrush.Params, both of
type ParameterContainer:
Tile tile = m_ground.GetTile(x, y);
if (tile != null)
{
int damage = tile.paramContainer.GetIntParam("damage", 0);
Sprite icon = tile.paramContainer.GetObjectParam("icon") as Sprite;
}
TilemapUtils.GetParamsFromTileData(tilemap, tileData) returns the brush's container when the
cell has a brush and the tile's otherwise, without merging the two.
Read object parameters with GetObjectParam and a cast. The generic GetParam<T> only returns
an object when T is exactly the stored object's type, and it does not accept
UnityEngine.Object itself.
Warning
Parameters live in the tileset and brush assets, shared by every cell that uses that tile.
SetParam changes all of them at once, and in the editor it changes the asset on disk even
in play mode. Keep per cell state in your own data, keyed by grid position.
A type mismatch, such as reading an int parameter as a float, logs an assertion in the editor
and returns 0 in a build, where assertions are stripped.
Reference¶
| Member | What it does |
|---|---|
Tileset |
The tileset the tilemap draws from. Setting it on a tilemap with no cell size also sets CellSize from the tileset. |
CellSize |
Size of one cell in local units. Call Refresh() after changing it. |
SetTile, SetTileData, Erase |
Write cells. Deferred until a rebuild. |
GetTileData, GetTile, GetBrush, GetTileObject |
Read cells. |
UpdateMesh, UpdateMeshImmediate, Refresh |
Rebuild. |
RefreshTile(x, y) |
Makes the brush at that cell run its refresh on the next rebuild. |
ClearMap, Trim, SetMapBounds |
Whole map operations. |
FlipH(bool), FlipV(bool), Rot90(bool) |
Mirror or rotate the whole map inside its bounds. true also flips each tile's own flags, except on brush tiles. |
IsUndoEnabled |
When true, writes are recorded for editor undo. The paint tools turn it on and off around each paint operation. |
STETilemap.DisableTilePrefabCreation |
Static switch that stops writes from instantiating tile prefabs. |