摄像机区域的架构改动

This commit is contained in:
2026-05-15 14:47:24 +08:00
parent 1b37297585
commit f264329751
3591 changed files with 1687228 additions and 446503 deletions

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

View File

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

View 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);
}
}

View File

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

View 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);
}
}
}

View File

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

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

View File

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