Custom Brushes¶
You can write your own brush type in C#. This page covers the class you derive from, which methods the tilemap calls and when, a complete working example, and how to give the brush a create menu and an inspector.
Concept¶
A brush type is a class that derives from TilesetBrush. TilesetBrush is a ScriptableObject and implements the IBrush interface with default behaviour, so you only override what your brush needs. Most brushes override two methods: PreviewTileData for the palette and Refresh for the autotiling.
There is nothing to register. The tileset finds brushes by type, so once your class compiles you can create assets of it and import them with Import all brushes found in the project like any built-in brush.
Super Tilemap Editor has no assembly definitions. Its runtime code compiles into Assembly-CSharp and its editor code into Assembly-CSharp-Editor. Keep your brush scripts outside any assembly definition of your own, or they cannot see TilesetBrush. Put the brush class in a normal folder and its editor class in a folder named Editor.
How a refresh reaches your brush¶
Every cell stores a tile id, a brush id and four flag bits. One of those bits marks whether a brush cell is up to date. See Tile Data for the layout.
When a cell is painted, the tilemap clears that bit on the cell itself if it holds a brush, and on each of the eight neighbours whose brush connects to the old or the new content of the cell. Nothing else happens at that point.
Later, when the chunk rebuilds its mesh, it calls Refresh on every brush cell whose bit is clear, stores the result, and sets the bit again. So Refresh runs once per change, not every frame, and it runs for the neighbours as well as the painted cell. Refresh Map, F5 and a change in map bounds size call it on every brush cell in the tilemap.
Which neighbours connect is decided by AutotileWith, which applies the brush's Autotiling Mode and Group. Call it from your own code rather than comparing brush ids yourself, so your brush honours the settings in its inspector.
The members you can override¶
| Member | When it is called | What to return |
|---|---|---|
uint PreviewTileData() |
Whenever an editor draws the brush: the brush palette, a brush grid slot that links this brush, the Tile Properties window. | The tile data to show. The base class returns an empty cell, so without an override your brush is invisible in the palette. |
uint Refresh(STETilemap tilemap, int gridX, int gridY, uint tileData) |
During the mesh rebuild, for each cell of this brush whose refresh bit is clear. tileData is the cell's current content. |
The tile data to store in the cell. Keep the incoming brush id in it, unless you mean to hand the cell to another brush. |
uint[] GetSubtiles(STETilemap tilemap, int gridX, int gridY, uint tileData) |
On every mesh rebuild and vertex colour update, for every cell of this brush, after Refresh. |
null to draw the cell as one tile. Or exactly four tile data values, ordered bottom-left, bottom-right, top-left, top-right. Each quarter of the cell then shows the matching quarter of its tile. Any other length breaks the mesh. |
uint GetTileData(STETilemap tilemap, int gridX, int gridY, uint tileData) |
On every mesh rebuild, for every cell of this brush, after Refresh. |
The tile data to draw with. The result is used for rendering only and is not stored. The base class returns the input. |
bool IsAnimated() |
On every mesh rebuild. | true to have the chunk update this cell's UVs every frame. |
Rect GetAnimUV() |
By editor previews, and by the base GetAnimUVWithFlags. |
The atlas UV rectangle of the current frame. |
uint GetAnimTileData() |
By the palette for animated brushes, and by the base GetAnimUVWithFlags. |
The tile data of the current frame. The base class returns PreviewTileData(). |
int GetAnimFrameIdx() |
By the base GetAnimUVWithFlags, to cache its result. |
The current frame index. It must change whenever the frame does. |
Vector2[] GetAnimUVWithFlags(float innerPadding, int index, uint flags) |
Every frame in LateUpdate, for each animated cell of the chunk. index is the cell's position among them and flags its flip and rotation bits. |
The four UVs of the quad. The base class builds them from GetAnimUV and GetAnimTileData, so you rarely need to override it. |
uint OnPaint(TilemapChunk chunk, int chunkGx, int chunkGy, uint tileData) |
Each time a cell is set to content that uses this brush. | Anything. The return value is not used, so this is only good for side effects. |
void OnErase(TilemapChunk chunk, int chunkGx, int chunkGy, uint tileData, int brushId) |
When a cell that used this brush is erased or set to content with a different brush. | Nothing. |
GetMergedSubtileColliderVertices is also on the interface, but the collider code that calls it is compiled out, so overriding it has no effect.
TilesetBrush also gives you helpers:
AutotileWith(tilemap, selfBrushId, gridX, gridY)returns whether the cell atgridX, gridYconnects to this brush. It handles empty cells, the map bounds and brush groups.RefreshLinkedBrush(tilemap, gridX, gridY, tileData)resolves tile data that points at another brush. Pass each tile you pick through it so a slot can hold a RandomBrush or an AnimBrush, the way the built-in brushes allow.Tilesetis the tileset the brush belongs to, andParamsholds the parameters set in the Tile Properties window.
Example: a pillar brush¶
This brush is for vertical runs such as ladders, pillars and tree trunks. It uses four tiles: a single piece, a top, a middle and a bottom, and it only looks at the cells above and below.
Both classes below compile against the current Super Tilemap Editor API.
using UnityEngine;
using CreativeSpore.SuperTilemapEditor;
// A brush for vertical runs: ladders, pillars, tree trunks.
// It looks only at the cells above and below.
[CreateAssetMenu(fileName = "New PillarBrush", menuName = "SuperTilemapEditor/Brush/PillarBrush")]
public class PillarBrush : TilesetBrush
{
// Slot order: 0 single, 1 top, 2 middle, 3 bottom.
// Each entry is full tile data (tile id, brush id and flags), not just a tile id.
public uint[] TileIds = { Tileset.k_TileData_Empty, Tileset.k_TileData_Empty, Tileset.k_TileData_Empty, Tileset.k_TileData_Empty };
// Shown in the brush palette.
public override uint PreviewTileData()
{
return TileIds[0];
}
// Called during the mesh rebuild for every cell painted with this brush
// whose refresh bit is clear. Returns the tile data to render.
public override uint Refresh(STETilemap tilemap, int gridX, int gridY, uint tileData)
{
int brushId = Tileset.GetBrushIdFromTileData(tileData);
bool north = AutotileWith(tilemap, brushId, gridX, gridY + 1);
bool south = AutotileWith(tilemap, brushId, gridX, gridY - 1);
int slot;
if (north && south) slot = 2; // middle
else if (south) slot = 1; // top: something below, nothing above
else if (north) slot = 3; // bottom
else slot = 0; // single
// Lets a slot hold another brush, for example a RandomBrush.
uint result = RefreshLinkedBrush(tilemap, gridX, gridY, TileIds[slot]);
// Put this brush's id back, or the cell stops being a PillarBrush cell.
result &= ~Tileset.k_TileDataMask_BrushId;
result |= tileData & Tileset.k_TileDataMask_BrushId;
return result;
}
}
The last two lines of Refresh matter. The tile you pick from TileIds carries no brush id, or the id of a linked brush. If you return it as is, the cell loses its link to your brush and the next refresh treats it as a plain tile.
[CreateAssetMenu] adds Assets > Create > SuperTilemapEditor > Brush > PillarBrush, next to the built-in types. The built-in brushes use a [MenuItem] in their editor class instead, which works too.
An inspector for it¶
Without an editor class the brush shows Unity's default inspector, where TileIds is a list of raw numbers. The editor below reuses the grid control of the built-in brushes, so you fill the slots by clicking a slot and then a tile in the Tile Palette, with the flags box and the autocomplete buttons included.
using UnityEditor;
using CreativeSpore.SuperTilemapEditor;
[CustomEditor(typeof(PillarBrush))]
public class PillarBrushEditor : TilesetBrushEditor
{
// Grid cell i shows TileIds[s_tileIdxMap[i]], drawn top to bottom.
static readonly int[] s_tileIdxMap = { 1, 2, 3, 0 };
// Placeholder drawn in an empty cell: neighbour flags N = 1, E = 2, S = 4, W = 8.
static readonly int[] s_symbolIdxMap = { 4, 5, 1, 0 };
PillarBrush m_brush;
BrushTileGridControl m_gridControl = new BrushTileGridControl();
public override void OnEnable()
{
base.OnEnable();
m_brush = (PillarBrush)target;
}
void OnDisable()
{
// Unsubscribes the control from the tileset's selection events.
m_gridControl.Tileset = null;
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI(); // Tileset, Show In Palette, Group, Autotiling Mode
if (!m_brush.Tileset) return;
m_gridControl.Tileset = m_brush.Tileset;
m_gridControl.Display(target, m_brush.TileIds, s_tileIdxMap, 1, 4, m_brush.Tileset.VisualTileSize, s_symbolIdxMap);
Repaint();
serializedObject.ApplyModifiedProperties();
if (UnityEngine.GUI.changed)
EditorUtility.SetDirty(target);
}
}
Display takes the tile data array, a map from grid cell to array index, the grid width and height, the tile size to draw at, and the placeholder symbols. Deriving from TilesetBrushEditor and calling base.OnInspectorGUI() gives you the Tileset, Show In Palette, Group and Autotiling Mode fields.
Warning
Set the control's Tileset to null in OnDisable. The control listens to the tileset's selection events, and a control left subscribed keeps writing into a brush you are no longer editing.
Try it¶
- Save the two classes, the second one in an
Editorfolder. - Choose
Assets > Create > SuperTilemapEditor > Brush > PillarBrushand set itsTileset. - Fill the four slots from top to bottom: top, middle, bottom, single.
- Select the tileset and press
Import all brushes found in the project. - Pick the brush in the brush palette and paint a vertical line.
Going further¶
- Animated tiles in slots.
RefreshLinkedBrushresolves a linked AnimBrush to its first frame, but the cell only animates if the chunk knows about it. The built-in brushes overrideGetSubtilesto callTilemapChunk.RegisterAnimatedBrush(brush, -1, (byte)(tileData >> 28))for a linked animated brush and then returnnull. ReadRoadBrush.csfor the pattern. - Quarter tiles. Return four tile data values from
GetSubtilesto build a cell from quarters, asCarpetBrushandA2X2Brushdo. Keep the method cheap and free of side effects, because it runs for every cell of your brush on every rebuild. - Diagonals. Call
AutotileWithfor the diagonal cells too.BrushFortySeven.csshows an eight-neighbour lookup table.
The built-in brushes in Assets/CreativeSpore/SuperTilemapEditor/Scripts/Tilemap/Brush are the best reference. Each is a short file, and each editor is in the Editor folder next to it.