A game about forced loneliness, made by TACStudios
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}