// Assets/_Project/Scripts/Editor/Towers/TowerIconGenerator.cs using System.Collections.Generic; using System.IO; using UnityEditor; using UnityEngine; using TD.Towers; namespace TD.Towers.Editor { /// /// Editor utility that renders each 's prefab to a square /// thumbnail PNG and assigns it back to the definition's Icon field, so the HUD /// build menu shows "how the building looks when built" on each tower tile. /// /// /// Run via TD > Generate Tower Icons. Uses /// (synchronous, controllable) rather than the async AssetPreview API so the whole /// pass completes within the menu callback. Generated PNGs are written to /// Assets/_Project/Art/Sprites/Towers/, imported as Sprites, and wired onto the /// matching asset. Re-running regenerates and overwrites in place. /// public static class TowerIconGenerator { private const string OutputFolder = "Assets/_Project/Art/Sprites/Towers"; private const int IconSize = 128; [MenuItem("TD/Generate Tower Icons")] public static void GenerateAll() { EnsureFolder(OutputFolder); string[] guids = AssetDatabase.FindAssets("t:TowerDefinition"); if (guids.Length == 0) { Debug.LogWarning("[TowerIconGenerator] No TowerDefinition assets found."); return; } int generated = 0; try { foreach (string guid in guids) { string defPath = AssetDatabase.GUIDToAssetPath(guid); var def = AssetDatabase.LoadAssetAtPath(defPath); if (def == null || def.TowerPrefab == null) { Debug.LogWarning($"[TowerIconGenerator] Skipping '{defPath}' — " + "missing TowerDefinition or TowerPrefab."); continue; } Texture2D icon = RenderPrefabIcon(def.TowerPrefab); if (icon == null) continue; string pngPath = $"{OutputFolder}/{def.name}.png"; File.WriteAllBytes(pngPath, icon.EncodeToPNG()); Object.DestroyImmediate(icon); AssetDatabase.ImportAsset(pngPath, ImportAssetOptions.ForceSynchronousImport); ConfigureAsSprite(pngPath); var sprite = AssetDatabase.LoadAssetAtPath(pngPath); AssignIcon(def, sprite); generated++; } } finally { AssetDatabase.SaveAssets(); AssetDatabase.Refresh(); } Debug.Log($"[TowerIconGenerator] Generated {generated} tower icon(s) into {OutputFolder}."); } // ----- Rendering -------------------------------------------------- private static Texture2D RenderPrefabIcon(GameObject prefab) { var pru = new PreviewRenderUtility(); GameObject instance = null; try { instance = Object.Instantiate(prefab); instance.hideFlags = HideFlags.HideAndDontSave; pru.AddSingleGO(instance); if (!TryGetWorldBounds(instance, out Bounds bounds)) { Debug.LogWarning($"[TowerIconGenerator] '{prefab.name}' has no renderers to capture."); return null; } // 3/4 view framed to the prefab's combined bounds. const float fov = 30f; float radius = Mathf.Max(bounds.extents.magnitude, 0.01f); float distance = radius / Mathf.Sin(Mathf.Deg2Rad * fov * 0.5f) * 1.15f; Quaternion dir = Quaternion.Euler(20f, 135f, 0f); var cam = pru.camera; cam.fieldOfView = fov; cam.transform.rotation = dir; cam.transform.position = bounds.center - dir * Vector3.forward * distance; cam.nearClipPlane = 0.01f; cam.farClipPlane = distance * 4f; cam.clearFlags = CameraClearFlags.SolidColor; cam.backgroundColor = new Color(0f, 0f, 0f, 0f); // transparent pru.lights[0].intensity = 1.2f; pru.lights[0].transform.rotation = Quaternion.Euler(40f, 40f, 0f); pru.lights[1].intensity = 0.6f; pru.ambientColor = new Color(0.35f, 0.35f, 0.35f, 1f); var rect = new Rect(0, 0, IconSize, IconSize); pru.BeginPreview(rect, GUIStyle.none); pru.camera.Render(); Texture rendered = pru.EndPreview(); var result = new Texture2D(IconSize, IconSize, TextureFormat.RGBA32, false); RenderTexture prev = RenderTexture.active; RenderTexture.active = rendered as RenderTexture; result.ReadPixels(rect, 0, 0); result.Apply(); RenderTexture.active = prev; return result; } finally { if (instance != null) Object.DestroyImmediate(instance); pru.Cleanup(); } } private static bool TryGetWorldBounds(GameObject go, out Bounds bounds) { var renderers = go.GetComponentsInChildren(); bounds = default; bool any = false; foreach (var r in renderers) { // Skip projector/decal-style renderers that don't represent the tower body. if (r is MeshRenderer || r is SkinnedMeshRenderer) { if (!any) { bounds = r.bounds; any = true; } else { bounds.Encapsulate(r.bounds); } } } return any; } // ----- Asset wiring ----------------------------------------------- private static void ConfigureAsSprite(string pngPath) { var importer = AssetImporter.GetAtPath(pngPath) as TextureImporter; if (importer == null) return; importer.textureType = TextureImporterType.Sprite; importer.spriteImportMode = SpriteImportMode.Single; importer.alphaIsTransparency = true; importer.mipmapEnabled = false; importer.SaveAndReimport(); } private static void AssignIcon(TowerDefinition def, Sprite sprite) { if (sprite == null) return; var so = new SerializedObject(def); so.FindProperty("Icon").objectReferenceValue = sprite; so.ApplyModifiedPropertiesWithoutUndo(); EditorUtility.SetDirty(def); } private static void EnsureFolder(string assetFolder) { if (AssetDatabase.IsValidFolder(assetFolder)) return; string[] parts = assetFolder.Split('/'); string current = parts[0]; // "Assets" for (int i = 1; i < parts.Length; i++) { string next = $"{current}/{parts[i]}"; if (!AssetDatabase.IsValidFolder(next)) AssetDatabase.CreateFolder(current, parts[i]); current = next; } } } }