Seige tower

This commit is contained in:
Ben Calegari 2026-06-23 22:57:15 -07:00
parent 430e66bfbf
commit 4f57499e40
18 changed files with 898 additions and 92 deletions

View file

@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: 2bef66c4dde743cc87f8272b863c12d4
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View file

@ -0,0 +1,190 @@
// 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
{
/// <summary>
/// Editor utility that renders each <see cref="TowerDefinition"/>'s prefab to a square
/// thumbnail PNG and assigns it back to the definition's <c>Icon</c> field, so the HUD
/// build menu shows "how the building looks when built" on each tower tile.
/// </summary>
/// <remarks>
/// Run via <b>TD &gt; Generate Tower Icons</b>. Uses <see cref="PreviewRenderUtility"/>
/// (synchronous, controllable) rather than the async <c>AssetPreview</c> API so the whole
/// pass completes within the menu callback. Generated PNGs are written to
/// <c>Assets/_Project/Art/Sprites/Towers/</c>, imported as Sprites, and wired onto the
/// matching asset. Re-running regenerates and overwrites in place.
/// </remarks>
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<TowerDefinition>(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<Sprite>(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<Renderer>();
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;
}
}
}
}

View file

@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: de83f68d5958449185d2a0c8199c41ec
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: