using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Tilemaps; using System.Linq; namespace UnityEditor.Tilemaps { /// /// This Brush helps draw lines of Tiles onto a Tilemap. /// Use this as an example to modify brush painting behaviour to making painting quicker with less actions. /// [HelpURL("https://docs.unity3d.com/Packages/com.unity.2d.tilemap.extras@latest/index.html?subfolder=/manual/LineBrush.html")] [CustomGridBrush(true, false, false, "Line Brush")] public class LineBrush : GridBrush { /// /// Whether the Line Brush has started drawing a line. /// public bool lineStartActive; /// /// Ensures that there are orthogonal connections of Tiles from the start of the line to the end. /// public bool fillGaps; /// /// The current starting point of the line. /// public Vector3Int lineStart = Vector3Int.zero; /// /// Indicates whether the brush is currently /// moving something using the "Move selection with active brush" tool. /// public bool IsMoving { get; private set; } /// /// Paints tiles and GameObjects into a given position within the selected layers. /// The LineBrush overrides this to provide line painting functionality. /// The first paint action sets the starting point of the line. /// The next paint action sets the ending point of the line and paints Tile from start to end. /// /// Grid used for layout. /// Target of the paint operation. By default the currently selected GameObject. /// The coordinates of the cell to paint data to. public override void Paint(GridLayout grid, GameObject brushTarget, Vector3Int position) { if (lineStartActive) { Vector2Int startPos = new Vector2Int(lineStart.x, lineStart.y); Vector2Int endPos = new Vector2Int(position.x, position.y); if (startPos == endPos) base.Paint(grid, brushTarget, position); else { foreach (var point in GetPointsOnLine(startPos, endPos, fillGaps)) { Vector3Int paintPos = new Vector3Int(point.x, point.y, position.z); base.Paint(grid, brushTarget, paintPos); } } lineStartActive = false; } else if (IsMoving) { base.Paint(grid, brushTarget, position); } else { lineStart = position; lineStartActive = true; } } /// /// Starts the movement of tiles and GameObjects from a given position within the selected layers. /// /// Grid used for layout. /// Target of the Move operation. By default the currently selected GameObject. /// The coordinates of the cell to move data from. public override void MoveStart(GridLayout grid, GameObject brushTarget, BoundsInt position) { base.MoveStart(grid, brushTarget, position); IsMoving = true; } /// /// Ends the movement of tiles and GameObjects to a given position within the selected layers. /// /// Grid used for layout. /// Target of the Move operation. By default the currently selected GameObject. /// The coordinates of the cell to move data to. public override void MoveEnd(GridLayout grid, GameObject brushTarget, BoundsInt position) { base.MoveEnd(grid, brushTarget, position); IsMoving = false; } /// /// Enumerates all the points between the start and end position which are /// linked diagonally or orthogonally. /// /// Start position of the line. /// End position of the line. /// Fills any gaps between the start and end position so that /// all points are linked only orthogonally. /// Returns an IEnumerable which enumerates all the points between the start and end position which are /// linked diagonally or orthogonally. public static IEnumerable GetPointsOnLine(Vector2Int startPos, Vector2Int endPos, bool fillGaps) { var points = GetPointsOnLine(startPos, endPos); if (fillGaps) { var rise = endPos.y - startPos.y; var run = endPos.x - startPos.x; if (rise != 0 || run != 0) { var extraStart = startPos; var extraEnd = endPos; if (Mathf.Abs(rise) >= Mathf.Abs(run)) { // up if (rise > 0) { extraStart.y += 1; extraEnd.y += 1; } // down else // rise < 0 { extraStart.y -= 1; extraEnd.y -= 1; } } else // Mathf.Abs(rise) < Mathf.Abs(run) { // right if (run > 0) { extraStart.x += 1; extraEnd.x += 1; } // left else // run < 0 { extraStart.x -= 1; extraEnd.x -= 1; } } var extraPoints = GetPointsOnLine(extraStart, extraEnd); extraPoints = extraPoints.Except(new[] { extraEnd }); points = points.Union(extraPoints); } } return points; } /// /// Gets an enumerable for all the cells directly between two points /// http://ericw.ca/notes/bresenhams-line-algorithm-in-csharp.html /// /// A starting point of a line /// An ending point of a line /// Gets an enumerable for all the cells directly between two points public static IEnumerable GetPointsOnLine(Vector2Int p1, Vector2Int p2) { int x0 = p1.x; int y0 = p1.y; int x1 = p2.x; int y1 = p2.y; bool steep = Math.Abs(y1 - y0) > Math.Abs(x1 - x0); if (steep) { int t; t = x0; // swap x0 and y0 x0 = y0; y0 = t; t = x1; // swap x1 and y1 x1 = y1; y1 = t; } if (x0 > x1) { int t; t = x0; // swap x0 and x1 x0 = x1; x1 = t; t = y0; // swap y0 and y1 y0 = y1; y1 = t; } int dx = x1 - x0; int dy = Math.Abs(y1 - y0); int error = dx / 2; int ystep = (y0 < y1) ? 1 : -1; int y = y0; for (int x = x0; x <= x1; x++) { yield return new Vector2Int((steep ? y : x), (steep ? x : y)); error = error - dy; if (error < 0) { y += ystep; error += dx; } } yield break; } } /// /// The Brush Editor for a Line Brush. /// [CustomEditor(typeof(LineBrush))] public class LineBrushEditor : GridBrushEditor { private LineBrush lineBrush { get { return target as LineBrush; } } private Tilemap lastTilemap; /// /// Callback for painting the GUI for the GridBrush in the Scene View. /// The CoordinateBrush Editor overrides this to draw the preview of the brush when drawing lines. /// /// Grid that the brush is being used on. /// Target of the GridBrushBase::ref::Tool operation. By default the currently selected GameObject. /// Current selected location of the brush. /// Current GridBrushBase::ref::Tool selected. /// Whether brush is being used. public override void OnPaintSceneGUI(GridLayout grid, GameObject brushTarget, BoundsInt position, GridBrushBase.Tool tool, bool executing) { base.OnPaintSceneGUI(grid, brushTarget, position, tool, executing); if (lineBrush.lineStartActive && brushTarget != null) { Tilemap tilemap = brushTarget.GetComponent(); if (tilemap != null) { tilemap.ClearAllEditorPreviewTiles(); lastTilemap = tilemap; } // Draw preview tiles for tilemap Vector2Int startPos = new Vector2Int(lineBrush.lineStart.x, lineBrush.lineStart.y); Vector2Int endPos = new Vector2Int(position.x, position.y); if (startPos == endPos) PaintPreview(grid, brushTarget, position.min); else { foreach (var point in LineBrush.GetPointsOnLine(startPos, endPos, lineBrush.fillGaps)) { Vector3Int paintPos = new Vector3Int(point.x, point.y, position.z); PaintPreview(grid, brushTarget, paintPos); } } if (Event.current.type == EventType.Repaint) { var min = lineBrush.lineStart; var max = lineBrush.lineStart + position.size; // Draws a box on the picked starting position GL.PushMatrix(); GL.MultMatrix(GUI.matrix); GL.Begin(GL.LINES); Handles.color = Color.blue; Handles.DrawLine(new Vector3(min.x, min.y, min.z), new Vector3(max.x, min.y, min.z)); Handles.DrawLine(new Vector3(max.x, min.y, min.z), new Vector3(max.x, max.y, min.z)); Handles.DrawLine(new Vector3(max.x, max.y, min.z), new Vector3(min.x, max.y, min.z)); Handles.DrawLine(new Vector3(min.x, max.y, min.z), new Vector3(min.x, min.y, min.z)); GL.End(); GL.PopMatrix(); } } } /// /// Clears all line previews. /// public override void ClearPreview() { base.ClearPreview(); if (lastTilemap != null) { lastTilemap.ClearAllEditorPreviewTiles(); lastTilemap = null; } } } }