// 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;
}
}
}
}