UnityVATTool/Assets/Common/Scripts/Editor/EnemyVATConverter.cs

369 lines
16 KiB
C#

/*
* EnemyVATConverter
*
* 一键给 Stage 1 的 6 种小怪挂上 VAT 视觉层:
* 1) 创建 Enemy_VAT 子目录(Variants/),复制原 prefab 为 _VAT.prefab
* 2) 在 _VAT.prefab 上加 EnemyVATAttachment 组件,引用 mon1001_VAT.prefab
* 3) 复制 EnemiesDatabase.asset 为 EnemiesDatabaseVAT.asset, 把 type 0/1/2/3/4/8 的 prefab 指向 _VAT 版本
*
* 注意: 不修改源 prefab/database。新文件落在:
* Assets/Common/Prefabs/Enemies/Variants_VAT/Enemy <Name>_VAT.prefab
* Assets/Common/Scriptables/Enemies Database VAT.asset
*
* 用法:
* 菜单 "Tools/OctoberStudio/VAT/Generate Stage1 VAT Enemies"
* 生成后:再用 Tools/OctoberStudio/VAT/Set Stage1 EnemiesDatabase To VAT(替换 GameController 中的引用)
*/
using System;
using System.Collections.Generic;
using System.IO;
using UnityEditor;
using UnityEditor.SceneManagement;
using UnityEngine;
using UnityEngine.SceneManagement;
namespace OctoberStudio.EditorTools
{
public static class EnemyVATConverter
{
private const string SourceEnemiesDir = "Assets/Common/Prefabs/Enemies";
private const string OutputDir = "Assets/Common/Prefabs/Enemies/Variants_VAT";
private const string VATPrefabPath = "Assets/_VAT_Baking/Output/mon1001/mon1001_VAT.prefab";
private const string SourceDBPath = "Assets/Common/Scriptables/Enemies Database.asset";
private const string OutputDBPath = "Assets/Common/Scriptables/Enemies Database VAT.asset";
// mon1001 mesh bounds.y ≈ 261(单位 cm * 0.01),实际渲染高度 2.61m;
// Survivors 原 sprite 怪大约 0.5~0.8m 高;0.4 让 VAT 怪约 ~1m 高,比 sprite 怪稍大一点更醒目
private static readonly Vector3 DefaultVATLocalScale = new Vector3(0.4f, 0.4f, 0.4f);
private static readonly Vector3 DefaultVATLocalOffset = new Vector3(0f, 0f, 0f);
// Stage 1 用到的 EnemyType: Pumpkin=0, Bat=1, Slime=2, Vampire=3, Plant=4, Bug=8
// 对应 prefab 文件名
private static readonly Dictionary<string, string> Stage1EnemyPrefabs = new Dictionary<string, string>
{
{ "Pumpkin", "Enemy Pumpkin.prefab" },
{ "Bat", "Enemy Bat.prefab" },
{ "Slime", "Enemy Slime.prefab" },
{ "Vampire", "Enemy Vampire.prefab" },
{ "Plant", "Enemy Plant.prefab" },
{ "Bug", "Enemy Bug.prefab" },
};
// 全 17 个 EnemyType → 源 sprite prefab 名 + 映射到的 mon* VAT(按枚举定义顺序一一对应)
// mon* 数量正好 17,与 EnemyType 一一对齐
private static readonly (string enumName, string srcPrefab, string monName)[] AllEnemyMapping =
{
("Pumpkin", "Enemy Pumpkin.prefab", "mon1001"),
("Bat", "Enemy Bat.prefab", "mon1002"),
("Slime", "Enemy Slime.prefab", "mon1003_02"),
("Vampire", "Enemy Vampire.prefab", "mon1004_01"),
("Plant", "Enemy Plant.prefab", "mon1008"),
("Jellyfish", "Enemy Jellyfish.prefab", "mon1009"),
("Bug", "Enemy Bug.prefab", "mon1010"),
("Wasp", "Enemy Wasp.prefab", "mon1012"),
("Hand", "Enemy Hand.prefab", "mon1014"),
("Eye", "Enemy Eye.prefab", "mon1015"),
("FireSlime", "Enemy Fire Slime.prefab", "mon1016"),
("PurpleJellyfish", "Enemy Jellyfish Purple.prefab", "mon1021"),
("StagBeetle", "Enemy Stag Beetle.prefab", "mon1025"),
("Shade", "Enemy Shade.prefab", "mon1030"),
("ShadeJellyfish", "Enemy Jellyfish Shade.prefab", "mon1031"),
("ShadeBat", "Enemy Bat Shade.prefab", "mon1032"),
("ShadeVampire", "Enemy Vampire Shade.prefab", "mon1033"),
};
private const string VATBakedDir = "Assets/_VAT_Baking/Output";
[MenuItem("Tools/OctoberStudio/VAT/Generate ALL VAT Enemies (17 → mon1001~mon1033)", priority = 71)]
public static void GenerateAllVATEnemies()
{
if (!AssetDatabase.IsValidFolder(OutputDir))
{
Directory.CreateDirectory(Path.GetFullPath(OutputDir));
AssetDatabase.Refresh();
}
int generated = 0, missing = 0;
foreach (var map in AllEnemyMapping)
{
var srcPath = $"{SourceEnemiesDir}/{map.srcPrefab}";
var src = AssetDatabase.LoadAssetAtPath<GameObject>(srcPath);
if (src == null)
{
Debug.LogWarning($"[EnemyVATConverter] 源 prefab 不存在: {srcPath}");
missing++;
continue;
}
var vatMonPath = $"{VATBakedDir}/{map.monName}/{map.monName}_VAT.prefab";
var vatMon = AssetDatabase.LoadAssetAtPath<GameObject>(vatMonPath);
if (vatMon == null)
{
Debug.LogWarning($"[EnemyVATConverter] VAT mon 不存在: {vatMonPath}");
missing++;
continue;
}
var dstPath = $"{OutputDir}/{Path.GetFileNameWithoutExtension(map.srcPrefab)}_VAT.prefab";
try
{
BuildOne(src, dstPath, vatMon);
generated++;
}
catch (Exception e)
{
Debug.LogError($"[EnemyVATConverter] 失败 {map.enumName}: {e.Message}");
}
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
// 重生成 EnemiesDatabase VAT 副本(用所有 17 个映射,覆盖之前只 Stage1 的逻辑)
GenerateVATDatabaseForAll();
// GenerateVATDatabaseForAll 中 DeleteAsset + CopyAsset 会让 VAT DB 的 GUID 变,
// 场景里 EnemiesSpawner.database 旧 GUID 引用会失效 → Init 时崩 NPE。
// 这里自动把 Game 场景重新指向新 GUID 的 VAT DB,避免 user 每次 Generate 后手动切。
SwitchSceneDatabase(OutputDBPath, "VAT");
Debug.Log($"[EnemyVATConverter] Generate ALL Done — 生成 {generated} 个 _VAT prefab, 缺失 {missing}");
}
private static void GenerateVATDatabaseForAll()
{
var src = AssetDatabase.LoadAssetAtPath<EnemiesDatabase>(SourceDBPath);
if (src == null)
{
Debug.LogWarning($"[EnemyVATConverter] 源 DB 不存在: {SourceDBPath}");
return;
}
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(OutputDBPath) != null)
AssetDatabase.DeleteAsset(OutputDBPath);
AssetDatabase.CopyAsset(SourceDBPath, OutputDBPath);
AssetDatabase.Refresh();
var dbVAT = AssetDatabase.LoadAssetAtPath<EnemiesDatabase>(OutputDBPath);
if (dbVAT == null) throw new Exception("无法加载复制后的 DB");
// 构建 enumName → _VAT prefab 路径 的快查表
var nameToPath = new Dictionary<string, string>();
foreach (var m in AllEnemyMapping)
{
var vp = $"{OutputDir}/{Path.GetFileNameWithoutExtension(m.srcPrefab)}_VAT.prefab";
nameToPath[m.enumName] = vp;
}
var so = new SerializedObject(dbVAT);
var enemiesProp = so.FindProperty("enemies");
int replaced = 0;
for (int i = 0; i < enemiesProp.arraySize; i++)
{
var entry = enemiesProp.GetArrayElementAtIndex(i);
var typeProp = entry.FindPropertyRelative("type");
var prefabProp = entry.FindPropertyRelative("prefab");
string enumName = typeProp.enumNames[typeProp.enumValueIndex];
if (!nameToPath.TryGetValue(enumName, out var vatPath)) continue;
var vatPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(vatPath);
if (vatPrefab == null) continue;
prefabProp.objectReferenceValue = vatPrefab;
replaced++;
}
so.ApplyModifiedProperties();
AssetDatabase.SaveAssets();
Debug.Log($"[EnemyVATConverter] DB VAT 全量替换: {OutputDBPath} (替换 {replaced}/{enemiesProp.arraySize} 个条目)");
}
[MenuItem("Tools/OctoberStudio/VAT/Generate Stage1 VAT Enemies", priority = 70)]
public static void GenerateStage1Enemies()
{
var vatPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(VATPrefabPath);
if (vatPrefab == null)
{
Debug.LogError($"[EnemyVATConverter] VAT prefab 未找到: {VATPrefabPath}\n请先跑 Tools/OctoberStudio/VAT/Bake mon9020 (Test Sample)");
return;
}
if (!AssetDatabase.IsValidFolder(OutputDir))
{
Directory.CreateDirectory(Path.GetFullPath(OutputDir));
AssetDatabase.Refresh();
}
int generated = 0;
foreach (var kv in Stage1EnemyPrefabs)
{
var srcPath = $"{SourceEnemiesDir}/{kv.Value}";
var src = AssetDatabase.LoadAssetAtPath<GameObject>(srcPath);
if (src == null)
{
Debug.LogWarning($"[EnemyVATConverter] 源 prefab 不存在: {srcPath}");
continue;
}
var dstPath = $"{OutputDir}/{Path.GetFileNameWithoutExtension(kv.Value)}_VAT.prefab";
try
{
BuildOne(src, dstPath, vatPrefab);
generated++;
}
catch (Exception e)
{
Debug.LogError($"[EnemyVATConverter] 失败 {kv.Key}: {e}");
}
}
AssetDatabase.SaveAssets();
AssetDatabase.Refresh();
// 生成 EnemiesDatabase VAT 副本
GenerateVATDatabase();
// 自动 rewire 场景到新 GUID,避免 user 每次手动切
SwitchSceneDatabase(OutputDBPath, "VAT");
Debug.Log($"[EnemyVATConverter] Done. Generated {generated} VAT prefabs in {OutputDir}");
}
private static void BuildOne(GameObject srcPrefab, string dstPath, GameObject vatPrefab)
{
// 加载 prefab 实例(unpacked = false 保留嵌套 prefab 链接)
var instance = (GameObject)PrefabUtility.InstantiatePrefab(srcPrefab);
if (instance == null) throw new Exception("Instantiate failed");
try
{
// 找到 EnemyBehavior 所在的 root
var behavior = instance.GetComponent<EnemyBehavior>();
if (behavior == null) throw new Exception("EnemyBehavior 组件不存在");
// 加 EnemyVATAttachment 组件
var attach = instance.GetComponent<EnemyVATAttachment>();
if (attach == null) attach = instance.AddComponent<EnemyVATAttachment>();
attach.VATPrefab = vatPrefab;
attach.LocalScale = DefaultVATLocalScale;
attach.LocalOffset = DefaultVATLocalOffset;
// 保存为新 prefab (覆盖式)
PrefabUtility.SaveAsPrefabAsset(instance, dstPath);
Debug.Log($"[EnemyVATConverter] Saved: {dstPath}");
}
finally
{
UnityEngine.Object.DestroyImmediate(instance);
}
}
private static void GenerateVATDatabase()
{
var src = AssetDatabase.LoadAssetAtPath<EnemiesDatabase>(SourceDBPath);
if (src == null)
{
Debug.LogWarning($"[EnemyVATConverter] 源 DB 不存在: {SourceDBPath},跳过 DB 生成");
return;
}
// 复制原 asset 文件作为 base
if (AssetDatabase.LoadAssetAtPath<UnityEngine.Object>(OutputDBPath) != null)
AssetDatabase.DeleteAsset(OutputDBPath);
AssetDatabase.CopyAsset(SourceDBPath, OutputDBPath);
AssetDatabase.Refresh();
var dbVAT = AssetDatabase.LoadAssetAtPath<EnemiesDatabase>(OutputDBPath);
if (dbVAT == null) throw new Exception("无法加载复制后的 DB");
// 通过 SerializedObject 把每个 entry 的 prefab 指向 _VAT
var so = new SerializedObject(dbVAT);
var enemiesProp = so.FindProperty("enemies");
int replaced = 0;
for (int i = 0; i < enemiesProp.arraySize; i++)
{
var entry = enemiesProp.GetArrayElementAtIndex(i);
var typeProp = entry.FindPropertyRelative("type");
var prefabProp = entry.FindPropertyRelative("prefab");
EnemyType t = (EnemyType)typeProp.enumValueIndex;
// 注意: enum value 是 enumValueIndex 不是 intValue;但 EnemyType 枚举值不连续(Jellyfish=5 后是 Bug=8)
// 用 enumDisplayNames + enumNames 检查
string enumName = typeProp.enumNames[typeProp.enumValueIndex];
if (!Stage1EnemyPrefabs.TryGetValue(enumName, out string srcFileName)) continue;
var vatPrefabPath = $"{OutputDir}/{Path.GetFileNameWithoutExtension(srcFileName)}_VAT.prefab";
var vatPrefab = AssetDatabase.LoadAssetAtPath<GameObject>(vatPrefabPath);
if (vatPrefab == null)
{
Debug.LogWarning($"[EnemyVATConverter] VAT prefab 未找到: {vatPrefabPath}");
continue;
}
prefabProp.objectReferenceValue = vatPrefab;
replaced++;
}
so.ApplyModifiedProperties();
AssetDatabase.SaveAssets();
Debug.Log($"[EnemyVATConverter] DB VAT 副本: {OutputDBPath} (替换 {replaced} 个条目)");
}
// ============== 场景切换:把 EnemiesSpawner.database 切换为 VAT 副本 ==============
private const string GameScenePath = "Assets/Common/Scenes/Game.unity";
[MenuItem("Tools/OctoberStudio/VAT/Switch Stage1 to VAT (Game scene)", priority = 80)]
public static void SwitchSceneToVAT()
{
SwitchSceneDatabase(OutputDBPath, "VAT");
}
[MenuItem("Tools/OctoberStudio/VAT/Switch Stage1 to Original (Game scene)", priority = 81)]
public static void SwitchSceneToOriginal()
{
SwitchSceneDatabase(SourceDBPath, "Original");
}
private static void SwitchSceneDatabase(string dbAssetPath, string label)
{
var dbAsset = AssetDatabase.LoadAssetAtPath<EnemiesDatabase>(dbAssetPath);
if (dbAsset == null)
{
Debug.LogError($"[EnemyVATConverter] DB 不存在: {dbAssetPath}");
return;
}
// Load Game scene additively if not already open
var scene = SceneManager.GetSceneByPath(GameScenePath);
bool wasOpen = scene.IsValid() && scene.isLoaded;
if (!wasOpen)
{
scene = EditorSceneManager.OpenScene(GameScenePath, OpenSceneMode.Single);
}
int patched = 0;
foreach (var root in scene.GetRootGameObjects())
{
foreach (var sp in root.GetComponentsInChildren<EnemiesSpawner>(true))
{
var so = new SerializedObject(sp);
var dbProp = so.FindProperty("database");
if (dbProp != null)
{
dbProp.objectReferenceValue = dbAsset;
so.ApplyModifiedProperties();
EditorUtility.SetDirty(sp);
patched++;
}
}
}
EditorSceneManager.MarkSceneDirty(scene);
EditorSceneManager.SaveScene(scene);
Debug.Log($"[EnemyVATConverter] 场景切换到 {label}: 替换 {patched} 个 EnemiesSpawner.database → {dbAssetPath}");
}
}
}