using System; using System.Collections.Generic; using UnityEngine; using UnityEngine.Tilemaps; using Object = UnityEngine.Object; namespace UnityEditor.Tilemaps { /// /// Utility Class for creating Palettes /// public static class GridPaletteUtility { internal enum GridPaletteType { Rectangle, HexagonalPointTop, HexagonalFlatTop, Isometric, IsometricZAsY, }; internal static readonly Vector3 defaultSortAxis = new Vector3(0f, 0f, 1f); internal static GridLayout.CellLayout GetCellLayoutFromGridPaletteType(GridPaletteType paletteType) { switch (paletteType) { case GridPaletteType.HexagonalPointTop: case GridPaletteType.HexagonalFlatTop: { return GridLayout.CellLayout.Hexagon; } case GridPaletteType.Isometric: { return GridLayout.CellLayout.Isometric; } case GridPaletteType.IsometricZAsY: { return GridLayout.CellLayout.IsometricZAsY; } } return GridLayout.CellLayout.Rectangle; } internal static RectInt GetBounds(GameObject palette) { if (palette == null) return new RectInt(); Vector2Int min = new Vector2Int(int.MaxValue, int.MaxValue); Vector2Int max = new Vector2Int(int.MinValue, int.MinValue); foreach (var tilemap in palette.GetComponentsInChildren()) { Vector3Int p1 = tilemap.editorPreviewOrigin; Vector3Int p2 = p1 + tilemap.editorPreviewSize; Vector2Int tilemapMin = new Vector2Int(Mathf.Min(p1.x, p2.x), Mathf.Min(p1.y, p2.y)); Vector2Int tilemapMax = new Vector2Int(Mathf.Max(p1.x, p2.x), Mathf.Max(p1.y, p2.y)); min = new Vector2Int(Mathf.Min(min.x, tilemapMin.x), Mathf.Min(min.y, tilemapMin.y)); max = new Vector2Int(Mathf.Max(max.x, tilemapMax.x), Mathf.Max(max.y, tilemapMax.y)); } return GridEditorUtility.GetMarqueeRect(min, max); } /// /// Creates a Palette Asset at the current selected folder path. This will show a popup allowing you to choose /// a different folder path for saving the Palette Asset if required. /// /// Name of the Palette Asset. /// Grid Layout of the Palette Asset. /// Cell Sizing of the Palette Asset. /// Cell Size of the Palette Asset. /// Cell Swizzle of the Palette. /// The created Palette Asset if successful. public static GameObject CreateNewPaletteAtCurrentFolder(string name, GridLayout.CellLayout layout, GridPalette.CellSizing cellSizing, Vector3 cellSize, GridLayout.CellSwizzle swizzle) { return CreateNewPaletteAtCurrentFolder(name, layout, cellSizing, cellSize, swizzle , TransparencySortMode.Default, defaultSortAxis); } /// /// Creates a Palette Asset at the current selected folder path. This will show a popup allowing you to choose /// a different folder path for saving the Palette Asset if required. /// /// Name of the Palette Asset. /// Grid Layout of the Palette Asset. /// Cell Sizing of the Palette Asset. /// Cell Size of the Palette Asset. /// Cell Swizzle of the Palette. /// Transparency Sort Mode for the Palette /// Transparency Sort Axis for the Palette /// The created Palette Asset if successful. public static GameObject CreateNewPaletteAtCurrentFolder(string name , GridLayout.CellLayout layout , GridPalette.CellSizing cellSizing , Vector3 cellSize , GridLayout.CellSwizzle swizzle , TransparencySortMode sortMode , Vector3 sortAxis) { string defaultPath = ProjectBrowser.s_LastInteractedProjectBrowser ? ProjectBrowser.s_LastInteractedProjectBrowser.GetActiveFolderPath() : "Assets"; string folderPath = EditorUtility.SaveFolderPanel("Create palette into folder ", defaultPath, ""); folderPath = FileUtil.GetProjectRelativePath(folderPath); if (!TilePaletteSaveUtility.ValidateSaveFolder(folderPath)) return null; return CreateNewPalette(folderPath, name, layout, cellSizing, cellSize, swizzle, sortMode, sortAxis); } /// /// Creates a Palette Asset at the given folder path. /// /// Folder Path of the Palette Asset. /// Name of the Palette Asset. /// Grid Layout of the Palette Asset. /// Cell Sizing of the Palette Asset. /// Cell Size of the Palette Asset. /// Cell Swizzle of the Palette. /// The created Palette Asset if successful. public static GameObject CreateNewPalette(string folderPath , string name , GridLayout.CellLayout layout , GridPalette.CellSizing cellSizing , Vector3 cellSize , GridLayout.CellSwizzle swizzle) { return CreateNewPalette(folderPath, name, layout, cellSizing, cellSize, swizzle, TransparencySortMode.Default, defaultSortAxis); } /// /// Creates a Palette Asset at the given folder path. /// /// Folder Path of the Palette Asset. /// Name of the Palette Asset. /// Grid Layout of the Palette Asset. /// Cell Sizing of the Palette Asset. /// Cell Size of the Palette Asset. /// Cell Swizzle of the Palette. /// Transparency Sort Mode for the Palette /// Transparency Sort Axis for the Palette /// The created Palette Asset if successful. public static GameObject CreateNewPalette(string folderPath , string name , GridLayout.CellLayout layout , GridPalette.CellSizing cellSizing , Vector3 cellSize , GridLayout.CellSwizzle swizzle , TransparencySortMode sortMode , Vector3 sortAxis) { var temporaryGO = CreateNewPaletteGameObject(name, layout, cellSize, swizzle); var path = AssetDatabase.GenerateUniqueAssetPath(folderPath + "/" + name + ".prefab"); var prefab = PrefabUtility.SaveAsPrefabAssetAndConnect(temporaryGO, path, InteractionMode.AutomatedAction); var gridPalette = CreateGridPalette(cellSizing, sortMode, sortAxis); AssetDatabase.AddObjectToAsset(gridPalette, prefab); PrefabUtility.ApplyPrefabInstance(temporaryGO, InteractionMode.AutomatedAction); AssetDatabase.Refresh(); Object.DestroyImmediate(temporaryGO); return AssetDatabase.LoadAssetAtPath(path); } internal static GameObject CreateNewPaletteGameObject(string name , GridLayout.CellLayout layout , Vector3 cellSize , GridLayout.CellSwizzle swizzle) { var temporaryGO = new GameObject(name); var grid = temporaryGO.AddComponent(); grid.cellSize = cellSize; grid.cellLayout = layout; grid.cellSwizzle = swizzle; CreateNewLayer(temporaryGO, "Layer1", layout); return temporaryGO; } internal static IEnumerable<(T1, T2, T3)> MultipleEnumerate(IEnumerable t1s, IEnumerable t2s, IEnumerable t3s) { using IEnumerator enum1 = t1s.GetEnumerator(); using IEnumerator enum2 = t2s.GetEnumerator(); using IEnumerator enum3 = t3s.GetEnumerator(); while (enum1.MoveNext() && enum2.MoveNext() && enum3.MoveNext()) yield return (enum1.Current, enum2.Current, enum3.Current); } /// /// Creates a Palette Asset to be used as a sub-asset. /// /// Name of the Palette Asset. /// Grid Layout of the Palette Asset. /// Cell Sizing of the Palette Asset. /// Cell Size of the Palette Asset. /// Cell Swizzle of the Palette. /// Transparency Sort Mode for the Palette /// Transparency Sort Axis for the Palette /// Texture Sources of Sprites /// Sprites to add to the Palette, organised by containing Texture /// Palette Settings asset created if successful /// Tile assets created if successful /// The created Palette Asset if successful. internal static GameObject CreateNewPaletteAsSubAsset(string name , GridLayout.CellLayout layout , GridPalette.CellSizing cellSizing , Vector3 cellSize , GridLayout.CellSwizzle swizzle , TransparencySortMode sortMode , Vector3 sortAxis , IEnumerable texture2Ds , IEnumerable> textureSprites , IEnumerable templates , out GridPalette palette , out List tiles) { palette = null; tiles = null; if (texture2Ds == null || textureSprites == null || templates == null) return null; var createTileMethod = GridPaintActiveTargetsPreferences.GetCreateTileFromPaletteUsingPreferences(); var paletteGO = new GameObject(name); var grid = paletteGO.AddComponent(); grid.cellSize = cellSize; grid.cellLayout = layout; grid.cellSwizzle = swizzle; var layer = CreateNewLayer(paletteGO, "Layer1", layout); var tilemap = layer.GetComponent(); palette = CreateGridPalette(cellSizing, sortMode, sortAxis); var tilesSet = new HashSet(); var paletteOffset = Vector3Int.zero; var uniqueNames = new HashSet(); var itemCount = 0; foreach (var (_, _, _) in MultipleEnumerate(texture2Ds, textureSprites, templates)) { itemCount++; } var square = Mathf.CeilToInt(Mathf.Sqrt(itemCount)); var height = 0; foreach (var (texture2D, textureSprite, tileTemplate) in MultipleEnumerate(texture2Ds, textureSprites, templates)) { if (texture2D == null || textureSprite == null) continue; var tileChangeData = new List(); if (tileTemplate != null) { try { tileTemplate.CreateTileAssets(texture2D, textureSprite, ref tileChangeData); } catch (Exception e) { Debug.LogError($"Unable to create Tile Assets for {tileTemplate} using {texture2D} with error: {e.ToString()}", tileTemplate); } var x = Int32.MaxValue; var y = Int32.MinValue; foreach (var tileChange in tileChangeData) { var position = tileChange.position; x = Math.Min(x, position.x); y = Math.Max(y, position.y); } var localOffset = new Vector3Int(-x, -y, 0); for (var i = 0; i < tileChangeData.Count; ++i) { var tileChange = tileChangeData[i]; tileChange.position += paletteOffset + localOffset; tileChangeData[i] = tileChange; tilesSet.Add(tileChange.tile); } } else { if (createTileMethod == null) continue; var sprites = new List(); foreach (var sprite in textureSprite) { sprites.Add(sprite); } var ratio = TileDragAndDrop.GetRatioOfSameSizedSprites(sprites); Dictionary hoverData = null; var tileMap = new Dictionary(); // Use Texture position to place sprites if they are same sized if (GridPalette.CellSizing.Automatic == cellSizing && ratio >= 0.66f) { hoverData = TileDragAndDrop.CreateHoverData(sprites, layout); } else { hoverData = new Dictionary(); var width = Mathf.RoundToInt(Mathf.Sqrt(sprites.Count)); var currentPosition = Vector2Int.zero; foreach (Sprite sprite in sprites) { hoverData.Add(currentPosition, new TileDragAndDropHoverData(sprite, tilemap.tileAnchor, Vector3.one, true)); currentPosition += new Vector2Int(1, 0); if (currentPosition.x >= width) currentPosition = new Vector2Int(0, currentPosition.y - 1); } } var y = Int32.MinValue; foreach (var key in hoverData.Keys) { y = Math.Max(y, key.y); } foreach (var item in hoverData) { var i = 0; if (item.Value.hoverObject is Sprite sprite) { if (!tileMap.TryGetValue(sprite, out TileBase tile)) { tile = createTileMethod.Invoke(null, new object[] { sprite }) as TileBase; tileMap.Add(sprite, tile); } if (tile == null) continue; var tileName = tile.name; if (string.IsNullOrEmpty(tileName) || uniqueNames.Contains(tileName)) { tileName = TileDragAndDrop.GenerateUniqueNameForNamelessSprite(sprite, uniqueNames, ref i); tile.name = tileName; } uniqueNames.Add(tileName); tileChangeData.Add(new TileChangeData() { position = new Vector3Int(item.Key.x, item.Key.y - y, 0) + paletteOffset, tile = tile, transform = Matrix4x4.TRS(item.Value.positionOffset - tilemap.tileAnchor, Quaternion.identity, Vector3.one), color = Color.white }); tilesSet.Add(tile); } } } if (tileChangeData.Count > 0) tilemap.SetTiles(tileChangeData.ToArray(), true); if (++height == square) { height = 0; paletteOffset.y = 0; paletteOffset.x = tilemap.size.x; } else { var y = 0; foreach (var changeData in tileChangeData) { y = Math.Min(y, changeData.position.y); } paletteOffset.y = y - 1; } } tilemap.CompressBounds(); tiles = new List(tilesSet); return paletteGO; } private static GameObject CreateNewLayer(GameObject paletteGO, string name, GridLayout.CellLayout layout) { GameObject newLayerGO = new GameObject(name); var tilemap = newLayerGO.AddComponent(); var renderer = newLayerGO.AddComponent(); newLayerGO.transform.parent = paletteGO.transform; newLayerGO.layer = paletteGO.layer; // Set defaults for certain layouts switch (layout) { case GridLayout.CellLayout.Hexagon: { tilemap.tileAnchor = Vector3.zero; break; } case GridLayout.CellLayout.Isometric: { renderer.sortOrder = TilemapRenderer.SortOrder.TopRight; break; } case GridLayout.CellLayout.IsometricZAsY: { renderer.sortOrder = TilemapRenderer.SortOrder.TopRight; renderer.mode = TilemapRenderer.Mode.Individual; break; } } return newLayerGO; } internal static GridPalette GetGridPaletteFromPaletteAsset(Object palette) { string assetPath = AssetDatabase.GetAssetPath(palette); GridPalette paletteAsset = AssetDatabase.LoadAssetAtPath(assetPath); return paletteAsset; } internal static GridPalette CreateGridPalette(GridPalette.CellSizing cellSizing) { return CreateGridPalette(cellSizing, TransparencySortMode.Default, defaultSortAxis); } internal static GridPalette CreateGridPalette(GridPalette.CellSizing cellSizing , TransparencySortMode sortMode , Vector3 sortAxis ) { var palette = ScriptableObject.CreateInstance(); palette.name = "Palette Settings"; palette.cellSizing = cellSizing; palette.transparencySortMode = sortMode; palette.transparencySortAxis = sortAxis; return palette; } internal static Vector3 CalculateAutoCellSize(Grid grid, Vector3 defaultValue) { Tilemap[] tilemaps = grid.GetComponentsInChildren(); Sprite[] sprites = null; var maxSize = Vector2.negativeInfinity; var minSize = Vector2.positiveInfinity; // Get minimum and maximum sizes for Sprites foreach (var tilemap in tilemaps) { var spriteCount = tilemap.GetUsedSpritesCount(); if (sprites == null || sprites.Length < spriteCount) sprites = new Sprite[spriteCount]; tilemap.GetUsedSpritesNonAlloc(sprites); for (int i = 0; i < spriteCount; ++i) { Sprite sprite = sprites[i]; if (sprite != null) { var cellSize = new Vector3(sprite.rect.width, sprite.rect.height, 0f) / sprite.pixelsPerUnit; if (tilemap.cellSwizzle == GridLayout.CellSwizzle.YXZ) { (cellSize.x, cellSize.y) = (cellSize.y, cellSize.x); } minSize.x = Mathf.Min(cellSize.x, minSize.x); minSize.y = Mathf.Min(cellSize.y, minSize.y); maxSize.x = Mathf.Max(cellSize.x, maxSize.x); maxSize.y = Mathf.Max(cellSize.y, maxSize.y); } } } // Validate that Sprites are in multiples of sizes foreach (var tilemap in tilemaps) { var spriteCount = tilemap.GetUsedSpritesCount(); if (sprites == null || sprites.Length < spriteCount) sprites = new Sprite[spriteCount]; tilemap.GetUsedSpritesNonAlloc(sprites); for (int i = 0; i < spriteCount; ++i) { Sprite sprite = sprites[i]; if (sprite != null) { var cellSize = new Vector3(sprite.rect.width, sprite.rect.height, 0f) / sprite.pixelsPerUnit; if (tilemap.cellSwizzle == GridLayout.CellSwizzle.YXZ) { var swap = cellSize.x; cellSize.x = cellSize.y; cellSize.y = swap; } // Return maximum size if sprites are not multiples of the smallest size if (cellSize.x % minSize.x > 0) return maxSize.x * maxSize.y <= 0f ? defaultValue : new Vector3(maxSize.x, maxSize.y, 0f); if (cellSize.y % minSize.y > 0) return maxSize.x * maxSize.y <= 0f ? defaultValue : new Vector3(maxSize.x, maxSize.y, 0f); } } } return minSize.x * minSize.y <= 0f || minSize == Vector2.positiveInfinity ? defaultValue : new Vector3(minSize.x, minSize.y, 0f); } } }