369 lines
16 KiB
C#
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}");
|
|
}
|
|
}
|
|
}
|