摄像机区域的架构改动
This commit is contained in:
230
Assets/_Game/Scripts/Editor/Tools/MissingScriptCleaner.cs
Normal file
230
Assets/_Game/Scripts/Editor/Tools/MissingScriptCleaner.cs
Normal file
@@ -0,0 +1,230 @@
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 扫描当前场景及 Project 中所有 Prefab,移除丢失(Missing)脚本引用。
|
||||
///
|
||||
/// 菜单:BaseGames/Tools/Missing Scripts/
|
||||
/// </summary>
|
||||
public static class MissingScriptCleaner
|
||||
{
|
||||
// ──────────────────────────────────────────────
|
||||
// 场景
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
[MenuItem("BaseGames/Tools/Missing Scripts/Clear In Scene")]
|
||||
public static void ClearMissingScriptsInScene()
|
||||
{
|
||||
int totalRemoved = 0;
|
||||
var affected = new List<GameObject>();
|
||||
|
||||
foreach (var go in GetAllSceneObjects())
|
||||
{
|
||||
int removed = GameObjectUtility.RemoveMonoBehavioursWithMissingScript(go);
|
||||
if (removed > 0)
|
||||
{
|
||||
affected.Add(go);
|
||||
totalRemoved += removed;
|
||||
EditorUtility.SetDirty(go);
|
||||
Debug.Log($"[MissingScriptCleaner] 场景已清理:{GetFullPath(go)}", go);
|
||||
}
|
||||
}
|
||||
|
||||
if (totalRemoved == 0)
|
||||
Debug.Log("[MissingScriptCleaner] 场景中未发现丢失脚本。");
|
||||
else
|
||||
Debug.Log($"[MissingScriptCleaner] 场景完成。共移除 {totalRemoved} 个丢失脚本,影响 {affected.Count} 个 GameObject。");
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Tools/Missing Scripts/Find In Scene")]
|
||||
public static void FindMissingScriptsInScene()
|
||||
{
|
||||
int totalFound = 0;
|
||||
|
||||
foreach (var go in GetAllSceneObjects())
|
||||
{
|
||||
foreach (var component in go.GetComponents<Component>())
|
||||
{
|
||||
if (component == null)
|
||||
{
|
||||
Debug.LogWarning($"[MissingScriptCleaner] 场景丢失脚本:{GetFullPath(go)}", go);
|
||||
totalFound++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (totalFound == 0)
|
||||
Debug.Log("[MissingScriptCleaner] 场景中未发现丢失脚本。");
|
||||
else
|
||||
Debug.LogWarning($"[MissingScriptCleaner] 场景共发现 {totalFound} 个含丢失脚本的 GameObject。");
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// Prefab 资产
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
[MenuItem("BaseGames/Tools/Missing Scripts/Clear In All Prefabs")]
|
||||
public static void ClearMissingScriptsInPrefabs()
|
||||
{
|
||||
int totalRemoved = 0;
|
||||
int affectedPrefabs = 0;
|
||||
|
||||
string[] guids = AssetDatabase.FindAssets("t:Prefab");
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < guids.Length; i++)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
|
||||
if (!path.StartsWith("Assets/")) continue; // 跳过 Packages 等只读路径
|
||||
|
||||
if (EditorUtility.DisplayCancelableProgressBar(
|
||||
"清理 Prefab 丢失脚本",
|
||||
path,
|
||||
(float)i / guids.Length))
|
||||
break;
|
||||
|
||||
int removed = CleanPrefabAtPath(path);
|
||||
if (removed > 0)
|
||||
{
|
||||
totalRemoved += removed;
|
||||
affectedPrefabs++;
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
AssetDatabase.SaveAssets();
|
||||
}
|
||||
|
||||
if (totalRemoved == 0)
|
||||
Debug.Log("[MissingScriptCleaner] 所有 Prefab 中未发现丢失脚本。");
|
||||
else
|
||||
Debug.Log($"[MissingScriptCleaner] Prefab 完成。共移除 {totalRemoved} 个丢失脚本,影响 {affectedPrefabs} 个 Prefab。");
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Tools/Missing Scripts/Find In All Prefabs")]
|
||||
public static void FindMissingScriptsInPrefabs()
|
||||
{
|
||||
int totalFound = 0;
|
||||
|
||||
string[] guids = AssetDatabase.FindAssets("t:Prefab");
|
||||
try
|
||||
{
|
||||
for (int i = 0; i < guids.Length; i++)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guids[i]);
|
||||
if (!path.StartsWith("Assets/")) continue; // 跳过 Packages 等只读路径
|
||||
|
||||
if (EditorUtility.DisplayCancelableProgressBar(
|
||||
"查找 Prefab 丢失脚本",
|
||||
path,
|
||||
(float)i / guids.Length))
|
||||
break;
|
||||
|
||||
var contentsRoot = PrefabUtility.LoadPrefabContents(path);
|
||||
if (contentsRoot == null) continue;
|
||||
|
||||
try
|
||||
{
|
||||
var tempScene = contentsRoot.scene;
|
||||
foreach (var go in Resources.FindObjectsOfTypeAll<GameObject>())
|
||||
{
|
||||
if (go.scene != tempScene) continue;
|
||||
foreach (var component in go.GetComponents<Component>())
|
||||
{
|
||||
if (component == null)
|
||||
{
|
||||
Debug.LogWarning($"[MissingScriptCleaner] Prefab 丢失脚本:{path} → {go.name}");
|
||||
totalFound++;
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrefabUtility.UnloadPrefabContents(contentsRoot);
|
||||
}
|
||||
}
|
||||
}
|
||||
finally
|
||||
{
|
||||
EditorUtility.ClearProgressBar();
|
||||
}
|
||||
|
||||
if (totalFound == 0)
|
||||
Debug.Log("[MissingScriptCleaner] 所有 Prefab 中未发现丢失脚本。");
|
||||
else
|
||||
Debug.LogWarning($"[MissingScriptCleaner] Prefab 共发现 {totalFound} 个含丢失脚本的 GameObject。");
|
||||
}
|
||||
|
||||
// ──────────────────────────────────────────────
|
||||
// 内部辅助
|
||||
// ──────────────────────────────────────────────
|
||||
|
||||
static int CleanPrefabAtPath(string path)
|
||||
{
|
||||
// LoadPrefabContents 在临时预览场景中打开 Prefab,
|
||||
// 此时 Resources.FindObjectsOfTypeAll 可枚举到包括 HideInHierarchy 在内的全部对象。
|
||||
// 修改完毕后必须用 SaveAsPrefabAsset 写回,否则隐藏对象的改动不会持久化。
|
||||
var contentsRoot = PrefabUtility.LoadPrefabContents(path);
|
||||
if (contentsRoot == null) return 0;
|
||||
|
||||
int totalRemoved = 0;
|
||||
try
|
||||
{
|
||||
var tempScene = contentsRoot.scene;
|
||||
foreach (var go in Resources.FindObjectsOfTypeAll<GameObject>())
|
||||
{
|
||||
if (go.scene != tempScene) continue;
|
||||
int removed = GameObjectUtility.RemoveMonoBehavioursWithMissingScript(go);
|
||||
if (removed > 0)
|
||||
{
|
||||
totalRemoved += removed;
|
||||
Debug.Log($"[MissingScriptCleaner] Prefab 已清理:{path} → {go.name}");
|
||||
}
|
||||
}
|
||||
|
||||
if (totalRemoved > 0)
|
||||
PrefabUtility.SaveAsPrefabAsset(contentsRoot, path);
|
||||
}
|
||||
finally
|
||||
{
|
||||
PrefabUtility.UnloadPrefabContents(contentsRoot);
|
||||
}
|
||||
|
||||
return totalRemoved;
|
||||
}
|
||||
|
||||
static GameObject[] GetAllSceneObjects()
|
||||
{
|
||||
// FindObjectsByType 不返回 HideFlags.HideInHierarchy 的对象,
|
||||
// 使用 Resources.FindObjectsOfTypeAll 获取全部对象,再过滤出已加载场景内的实例。
|
||||
var all = Resources.FindObjectsOfTypeAll<GameObject>();
|
||||
var result = new List<GameObject>();
|
||||
foreach (var go in all)
|
||||
{
|
||||
if (go.scene.IsValid() && go.scene.isLoaded)
|
||||
result.Add(go);
|
||||
}
|
||||
return result.ToArray();
|
||||
}
|
||||
|
||||
static string GetFullPath(GameObject go)
|
||||
{
|
||||
var path = go.name;
|
||||
var parent = go.transform.parent;
|
||||
while (parent != null)
|
||||
{
|
||||
path = parent.name + "/" + path;
|
||||
parent = parent.parent;
|
||||
}
|
||||
return path;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: d13fb92923fd33e4c812025f203a9928
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
215
Assets/_Game/Scripts/Editor/Tools/Physics2DLayerReport.cs
Normal file
215
Assets/_Game/Scripts/Editor/Tools/Physics2DLayerReport.cs
Normal file
@@ -0,0 +1,215 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Text;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// Physics2D 层碰撞矩阵检查与修复工具。
|
||||
///
|
||||
/// 菜单:BaseGames → Tools → Physics2D Layer Matrix
|
||||
///
|
||||
/// 检查规则:
|
||||
/// · PlayerHitBox ↔ EnemyHurtBox → 应碰撞(玩家攻击伤害敌人)
|
||||
/// · EnemyHitBox ↔ PlayerHurtBox → 应碰撞(敌人攻击伤害玩家)
|
||||
/// · EnemyHitBox ↔ EnemyHurtBox → 应碰撞(敌人可互相伤害,HitBox 运行时排除自身根节点)
|
||||
/// · Player ↔ Ground → 应碰撞(玩家站在地面上)
|
||||
/// · Enemy ↔ Ground → 应碰撞(敌人站在地面上)
|
||||
/// · PlayerProjectile ↔ EnemyHurtBox → 应碰撞(玩家投射物伤害敌人)
|
||||
/// · PlayerProjectile ↔ PlayerHurtBox → 应忽略(玩家投射物不自伤)
|
||||
/// · PlayerProjectile ↔ Ground → 应碰撞(玩家投射物命中地形)
|
||||
/// · EnemyProjectile ↔ PlayerHurtBox → 应碰撞(敌人投射物伤害玩家)
|
||||
/// · EnemyProjectile ↔ EnemyHurtBox → 应忽略(敌人投射物不自伤)
|
||||
/// · EnemyProjectile ↔ Ground → 应碰撞(敌人投射物命中地形)
|
||||
/// · PlayerHitBox ↔ PlayerHurtBox → 应忽略(玩家不自伤)
|
||||
/// · PlayerProjectile ↔ EnemyProjectile → 应忽略(子弹不互相碰撞,Clash 系统单独处理)
|
||||
/// </summary>
|
||||
public static class Physics2DLayerReport
|
||||
{
|
||||
// ── 期望配置表 ────────────────────────────────────────────────────────
|
||||
private static readonly ExpectedPair[] ExpectedPairs =
|
||||
{
|
||||
new("PlayerHitBox", "EnemyHurtBox", true, "玩家攻击伤害敌人"),
|
||||
new("EnemyHitBox", "PlayerHurtBox", true, "敌人攻击伤害玩家"),
|
||||
new("EnemyHitBox", "EnemyHurtBox", true, "敌人可互相伤害(HitBox 运行时排除自身根节点)"),
|
||||
new("Player", "Ground", true, "玩家站在地面上"),
|
||||
new("Enemy", "Ground", true, "敌人站在地面上"),
|
||||
new("PlayerProjectile", "EnemyHurtBox", true, "玩家投射物伤害敌人"),
|
||||
new("PlayerProjectile", "PlayerHurtBox", false, "玩家投射物不自伤"),
|
||||
new("PlayerProjectile", "Ground", true, "玩家投射物命中地形"),
|
||||
new("EnemyProjectile", "PlayerHurtBox", true, "敌人投射物伤害玩家"),
|
||||
new("EnemyProjectile", "EnemyHurtBox", false, "敌人投射物不自伤"),
|
||||
new("EnemyProjectile", "Ground", true, "敌人投射物命中地形"),
|
||||
new("PlayerHitBox", "PlayerHurtBox", false, "玩家不自伤"),
|
||||
new("PlayerProjectile", "EnemyProjectile", false, "子弹不互相碰撞(Clash 系统单独处理)"),
|
||||
};
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
[MenuItem("BaseGames/Tools/Physics2D Layer Matrix/Check", priority = 210)]
|
||||
public static void CheckAndPrintReport()
|
||||
{
|
||||
var results = Check();
|
||||
PrintToConsole(results);
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Tools/Physics2D Layer Matrix/Auto Fix", priority = 211)]
|
||||
public static void FixAndReport()
|
||||
{
|
||||
var results = Check();
|
||||
int fixed_ = ApplyFixes(results);
|
||||
Debug.Log($"[Physics2DLayerReport] 修复完成,共修正 {fixed_} 项。");
|
||||
}
|
||||
|
||||
// ─────────────────────────────────────────────────────────────────────
|
||||
/// <summary>返回当前所有期望配置的检查结果列表。</summary>
|
||||
public static List<LayerPairResult> Check()
|
||||
{
|
||||
var results = new List<LayerPairResult>(ExpectedPairs.Length);
|
||||
|
||||
foreach (var pair in ExpectedPairs)
|
||||
{
|
||||
int layerA = LayerMask.NameToLayer(pair.LayerA);
|
||||
int layerB = LayerMask.NameToLayer(pair.LayerB);
|
||||
|
||||
bool layerMissing = layerA == -1 || layerB == -1;
|
||||
bool actualCollide = false;
|
||||
|
||||
if (!layerMissing)
|
||||
{
|
||||
// GetIgnoreLayerCollision 返回 true = 忽略碰撞(不碰)
|
||||
bool ignored = Physics2D.GetIgnoreLayerCollision(layerA, layerB);
|
||||
actualCollide = !ignored;
|
||||
}
|
||||
|
||||
results.Add(new LayerPairResult
|
||||
{
|
||||
LayerA = pair.LayerA,
|
||||
LayerB = pair.LayerB,
|
||||
ShouldCollide = pair.ShouldCollide,
|
||||
ActualCollide = actualCollide,
|
||||
LayerMissing = layerMissing,
|
||||
Description = pair.Description,
|
||||
});
|
||||
}
|
||||
|
||||
return results;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 对检查结果中所有不正确项应用修复。
|
||||
/// 返回修复数量。
|
||||
/// </summary>
|
||||
public static int ApplyFixes(List<LayerPairResult> results)
|
||||
{
|
||||
int count = 0;
|
||||
|
||||
foreach (var r in results)
|
||||
{
|
||||
if (r.IsOk) continue;
|
||||
if (r.LayerMissing)
|
||||
{
|
||||
Debug.LogWarning($"[Physics2DLayerReport] Layer '{r.LayerA}' 或 '{r.LayerB}' 不存在," +
|
||||
"请先在 Tags and Layers 中创建。");
|
||||
continue;
|
||||
}
|
||||
|
||||
int layerA = LayerMask.NameToLayer(r.LayerA);
|
||||
int layerB = LayerMask.NameToLayer(r.LayerB);
|
||||
|
||||
// IgnoreLayerCollision(a, b, ignore=true) = 不碰;ignore=false = 碰
|
||||
Physics2D.IgnoreLayerCollision(layerA, layerB, !r.ShouldCollide);
|
||||
count++;
|
||||
|
||||
string action = r.ShouldCollide ? "已启用碰撞" : "已禁用碰撞";
|
||||
Debug.Log($"[Physics2DLayerReport] {action}:{r.LayerA} ↔ {r.LayerB}({r.Description})");
|
||||
}
|
||||
|
||||
// 修改 ProjectSettings 使改动持久化
|
||||
if (count > 0)
|
||||
SavePhysicsSettings();
|
||||
|
||||
return count;
|
||||
}
|
||||
|
||||
// ── 私有辅助 ─────────────────────────────────────────────────────────
|
||||
private static void PrintToConsole(List<LayerPairResult> results)
|
||||
{
|
||||
var sb = new StringBuilder();
|
||||
sb.AppendLine("[Physics2DLayerReport] ── Physics2D 层碰撞矩阵检查报告 ──────────────────");
|
||||
|
||||
int okCount = 0;
|
||||
int errCount = 0;
|
||||
int missCount = 0;
|
||||
|
||||
foreach (var r in results)
|
||||
{
|
||||
if (r.LayerMissing)
|
||||
{
|
||||
sb.AppendLine($" ⚠ {r.LayerA} ↔ {r.LayerB} [Layer 不存在] {r.Description}");
|
||||
missCount++;
|
||||
}
|
||||
else if (r.IsOk)
|
||||
{
|
||||
sb.AppendLine($" ✅ {r.LayerA} ↔ {r.LayerB} [正常] {r.Description}");
|
||||
okCount++;
|
||||
}
|
||||
else
|
||||
{
|
||||
string current = r.ActualCollide ? "碰撞" : "忽略";
|
||||
string expected = r.ShouldCollide ? "碰撞" : "忽略";
|
||||
sb.AppendLine($" ❌ {r.LayerA} ↔ {r.LayerB} [当前:{current} 期望:{expected}] {r.Description}");
|
||||
errCount++;
|
||||
}
|
||||
}
|
||||
|
||||
sb.AppendLine($"──────────────── 正常:{okCount} 错误:{errCount} Layer缺失:{missCount} ────────────────");
|
||||
|
||||
if (errCount == 0 && missCount == 0)
|
||||
Debug.Log(sb.ToString());
|
||||
else
|
||||
Debug.LogWarning(sb.ToString());
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 通过 SerializedObject 修改 ProjectSettings/DynamicsManager.asset 以持久化 Physics2D 层矩阵。
|
||||
/// Unity 在退出时也会自动保存,但显式调用可立即落盘。
|
||||
/// </summary>
|
||||
private static void SavePhysicsSettings()
|
||||
{
|
||||
// Unity 内部会在下一次 AssetDatabase 刷新时持久化 Physics2D 设置
|
||||
// 使用 Physics2DLayerMatrix 写入后刷新即可
|
||||
AssetDatabase.SaveAssets();
|
||||
Physics2D.defaultContactOffset = Physics2D.defaultContactOffset; // 强制标记 dirty
|
||||
}
|
||||
|
||||
// ── 内部数据结构 ─────────────────────────────────────────────────────
|
||||
private readonly struct ExpectedPair
|
||||
{
|
||||
public readonly string LayerA;
|
||||
public readonly string LayerB;
|
||||
public readonly bool ShouldCollide;
|
||||
public readonly string Description;
|
||||
|
||||
public ExpectedPair(string a, string b, bool shouldCollide, string desc)
|
||||
{
|
||||
LayerA = a;
|
||||
LayerB = b;
|
||||
ShouldCollide = shouldCollide;
|
||||
Description = desc;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// ══ 层碰撞结果结构体 ══════════════════════════════════════════════════════
|
||||
public struct LayerPairResult
|
||||
{
|
||||
public string LayerA;
|
||||
public string LayerB;
|
||||
public bool ShouldCollide;
|
||||
public bool ActualCollide;
|
||||
public bool LayerMissing;
|
||||
public string Description;
|
||||
public bool IsOk => !LayerMissing && (ActualCollide == ShouldCollide);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 97f163d1f0e4f904cbb6c89abe5a546e
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
78
Assets/_Game/Scripts/Editor/Tools/SOValidationRunner.cs
Normal file
78
Assets/_Game/Scripts/Editor/Tools/SOValidationRunner.cs
Normal file
@@ -0,0 +1,78 @@
|
||||
using System.Collections.Generic;
|
||||
using System.Linq;
|
||||
using UnityEditor;
|
||||
using UnityEditor.Build;
|
||||
using UnityEditor.Build.Reporting;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 扫描项目中所有实现 <see cref="BaseGames.Core.IValidatable"/> 接口的 ScriptableObject,
|
||||
/// 调用 Validate() 并在 Console 报告验证结果。同时作为构建前处理器,发现错误时中止构建。
|
||||
///
|
||||
/// 菜单:BaseGames/Tools/Validate All ScriptableObjects
|
||||
/// Build 回调顺序 = 1(在 AddressKeyValidator callbackOrder = 0 之后执行)
|
||||
/// </summary>
|
||||
public class SOValidationRunner : IPreprocessBuildWithReport
|
||||
{
|
||||
public int callbackOrder => 1;
|
||||
|
||||
public void OnPreprocessBuild(BuildReport report)
|
||||
{
|
||||
var (errors, warnings) = RunAll();
|
||||
|
||||
foreach (var w in warnings)
|
||||
Debug.LogWarning(w);
|
||||
|
||||
if (errors.Count > 0)
|
||||
throw new BuildFailedException(
|
||||
$"[SOValidationRunner] {errors.Count} 处 SO 数据错误,构建中止:\n"
|
||||
+ string.Join("\n", errors));
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Tools/Validate All ScriptableObjects")]
|
||||
public static void ValidateMenu()
|
||||
{
|
||||
var (errors, warnings) = RunAll();
|
||||
|
||||
if (errors.Count == 0 && warnings.Count == 0)
|
||||
{
|
||||
Debug.Log("[SOValidationRunner] ✅ 所有 SO 数据均合法。");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var w in warnings) Debug.LogWarning(w);
|
||||
foreach (var e in errors) Debug.LogError(e);
|
||||
|
||||
Debug.Log($"[SOValidationRunner] 校验完成:{errors.Count} 错误,{warnings.Count} 警告。");
|
||||
}
|
||||
|
||||
// ── Internal ──────────────────────────────────────────────────────
|
||||
|
||||
private static (List<string> errors, List<string> warnings) RunAll()
|
||||
{
|
||||
var errors = new List<string>();
|
||||
var warnings = new List<string>();
|
||||
|
||||
var guids = AssetDatabase.FindAssets("t:ScriptableObject");
|
||||
foreach (var guid in guids)
|
||||
{
|
||||
var path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var so = AssetDatabase.LoadAssetAtPath<ScriptableObject>(path);
|
||||
if (so is BaseGames.Core.IValidatable validatable)
|
||||
{
|
||||
foreach (var result in validatable.Validate())
|
||||
{
|
||||
if (result.Severity == BaseGames.Core.ValidationSeverity.Error)
|
||||
errors.Add($"❌ {result.Message} ({path})");
|
||||
else
|
||||
warnings.Add($"⚠️ {result.Message} ({path})");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return (errors, warnings);
|
||||
}
|
||||
}
|
||||
}
|
||||
11
Assets/_Game/Scripts/Editor/Tools/SOValidationRunner.cs.meta
Normal file
11
Assets/_Game/Scripts/Editor/Tools/SOValidationRunner.cs.meta
Normal file
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 9c46a1c015cf48e4c871252c189bd01a
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
159
Assets/_Game/Scripts/Editor/Tools/ScriptExecutionOrderTools.cs
Normal file
159
Assets/_Game/Scripts/Editor/Tools/ScriptExecutionOrderTools.cs
Normal file
@@ -0,0 +1,159 @@
|
||||
using System;
|
||||
using System.Collections.Generic;
|
||||
using UnityEditor;
|
||||
using UnityEngine;
|
||||
|
||||
namespace BaseGames.Editor
|
||||
{
|
||||
/// <summary>
|
||||
/// 一键应用/校验项目推荐的 Script Execution Order。
|
||||
/// </summary>
|
||||
public static class ScriptExecutionOrderTools
|
||||
{
|
||||
private readonly struct OrderRule
|
||||
{
|
||||
public readonly string ClassName;
|
||||
public readonly int Order;
|
||||
|
||||
public OrderRule(string className, int order)
|
||||
{
|
||||
ClassName = className;
|
||||
Order = order;
|
||||
}
|
||||
}
|
||||
|
||||
private static readonly OrderRule[] Rules =
|
||||
{
|
||||
new OrderRule("GameServiceRegistrar", -2000),
|
||||
new OrderRule("GameManager", -1000),
|
||||
new OrderRule("SceneService", -900),
|
||||
new OrderRule("GameSaveManager", -900),
|
||||
new OrderRule("AudioManager", -500),
|
||||
new OrderRule("PlayerController", -100),
|
||||
};
|
||||
|
||||
[MenuItem("BaseGames/Tools/Apply Script Execution Order Preset")]
|
||||
public static void ApplyPreset()
|
||||
{
|
||||
int updated = 0;
|
||||
int skipped = 0;
|
||||
var issues = new List<string>();
|
||||
|
||||
foreach (var rule in Rules)
|
||||
{
|
||||
if (!TryFindMonoScript(rule.ClassName, out MonoScript script, out string issue))
|
||||
{
|
||||
skipped++;
|
||||
issues.Add(issue);
|
||||
continue;
|
||||
}
|
||||
|
||||
int current = MonoImporter.GetExecutionOrder(script);
|
||||
if (current == rule.Order)
|
||||
continue;
|
||||
|
||||
MonoImporter.SetExecutionOrder(script, rule.Order);
|
||||
updated++;
|
||||
}
|
||||
|
||||
AssetDatabase.SaveAssets();
|
||||
|
||||
if (issues.Count > 0)
|
||||
{
|
||||
Debug.LogWarning(
|
||||
"[ScriptExecutionOrderTools] 已应用执行顺序预设(部分脚本未处理)。\n" +
|
||||
$"更新: {updated}, 跳过: {skipped}\n- {string.Join("\n- ", issues)}");
|
||||
return;
|
||||
}
|
||||
|
||||
Debug.Log($"[ScriptExecutionOrderTools] 执行顺序预设应用完成。更新数量: {updated}。");
|
||||
}
|
||||
|
||||
[MenuItem("BaseGames/Tools/Validate Script Execution Order Preset")]
|
||||
public static void ValidatePreset()
|
||||
{
|
||||
var mismatches = new List<string>();
|
||||
var issues = new List<string>();
|
||||
|
||||
foreach (var rule in Rules)
|
||||
{
|
||||
if (!TryFindMonoScript(rule.ClassName, out MonoScript script, out string issue))
|
||||
{
|
||||
issues.Add(issue);
|
||||
continue;
|
||||
}
|
||||
|
||||
int current = MonoImporter.GetExecutionOrder(script);
|
||||
if (current != rule.Order)
|
||||
mismatches.Add($"{rule.ClassName}: 当前 {current}, 期望 {rule.Order}");
|
||||
}
|
||||
|
||||
if (mismatches.Count == 0 && issues.Count == 0)
|
||||
{
|
||||
Debug.Log("[ScriptExecutionOrderTools] 执行顺序校验通过,所有脚本均符合预设。");
|
||||
return;
|
||||
}
|
||||
|
||||
string message = "[ScriptExecutionOrderTools] 执行顺序校验发现问题。";
|
||||
if (mismatches.Count > 0)
|
||||
message += "\n顺序不一致:\n- " + string.Join("\n- ", mismatches);
|
||||
if (issues.Count > 0)
|
||||
message += "\n脚本解析问题:\n- " + string.Join("\n- ", issues);
|
||||
|
||||
Debug.LogWarning(message);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 根据 <paramref name="className"/> 查找 MonoScript。
|
||||
/// <para>当 className 包含 '.'(全限定名)时,用 <c>type.FullName</c> 精确匹配;
|
||||
/// 否则用 <c>type.Name</c> 匹配(向后兼容简单类名)。</para>
|
||||
/// </summary>
|
||||
private static bool TryFindMonoScript(string className, out MonoScript script, out string issue)
|
||||
{
|
||||
script = null;
|
||||
issue = null;
|
||||
|
||||
// 全限定名时,FindAssets 只取最后一段(简单类名)作为搜索词
|
||||
bool useFullName = className.Contains('.');
|
||||
string searchName = useFullName
|
||||
? className.Substring(className.LastIndexOf('.') + 1)
|
||||
: className;
|
||||
|
||||
string[] guids = AssetDatabase.FindAssets($"{searchName} t:MonoScript");
|
||||
var matches = new List<MonoScript>();
|
||||
|
||||
foreach (string guid in guids)
|
||||
{
|
||||
string path = AssetDatabase.GUIDToAssetPath(guid);
|
||||
var candidate = AssetDatabase.LoadAssetAtPath<MonoScript>(path);
|
||||
if (candidate == null)
|
||||
continue;
|
||||
|
||||
Type type = candidate.GetClass();
|
||||
if (type == null) continue;
|
||||
|
||||
bool nameMatch = useFullName
|
||||
? type.FullName == className
|
||||
: type.Name == className;
|
||||
|
||||
if (nameMatch)
|
||||
matches.Add(candidate);
|
||||
}
|
||||
|
||||
if (matches.Count == 0)
|
||||
{
|
||||
issue = $"未找到脚本: {className}";
|
||||
return false;
|
||||
}
|
||||
|
||||
if (matches.Count > 1)
|
||||
{
|
||||
issue = $"存在多个同名脚本: {className}(请消歧后重试)";
|
||||
return false;
|
||||
}
|
||||
|
||||
script = matches[0];
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
fileFormatVersion: 2
|
||||
guid: 1d2bcc35606ec6a47b82e00462955dbe
|
||||
MonoImporter:
|
||||
externalObjects: {}
|
||||
serializedVersion: 2
|
||||
defaultReferences: []
|
||||
executionOrder: 0
|
||||
icon: {instanceID: 0}
|
||||
userData:
|
||||
assetBundleName:
|
||||
assetBundleVariant:
|
||||
Reference in New Issue
Block a user