Abstract editor TileBrush for a tilemap editor. Defines built-in brushes, rasterizes brush strokes, determines which tiles are painted and which neighbouring tiles are affected, and delegates per-cell sprite/flag computation to concrete brushes.
using Editor.TerrainEditor;
using Saandy.Tilemapper;
using System;
using System.Collections.Generic;
using System.Linq;
namespace Saandy.Editor.Tilemapper;
public abstract class TileBrush
{
public static readonly TileBrush Manual = new TileBrushManual();
public static readonly TileBrush Bitmask2x2Edge = new TileBrush2x2Edge();
public static readonly TileBrush Bitmask3x3Complex = new TileBrush3x3Complex();
public static TileBrush GetBrush( BrushType brush ) => brush switch
{
BrushType.Manual => Manual,
BrushType.Edge2x2 => Bitmask2x2Edge,
BrushType.Complex3x3 => Bitmask3x3Complex,
_ => null
};
public abstract string Name { get; protected set; }
public void Draw( TileMap tilemap, TilesetResource tileset, Vector2Int from, Vector2Int to, bool erase = false )
{
if ( tilemap == null )
return;
if ( tileset == null )
return;
var paintedTiles = RasterizeLine( from, to )
.Distinct()
.ToList();
if ( paintedTiles.Count == 0 )
return;
var paintedSet = paintedTiles.ToHashSet();
var affectedTiles = paintedTiles
.SelectMany( GetAffectedTiles )
.Where( t => !paintedSet.Contains( t ) )
.Distinct()
.ToList();
tilemap.SetTiles(
paintedTiles,
affectedTiles,
pos =>
{
bool isPaintedTile = paintedSet.Contains( pos );
// Painted cells always use the active tileset/brush.
if ( isPaintedTile )
{
if ( erase )
return (null, (ushort)0, false, false);
return GetTileToSet( tilemap, tileset, default, pos.x, pos.y, isRefresh: false );
}
// Affected cells already exist in the tilemap. Refresh them using their own
// tileset and that tileset's brush, not the brush currently being painted with.
if ( !tilemap.TryGetTile( pos.x, pos.y, out var existingTile ) || existingTile.IsEmpty )
return (null, (ushort)0, false, false);
var existingTileset = tilemap.GetTileset( existingTile.TilesetId );
if ( existingTileset == null || !existingTileset.IsValid() )
return (null, (ushort)0, false, false);
var existingBrush = GetBrush( existingTileset.BrushType );
if ( existingBrush == null )
return (existingTileset, existingTile.TileId, existingTile.IsVisible, existingTile.UseCollider);
return existingBrush.GetTileToSet( tilemap, existingTileset, existingTile, pos.x, pos.y, isRefresh: true );
}
);
}
/// <summary>
/// Builds the tile data that should be written for this brush at a specific cell.
/// Autotile brushes recompute their sprite. Manual brush overrides this during refresh
/// so neighbouring autotile changes do not overwrite manually selected tile ids.
/// </summary>
public virtual (TilesetResource Tileset, ushort TileId, bool IsVisible, bool UseCollider) GetTileToSet(
TileMap tilemap,
TilesetResource tileset,
TileMap.Tile existingTile,
int x,
int y,
bool isRefresh )
{
ushort spriteIndex = GetSpriteIndexToSet( tilemap, tileset, x, y );
var flags = GetDefaultTileFlags( spriteIndex );
return (tileset, spriteIndex, flags.IsVisible, flags.UseCollider);
}
/// <summary>
/// Per-cell flags written into TileMap.Tile when this brush paints a sprite.
/// Override this in brushes that need logical/mask tiles that should not render or collide.
/// </summary>
public abstract (bool IsVisible, bool UseCollider) GetDefaultTileFlags( ushort spriteIndex );
/// <summary>
/// Legacy helper for callers that only care about map adjacency.
/// Manual brush needs the TilesetResource overload so it can read the selected manual tile.
/// </summary>
public virtual ushort GetSpriteIndexToSet( TileMap tilemap, int x, int y )
{
return GetSpriteIndexToSet( tilemap, null, x, y );
}
/// <summary>
/// What tile sprite index should exist at the given map cell.
/// </summary>
public abstract ushort GetSpriteIndexToSet( TileMap tilemap, TilesetResource tileset, int x, int y );
/// <summary>
/// Get a list of cells that are affected by changing this cell.
/// </summary>
protected abstract IEnumerable<Vector2Int> GetAffectedTiles( Vector2Int cell );
protected List<Vector2Int> RasterizeLine( Vector2Int from, Vector2Int to )
{
var cells = new List<Vector2Int>();
int x0 = from.x;
int y0 = from.y;
int x1 = to.x;
int y1 = to.y;
int dx = Math.Abs( x1 - x0 );
int sx = x0 < x1 ? 1 : -1;
int dy = -Math.Abs( y1 - y0 );
int sy = y0 < y1 ? 1 : -1;
int error = dx + dy;
while ( true )
{
cells.Add( new Vector2Int( x0, y0 ) );
if ( x0 == x1 && y0 == y1 )
break;
int doubledError = error * 2;
if ( doubledError >= dy )
{
error += dy;
x0 += sx;
}
if ( doubledError <= dx )
{
error += dx;
y0 += sy;
}
}
return cells;
}
}