A game about forced loneliness, made by TACStudios
at master 24 kB view raw
1using System; 2using System.Collections.Generic; 3using System.IO; 4using UnityEngine; 5using UnityEngine.Tilemaps; 6using Object = UnityEngine.Object; 7 8namespace UnityEditor.Tilemaps 9{ 10 internal static class TileDragAndDrop 11 { 12 private enum UserTileCreationMode 13 { 14 Overwrite, 15 CreateUnique, 16 Reuse, 17 } 18 19 private static readonly string k_TileExtension = "asset"; 20 21 private static List<Sprite> GetSpritesFromTexture(Texture2D texture) 22 { 23 string path = AssetDatabase.GetAssetPath(texture); 24 Object[] assets = AssetDatabase.LoadAllAssetsAtPath(path); 25 List<Sprite> sprites = new List<Sprite>(); 26 27 foreach (Object asset in assets) 28 { 29 if (asset is Sprite sprite) 30 { 31 sprites.Add(sprite); 32 } 33 } 34 35 return sprites; 36 } 37 38 private static bool AllSpritesAreSameSizeOrMultiples(List<Sprite> sprites) 39 { 40 if (sprites.Count == 0) 41 return false; 42 if (sprites.Count == 1) 43 return true; 44 45 var size = new Vector2(sprites[0].rect.width, sprites[0].rect.height); 46 for (int i = 1; i < sprites.Count; i++) 47 { 48 var rect = sprites[i].rect; 49 if (rect.width < size.x) 50 size.x = rect.width; 51 if (rect.height < size.y) 52 size.y = rect.height; 53 } 54 foreach (var sprite in sprites) 55 { 56 var rect = sprite.rect; 57 if (rect.width % size.x > 0) 58 return false; 59 if (rect.height % size.y > 0) 60 return false; 61 } 62 return true; 63 } 64 65 /// <summary> 66 /// Converts Objects that can be laid out in the Tile Palette and organises them for placement into a given CellLayout 67 /// </summary> 68 /// <param name="sheetTextures">Textures containing 2-N equal sized Sprites</param> 69 /// <param name="singleSprites">All the leftover Sprites that were in same texture but different sizes or just dragged in as Sprite</param> 70 /// <param name="tiles">Just plain Tiles</param> 71 /// <param name="gos">Good old GameObjects</param> 72 /// <param name="cellLayout">Cell Layout to place objects on</param> 73 /// <returns>Dictionary mapping the positions of the Objects on the Grid Layout with details of how to place the Objects</returns> 74 public static Dictionary<Vector2Int, TileDragAndDropHoverData> CreateHoverData(List<Texture2D> sheetTextures 75 , List<Sprite> singleSprites 76 , List<TileBase> tiles 77 , List<GameObject> gos 78 , GridLayout.CellLayout cellLayout) 79 { 80 var result = new Dictionary<Vector2Int, TileDragAndDropHoverData>(); 81 var currentPosition = new Vector2Int(0, 0); 82 var width = 0; 83 84 if (sheetTextures != null) 85 { 86 foreach (var sheetTexture in sheetTextures) 87 { 88 var sheet = CreateHoverData(sheetTexture, cellLayout); 89 foreach (KeyValuePair<Vector2Int, TileDragAndDropHoverData> item in sheet) 90 { 91 result.Add(item.Key + currentPosition, item.Value); 92 } 93 Vector2Int min = GetMinMaxRect(sheet.Keys).min; 94 currentPosition += new Vector2Int(0, min.y - 1); 95 } 96 } 97 if (currentPosition.x > 0) 98 currentPosition = new Vector2Int(0, currentPosition.y - 1); 99 100 if (singleSprites != null) 101 { 102 width = Mathf.RoundToInt(Mathf.Sqrt(singleSprites.Count)); 103 foreach (Sprite sprite in singleSprites) 104 { 105 result.Add(currentPosition, new TileDragAndDropHoverData(sprite)); 106 currentPosition += new Vector2Int(1, 0); 107 if (currentPosition.x >= width) 108 currentPosition = new Vector2Int(0, currentPosition.y - 1); 109 } 110 } 111 if (currentPosition.x > 0) 112 currentPosition = new Vector2Int(0, currentPosition.y - 1); 113 114 if (tiles != null) 115 { 116 width = Math.Max(Mathf.RoundToInt(Mathf.Sqrt(tiles.Count)), width); 117 foreach (TileBase tile in tiles) 118 { 119 result.Add(currentPosition, new TileDragAndDropHoverData(tile)); 120 currentPosition += new Vector2Int(1, 0); 121 if (currentPosition.x >= width) 122 currentPosition = new Vector2Int(0, currentPosition.y - 1); 123 } 124 } 125 if (currentPosition.x > 0) 126 currentPosition = new Vector2Int(0, currentPosition.y - 1); 127 128 if (gos != null) 129 { 130 width = Math.Max(Mathf.RoundToInt(Mathf.Sqrt(gos.Count)), width); 131 foreach (var go in gos) 132 { 133 result.Add(currentPosition, new TileDragAndDropHoverData(go)); 134 currentPosition += new Vector2Int(1, 0); 135 if (currentPosition.x >= width) 136 currentPosition = new Vector2Int(0, currentPosition.y - 1); 137 } 138 } 139 140 return result; 141 } 142 143 // Get all textures that are valid spritesheets. More than one Sprites and all equal size. 144 public static List<Texture2D> GetValidSpritesheets(Object[] objects) 145 { 146 List<Texture2D> result = new List<Texture2D>(); 147 foreach (Object obj in objects) 148 { 149 if (obj is Texture2D texture) 150 { 151 List<Sprite> sprites = GetSpritesFromTexture(texture); 152 if (sprites.Count > 1 && AllSpritesAreSameSizeOrMultiples(sprites)) 153 { 154 result.Add(texture); 155 } 156 } 157 } 158 return result; 159 } 160 161 // Get all single Sprite(s) and all Sprite(s) that are part of Texture2D that is not valid sheet (it sprites of varying sizes) 162 public static List<Sprite> GetValidSingleSprites(Object[] objects) 163 { 164 List<Sprite> result = new List<Sprite>(); 165 foreach (Object obj in objects) 166 { 167 if (obj is Sprite sprite) 168 { 169 result.Add(sprite); 170 } 171 else if (obj is Texture2D texture) 172 { 173 var sprites = GetSpritesFromTexture(texture); 174 if (sprites.Count == 1 || !AllSpritesAreSameSizeOrMultiples(sprites)) 175 { 176 result.AddRange(sprites); 177 } 178 } 179 } 180 return result; 181 } 182 183 public static List<TileBase> GetValidTiles(Object[] objects) 184 { 185 var result = new List<TileBase>(); 186 foreach (var obj in objects) 187 { 188 if (obj is TileBase tileBase) 189 { 190 result.Add(tileBase); 191 } 192 } 193 return result; 194 } 195 196 public static List<GameObject> GetValidGameObjects(Object[] objects) 197 { 198 var result = new List<GameObject>(); 199 foreach (var obj in objects) 200 { 201 if (obj is GameObject gameObject) 202 { 203 result.Add(gameObject); 204 } 205 } 206 return result; 207 } 208 209 public static void FilterForValidGameObjectsForPrefab(Object prefab, List<GameObject> gameObjects) 210 { 211 for (var i = 0; i < gameObjects.Count; ++i) 212 { 213 var go = gameObjects[i]; 214 if (PrefabUtility.IsPartOfAnyPrefab(go)) 215 { 216 if (PrefabUtility.CheckIfAddingPrefabWouldResultInCyclicNesting(prefab, go)) 217 { 218 gameObjects.Remove(go); 219 i--; 220 } 221 } 222 } 223 } 224 225 private static Vector2Int GetMinimum(List<Sprite> sprites, Func<Sprite, float> minX, Func<Sprite, float> minY) 226 { 227 Vector2 minVector = new Vector2(Int32.MaxValue, Int32.MaxValue); 228 foreach (var sprite in sprites) 229 { 230 minVector.x = Mathf.Min(minVector.x, minX(sprite)); 231 minVector.y = Mathf.Min(minVector.y, minY(sprite)); 232 } 233 return Vector2Int.FloorToInt(minVector); 234 } 235 236 public static Vector2Int EstimateGridPixelSize(List<Sprite> sprites) 237 { 238 if (sprites.Count == 0) 239 return Vector2Int.zero; 240 241 foreach (var sprite in sprites) 242 { 243 if (sprite == null) 244 return Vector2Int.zero; 245 } 246 247 if (sprites.Count == 1) 248 return Vector2Int.FloorToInt(sprites[0].rect.size); 249 250 return GetMinimum(sprites, s => s.rect.width, s => s.rect.height); 251 } 252 253 public static Vector2Int EstimateGridOffsetSize(List<Sprite> sprites) 254 { 255 if (sprites.Count == 0) 256 return Vector2Int.zero; 257 258 foreach (var sprite in sprites) 259 { 260 if (sprite == null) 261 return Vector2Int.zero; 262 } 263 264 if (sprites.Count == 1) 265 return Vector2Int.FloorToInt(sprites[0].rect.position); 266 267 return GetMinimum(sprites, s => s.rect.xMin, s => s.rect.yMin); 268 } 269 270 public static Vector2Int EstimateGridPaddingSize(List<Sprite> sprites, Vector2Int cellSize, Vector2Int offsetSize) 271 { 272 if (sprites.Count < 2) 273 return Vector2Int.zero; 274 275 foreach (var sprite in sprites) 276 { 277 if (sprite == null) 278 return Vector2Int.zero; 279 } 280 281 var paddingSize = GetMinimum(sprites 282 , (s => 283 { 284 var xMin = s.rect.xMin - cellSize.x - offsetSize.x; 285 return xMin >= 0 ? xMin : Int32.MaxValue; 286 }) 287 , (s => 288 { 289 var yMin = s.rect.yMin - cellSize.y - offsetSize.y; 290 return yMin >= 0 ? yMin : Int32.MaxValue; 291 }) 292 ); 293 294 // Assume there is no padding if the detected padding is greater than the cell size 295 if (paddingSize.x >= cellSize.x) 296 paddingSize.x = 0; 297 if (paddingSize.y >= cellSize.y) 298 paddingSize.y = 0; 299 return paddingSize; 300 } 301 302 // Turn texture pixel position into integer grid position based on cell size, offset size and padding 303 private static void GetGridPosition(Sprite sprite, Vector2Int cellPixelSize, Vector2Int offsetSize, Vector2Int paddingSize, out Vector2Int cellPosition, out Vector3 positionOffset) 304 { 305 var spritePosition = sprite.rect.position; 306 var spriteCenter = sprite.rect.center; 307 var position = new Vector2( 308 ((spriteCenter.x - offsetSize.x) / (cellPixelSize.x + paddingSize.x)), 309 (-(sprite.texture.height - spriteCenter.y - offsetSize.y) / (cellPixelSize.y + paddingSize.y)) + 1 310 ); 311 cellPosition = new Vector2Int(Mathf.FloorToInt(position.x), Mathf.FloorToInt(position.y)); 312 positionOffset = (spriteCenter - spritePosition) / cellPixelSize; 313 positionOffset.x = (float)(positionOffset.x - Math.Truncate(positionOffset.x)); 314 positionOffset.y = (float)(positionOffset.y - Math.Truncate(positionOffset.y)); 315 } 316 317 // Turn texture pixel position into integer isometric grid position based on cell size and offset size 318 private static void GetIsometricGridPosition(Sprite sprite, Vector2Int cellPixelSize, Vector2Int offsetSize, out Vector2Int cellPosition) 319 { 320 var offsetPosition = new Vector2(sprite.rect.center.x - offsetSize.x, sprite.rect.center.y - offsetSize.y); 321 var cellStride = new Vector2(cellPixelSize.x, cellPixelSize.y) * 0.5f; 322 var invCellStride = new Vector2(1.0f / cellStride.x, 1.0f / cellStride.y); 323 324 var position = offsetPosition * invCellStride; 325 position.y = (position.y - position.x) * 0.5f; 326 position.x += position.y; 327 cellPosition = new Vector2Int(Mathf.FloorToInt(position.x), Mathf.FloorToInt(position.y)); 328 } 329 330 // Organizes all the sprites in a single texture nicely on a 2D "table" based on their original texture position 331 // Only call this with spritesheet with all Sprites equal size 332 public static Dictionary<Vector2Int, TileDragAndDropHoverData> CreateHoverData(Texture2D sheet, GridLayout.CellLayout cellLayout) 333 { 334 var result = new Dictionary<Vector2Int, TileDragAndDropHoverData>(); 335 var sprites = GetSpritesFromTexture(sheet); 336 var cellPixelSize = EstimateGridPixelSize(sprites); 337 338 // Get Offset 339 var offsetSize = EstimateGridOffsetSize(sprites); 340 341 // Get Padding 342 var paddingSize = EstimateGridPaddingSize(sprites, cellPixelSize, offsetSize); 343 344 if ((cellLayout == GridLayout.CellLayout.Isometric 345 || cellLayout == GridLayout.CellLayout.IsometricZAsY) 346 && (HasSpriteRectOverlaps(sprites))) 347 { 348 foreach (Sprite sprite in sprites) 349 { 350 GetIsometricGridPosition(sprite, cellPixelSize, offsetSize, out Vector2Int position); 351 result[position] = new TileDragAndDropHoverData(sprite, Vector3.zero, (Vector2)cellPixelSize / sprite.pixelsPerUnit, false); 352 } 353 } 354 else 355 { 356 foreach (Sprite sprite in sprites) 357 { 358 GetGridPosition(sprite, cellPixelSize, offsetSize, paddingSize, out Vector2Int position, out Vector3 offset); 359 if (cellLayout == GridLayout.CellLayout.Hexagon) 360 offset -= new Vector3(0.5f, 0.5f, 0.0f); 361 result[position] = new TileDragAndDropHoverData(sprite, offset, (Vector2)cellPixelSize / sprite.pixelsPerUnit); 362 } 363 } 364 365 return result; 366 } 367 368 private static bool HasSpriteRectOverlaps(IReadOnlyList<Sprite> sprites) 369 { 370 var count = sprites.Count; 371 for (int i = 0; i < count; i++) 372 { 373 var rect = sprites[i].rect; 374 for (int j = i + 1; j < count; j++) 375 { 376 if (rect.Overlaps(sprites[j].rect)) 377 return true; 378 } 379 } 380 return false; 381 } 382 383 internal static string GenerateUniqueNameForNamelessSprite(Sprite sprite, HashSet<string> uniqueNames, ref int count) 384 { 385 var baseName = "Nameless"; 386 if (sprite.texture != null) 387 baseName = sprite.texture.name; 388 string name; 389 do 390 { 391 name = $"{baseName}_{count++}"; 392 } 393 while (uniqueNames.Contains(name)); 394 return name; 395 } 396 397 public static List<TileBase> ConvertToTileSheet(Dictionary<Vector2Int, TileDragAndDropHoverData> sheet, String tileDirectory = null) 398 { 399 var result = new List<TileBase>(); 400 var defaultPath = TileDragAndDropManager.GetDefaultTileAssetDirectoryPath(); 401 402 // Early out if all objects are already tiles or GOs 403 var sheetCount = sheet.Count; 404 var tileCount = 0; 405 var nonTileCount = 0; 406 string firstName = null; 407 foreach (var sheetData in sheet.Values) 408 { 409 if (sheetData.hoverObject is TileBase) 410 tileCount++; 411 if (sheetData.hoverObject is GameObject) 412 nonTileCount++; 413 if (string.IsNullOrEmpty(firstName) && sheetData.hoverObject != null) 414 firstName = sheetData.hoverObject.name; 415 } 416 if (tileCount == sheetCount) 417 { 418 foreach (var item in sheet.Values) 419 { 420 result.Add(item.hoverObject as TileBase); 421 } 422 } 423 if (tileCount == sheetCount || nonTileCount == sheetCount) 424 return result; 425 426 var userTileCreationMode = UserTileCreationMode.Overwrite; 427 var path = tileDirectory; 428 var multipleTiles = sheetCount > 1; 429 var i = 0; 430 var uniqueNames = new HashSet<string>(); 431 432 if (multipleTiles) 433 { 434 var userInterventionRequired = false; 435 if (string.IsNullOrEmpty(path)) 436 { 437 path = EditorUtility.SaveFolderPanel("Generate tiles into folder ", defaultPath, ""); 438 path = FileUtil.GetProjectRelativePath(path); 439 if (!TilePaletteSaveUtility.ValidateSaveFolder(path)) 440 { 441 return result; 442 } 443 } 444 445 // Check if this will overwrite any existing assets 446 foreach (var item in sheet.Values) 447 { 448 if (item.hoverObject is Sprite sprite) 449 { 450 var name = sprite.name; 451 if (string.IsNullOrEmpty(name) || uniqueNames.Contains(name)) 452 { 453 name = GenerateUniqueNameForNamelessSprite(sprite, uniqueNames, ref i); 454 } 455 uniqueNames.Add(name); 456 var tilePath = FileUtil.CombinePaths(path, string.Format("{0}.{1}", name, k_TileExtension)); 457 if (File.Exists(tilePath)) 458 { 459 userInterventionRequired = true; 460 break; 461 } 462 } 463 } 464 // There are existing tile assets in the folder with names matching the items to be created 465 if (userInterventionRequired) 466 { 467 var option = EditorUtility.DisplayDialogComplex("Overwrite?", string.Format("Assets exist at {0}. Do you wish to overwrite existing assets?", path), "Overwrite", "Create New Copy", "Reuse"); 468 switch (option) 469 { 470 case 0: // Overwrite 471 { 472 userTileCreationMode = UserTileCreationMode.Overwrite; 473 } 474 break; 475 case 1: // Create New Copy 476 { 477 userTileCreationMode = UserTileCreationMode.CreateUnique; 478 } 479 break; 480 case 2: // Reuse 481 { 482 userTileCreationMode = UserTileCreationMode.Reuse; 483 } 484 break; 485 } 486 } 487 } 488 else if (string.IsNullOrEmpty(path)) 489 { 490 // Do not check if this will overwrite new tile as user has explicitly selected the file to save to 491 path = EditorUtility.SaveFilePanelInProject("Generate new tile", firstName, k_TileExtension, "Generate new tile", defaultPath); 492 } 493 TileDragAndDropManager.SetUserTileAssetDirectoryPath(path); 494 495 if (string.IsNullOrEmpty(path)) 496 return result; 497 498 i = 0; 499 uniqueNames.Clear(); 500 EditorUtility.DisplayProgressBar("Generating Tile Assets (" + i + "/" + sheetCount + ")", "Generating tiles", 0f); 501 502 AssetDatabase.StartAssetEditing(); 503 try 504 { 505 var createTileMethod = GridPaintActiveTargetsPreferences.GetCreateTileFromPaletteUsingPreferences(); 506 if (createTileMethod == null) 507 return null; 508 509 foreach (KeyValuePair<Vector2Int, TileDragAndDropHoverData> item in sheet) 510 { 511 TileBase tile; 512 string tilePath = ""; 513 if (item.Value.hoverObject is Sprite sprite) 514 { 515 tile = createTileMethod.Invoke(null, new object[] {sprite}) as TileBase; 516 if (tile == null) 517 continue; 518 519 var name = tile.name; 520 if (string.IsNullOrEmpty(name) || uniqueNames.Contains(name)) 521 { 522 name = GenerateUniqueNameForNamelessSprite(sprite, uniqueNames, ref i); 523 } 524 uniqueNames.Add(name); 525 526 tilePath = multipleTiles || String.IsNullOrWhiteSpace(FileUtil.GetPathExtension(path)) 527 ? FileUtil.CombinePaths(path, $"{name}.{k_TileExtension}") 528 : path; 529 // Case 1216101: Fix path slashes for Windows 530 tilePath = FileUtil.NiceWinPath(tilePath); 531 switch (userTileCreationMode) 532 { 533 case UserTileCreationMode.CreateUnique: 534 { 535 if (File.Exists(tilePath)) 536 tilePath = AssetDatabase.GenerateUniqueAssetPath(tilePath); 537 AssetDatabase.CreateAsset(tile, tilePath); 538 } 539 break; 540 case UserTileCreationMode.Overwrite: 541 { 542 AssetDatabase.CreateAsset(tile, tilePath); 543 } 544 break; 545 case UserTileCreationMode.Reuse: 546 { 547 if (File.Exists(tilePath)) 548 tile = AssetDatabase.LoadAssetAtPath<TileBase>(tilePath); 549 else 550 AssetDatabase.CreateAsset(tile, tilePath); 551 } 552 break; 553 } 554 } 555 else 556 { 557 tile = item.Value.hoverObject as TileBase; 558 } 559 EditorUtility.DisplayProgressBar($"Generating Tile Assets ({i}/{sheet.Count})", $"Generating {tilePath}", (float)i++ / sheet.Count); 560 if (tile != null) 561 result.Add(tile); 562 } 563 } 564 finally 565 { 566 AssetDatabase.StopAssetEditing(); 567 EditorUtility.ClearProgressBar(); 568 } 569 570 AssetDatabase.Refresh(); 571 return result; 572 } 573 574 internal static RectInt GetMinMaxRect(IEnumerable<Vector2Int> positions) 575 { 576 if (positions == null) 577 return new RectInt(); 578 579 var hasValue = false; 580 var min = new Vector2Int(Int32.MaxValue, Int32.MaxValue); 581 var max = new Vector2Int(Int32.MinValue, Int32.MinValue); 582 foreach (var position in positions) 583 { 584 min.x = Math.Min(min.x, position.x); 585 max.x = Math.Max(max.x, position.x); 586 min.y = Math.Min(min.y, position.y); 587 max.y = Math.Max(max.y, position.y); 588 hasValue = true; 589 } 590 return hasValue ? GridEditorUtility.GetMarqueeRect(min, max) : new RectInt(); 591 } 592 } 593}