Files
DuckovMods/SceneSnapshot/PrintTool.cs

466 lines
21 KiB
C#
Raw Permalink Normal View History

2025-11-05 21:34:21 +08:00
using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.EventSystems; // 用于UI射线检测
2025-11-01 15:18:34 +08:00
using System;
using System.IO;
using System.Text;
2025-11-05 21:34:21 +08:00
using System.Reflection;
using System.Collections; // 用于 IEnumerable
using System.Collections.Generic; // 用于 HashSet, List, Dictionary
using System.Linq; // 用于 OrderBy
2025-11-01 15:18:34 +08:00
namespace SceneSnapshot
{
internal class PrintTool : MonoBehaviour
{
2025-11-05 21:34:21 +08:00
private const string BASE_FOLDER_NAME = "GameObjectSnapshots"; // 主文件夹名称
private const int MAX_REFLECTION_DEPTH = 3; // 最大反射深度,防止循环引用或过深的对象图
private const int MAX_COLLECTION_ELEMENTS_TO_PRINT = 5; // 集合最多打印的元素数量
2025-11-01 15:18:34 +08:00
private void Update()
{
2025-11-05 21:34:21 +08:00
if (Input.GetKeyDown(KeyCode.F2))
{
CaptureAndPrintSnapshot();
}
2025-11-01 15:18:34 +08:00
}
2025-11-05 21:34:21 +08:00
private void CaptureAndPrintSnapshot()
2025-11-01 15:18:34 +08:00
{
2025-11-05 21:34:21 +08:00
// 获取桌面路径
string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
2025-11-01 15:18:34 +08:00
2025-11-05 21:34:21 +08:00
// 1. 创建主文件夹 (如果不存在)
string baseOutputPath = Path.Combine(desktopPath, BASE_FOLDER_NAME);
2025-11-01 15:18:34 +08:00
try
{
2025-11-05 21:34:21 +08:00
if (!Directory.Exists(baseOutputPath))
2025-11-01 15:18:34 +08:00
{
2025-11-05 21:34:21 +08:00
Directory.CreateDirectory(baseOutputPath);
Debug.Log($"已创建主快照文件夹: {baseOutputPath}");
2025-11-01 15:18:34 +08:00
}
}
2025-11-05 21:34:21 +08:00
catch (Exception e)
2025-11-01 15:18:34 +08:00
{
2025-11-05 21:34:21 +08:00
Debug.LogError($"无法创建主快照文件夹 {baseOutputPath}: {e.Message}");
2025-11-01 15:18:34 +08:00
return;
}
2025-11-05 21:34:21 +08:00
// 2. 在主文件夹内创建带时间戳的子文件夹
string timestampFolderName = DateTime.Now.ToString("yyyyMMdd_HHmmss");
string currentSnapshotOutputPath = Path.Combine(baseOutputPath, timestampFolderName);
2025-11-01 15:18:34 +08:00
try
{
2025-11-05 21:34:21 +08:00
if (!Directory.Exists(currentSnapshotOutputPath))
{
Directory.CreateDirectory(currentSnapshotOutputPath);
}
2025-11-01 15:18:34 +08:00
}
2025-11-05 21:34:21 +08:00
catch (Exception e)
2025-11-01 15:18:34 +08:00
{
2025-11-05 21:34:21 +08:00
Debug.LogError($"无法创建快照子文件夹 {currentSnapshotOutputPath}: {e.Message}");
return;
2025-11-01 15:18:34 +08:00
}
2025-11-05 21:34:21 +08:00
Debug.Log($"开始生成场景快照到: {currentSnapshotOutputPath}");
2025-11-01 15:18:34 +08:00
2025-11-05 21:34:21 +08:00
// Part 1: 打印所有对象的对象树及其组件
PrintAllGameObjectsTree(currentSnapshotOutputPath);
2025-11-01 15:18:34 +08:00
2025-11-05 21:34:21 +08:00
// Part 2: 打印鼠标位置对象的组件值
PrintMouseHoveredObjectDetails(currentSnapshotOutputPath);
2025-11-01 15:18:34 +08:00
2025-11-05 21:34:21 +08:00
Debug.Log("场景快照生成完毕!");
2025-11-01 15:18:34 +08:00
}
/// <summary>
2025-11-05 21:34:21 +08:00
/// 打印所有场景对象的对象树包括DontDestroyOnLoad及其组件。
2025-11-01 15:18:34 +08:00
/// </summary>
2025-11-05 21:34:21 +08:00
private void PrintAllGameObjectsTree(string outputPath)
2025-11-01 15:18:34 +08:00
{
2025-11-05 21:34:21 +08:00
StringBuilder sb = new StringBuilder();
sb.AppendLine("--- 所有激活场景对象树 ---");
sb.AppendLine("--------------------------\n");
// 用于存储按场景分组的根对象
var sceneRootGameObjects = new Dictionary<Scene, List<GameObject>>();
// 用于存储 DontDestroyOnLoad 对象
var dontDestroyOnLoadRoots = new List<GameObject>();
2025-11-01 15:18:34 +08:00
2025-11-05 21:34:21 +08:00
// 1. 遍历所有加载的场景,获取其根对象
for (int i = 0; i < SceneManager.sceneCount; i++)
2025-11-01 15:18:34 +08:00
{
2025-11-05 21:34:21 +08:00
Scene scene = SceneManager.GetSceneAt(i);
sceneRootGameObjects[scene] = new List<GameObject>(scene.GetRootGameObjects());
2025-11-01 15:18:34 +08:00
}
2025-11-05 21:34:21 +08:00
// 2. 查找 DontDestroyOnLoad 对象
// DontDestroyOnLoad 对象不属于任何通过 SceneManager.GetSceneAt 获取的“普通”场景
// 它们通常在特殊的 "DontDestroyOnLoad" 场景中在Unity编辑器中可见但在运行时无法直接通过 SceneManager.GetSceneAt 访问。
// 因此我们遍历所有活跃的GameObject找出那些是根对象但又不属于任何已知场景的。
GameObject[] allActiveGameObjectsInHierarchy = FindObjectsOfType<GameObject>(); // 获取所有活跃的GameObject
foreach (GameObject go in allActiveGameObjectsInHierarchy)
2025-11-01 15:18:34 +08:00
{
2025-11-05 21:34:21 +08:00
if (go.transform.parent == null) // 这是一个根对象
2025-11-01 15:18:34 +08:00
{
2025-11-05 21:34:21 +08:00
bool foundInLoadedScene = false;
foreach (var kvp in sceneRootGameObjects)
{
if (kvp.Value.Contains(go))
{
foundInLoadedScene = true;
break;
}
}
2025-11-05 21:34:21 +08:00
if (!foundInLoadedScene)
{
// 如果它不是任何已加载场景的根对象那么它可能是DontDestroyOnLoad对象
dontDestroyOnLoadRoots.Add(go);
}
2025-11-01 15:18:34 +08:00
}
}
2025-11-05 21:34:21 +08:00
// 3. 打印普通场景的对象树
foreach (var kvp in sceneRootGameObjects)
{
Scene currentScene = kvp.Key;
List<GameObject> roots = kvp.Value;
sb.AppendLine($"=== 场景: {currentScene.name} (路径: {currentScene.path}, 已加载: {currentScene.isLoaded}) ===\n");
// 按名称排序根对象以保持输出一致性
foreach (GameObject root in roots.OrderBy(g => g.name))
{
PrintGameObjectRecursive(root, 0, sb, new HashSet<GameObject>());
}
}
// 4. 打印 DontDestroyOnLoad 对象的对象树
if (dontDestroyOnLoadRoots.Count > 0)
2025-11-01 15:18:34 +08:00
{
2025-11-05 21:34:21 +08:00
// 检查是否已经有一个伪的 "DontDestroyOnLoad" 场景被 Unity 在某些情境下自动添加
// 如果是,为了避免重复,且让输出更清晰,可以先尝试移除这些。
// 但是在 FindObjectsOfType 之后再分组,这种方式更健壮,不用管它是否有“场景”
sb.AppendLine("\n=== DontDestroyOnLoad 对象 ===\n");
foreach (GameObject root in dontDestroyOnLoadRoots.OrderBy(g => g.name))
{
PrintGameObjectRecursive(root, 0, sb, new HashSet<GameObject>());
}
2025-11-01 15:18:34 +08:00
}
2025-11-05 21:34:21 +08:00
string filePath = Path.Combine(outputPath, "SceneObjectTree.txt");
File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8); // 使用UTF8编码以支持更多字符
Debug.Log($"场景对象树已保存到: {filePath}");
2025-11-01 15:18:34 +08:00
}
/// <summary>
2025-11-05 21:34:21 +08:00
/// 递归打印GameObject及其子级和组件。
/// </summary>
2025-11-05 21:34:21 +08:00
private void PrintGameObjectRecursive(GameObject go, int depth, StringBuilder sb, HashSet<GameObject> visited)
{
2025-11-05 21:34:21 +08:00
// 防止循环引用或重复打印
if (visited.Contains(go))
{
2025-11-05 21:34:21 +08:00
sb.AppendLine($"{GetIndent(depth)}{go.name} (循环引用检测到!)");
return;
}
2025-11-05 21:34:21 +08:00
visited.Add(go);
string indent = GetIndent(depth);
sb.AppendLine($"{indent}GameObject: {go.name} (激活状态: {go.activeSelf}, 标签: {go.tag}, 层: {LayerMask.LayerToName(go.layer)})");
2025-11-05 21:34:21 +08:00
Component[] components = go.GetComponents<Component>();
foreach (Component comp in components)
{
2025-11-05 21:34:21 +08:00
if (comp == null) continue; // 避免NRE尽管不常见
sb.AppendLine($"{indent} 组件: {comp.GetType().Name}");
}
2025-11-05 21:34:21 +08:00
for (int i = 0; i < go.transform.childCount; i++)
{
PrintGameObjectRecursive(go.transform.GetChild(i).gameObject, depth + 1, sb, visited);
}
2025-11-05 21:34:21 +08:00
// 重要在递归完成后通常不需要从visited中移除GameObject
// 因为一个GameObject在对象树中只会以唯一的路径出现一次
// (除非它以某种非常规方式被引用但这不属于标准GameObject层级)。
// 对于值类型或简单引用可以在PrintObjectProperties中在处理完后移除。
// 对于GameObject层级一旦访问完其所有子节点它在该“分支”的任务就完成了。
// 如果不同根节点下可能会有相同的GameObject引用例如通过Inspector引用
// 那visited集合的作用是防止在*当前递归路径*中再次遇到同一个GameObject从而避免死循环。
// 对于整个场景树的打印visited集合可以保持不变因为我们不期望同一个GameObject作为不同根节点的子物体链中的一部分。
// visited.Remove(go); // 对于GameObject树结构这通常是不必要的因为每个GameObject在树中只有一个父级。
}
/// <summary>
2025-11-05 21:34:21 +08:00
/// 打印鼠标位置对象的组件值。
/// </summary>
2025-11-05 21:34:21 +08:00
private void PrintMouseHoveredObjectDetails(string outputPath)
{
2025-11-05 21:34:21 +08:00
StringBuilder sb = new StringBuilder();
sb.AppendLine("--- 鼠标悬停对象详细信息 ---");
sb.AppendLine("----------------------------\n");
GameObject hoveredObject = GetHoveredObject();
if (hoveredObject != null)
{
2025-11-05 21:34:21 +08:00
sb.AppendLine($"悬停的GameObject: {hoveredObject.name} (激活状态: {hoveredObject.activeSelf}, 标签: {hoveredObject.tag}, 层: {LayerMask.LayerToName(hoveredObject.layer)})");
sb.AppendLine($"组件及其值:\n");
Component[] components = hoveredObject.GetComponents<Component>();
foreach (Component comp in components)
{
2025-11-05 21:34:21 +08:00
if (comp == null) continue;
sb.AppendLine($" === 组件: {comp.GetType().Name} ===");
// 使用反射打印组件的字段和属性值
PrintObjectProperties(comp, 0, sb, new HashSet<object>(), " "); // 初始缩进4个空格
sb.AppendLine();
}
}
2025-11-05 21:34:21 +08:00
else
{
2025-11-05 21:34:21 +08:00
sb.AppendLine("当前鼠标下方没有对象。");
}
2025-11-05 21:34:21 +08:00
string filePath = Path.Combine(outputPath, "MouseHoverObjectDetails.txt");
File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8); // 使用UTF8编码以支持更多字符
Debug.Log($"鼠标悬停对象详情已保存到: {filePath}");
}
/// <summary>
2025-11-05 21:34:21 +08:00
/// 获取鼠标下方的GameObject优先UI其次3D场景对象
/// </summary>
2025-11-05 21:34:21 +08:00
private GameObject GetHoveredObject()
{
2025-11-05 21:34:21 +08:00
// 优先检测UI对象
if (EventSystem.current != null)
{
PointerEventData eventData = new PointerEventData(EventSystem.current);
eventData.position = Input.mousePosition;
List<RaycastResult> uiRaycastResults = new List<RaycastResult>();
EventSystem.current.RaycastAll(eventData, uiRaycastResults);
// 过滤掉非 interactable 的UI元素或者不包含 CanvasRenderer 的元素可能更关注可见和可交互的UI
foreach (var result in uiRaycastResults)
{
if (result.gameObject != null && result.gameObject.GetComponent<CanvasRenderer>() != null)
{
return result.gameObject; // 返回第一个有效的UI元素
}
}
}
else
{
2025-11-05 21:34:21 +08:00
Debug.LogWarning("场景中没有EventSystem无法检测UI对象。请确保场景中存在一个EventSystem GameObject。");
}
2025-11-05 21:34:21 +08:00
// 如果没有UI对象则检测3D场景对象
if (Camera.main != null)
{
2025-11-05 21:34:21 +08:00
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
// 仅检测默认层,或可配置的层
if (Physics.Raycast(ray, out RaycastHit hit))
{
2025-11-05 21:34:21 +08:00
return hit.collider.gameObject;
}
}
2025-11-05 21:34:21 +08:00
else
{
Debug.LogWarning("场景中没有主摄像机(Tagged 'MainCamera')无法进行3D射线检测。请确保主摄像机正确标记。");
}
2025-11-05 21:34:21 +08:00
return null;
}
/// <summary>
2025-11-05 21:34:21 +08:00
/// 使用反射递归打印对象的字段和属性值。
/// </summary>
2025-11-05 21:34:21 +08:00
/// <param name="obj">要打印的对象。</param>
/// <param name="currentDepth">当前反射深度。</param>
/// <param name="sb">StringBuilder用于构建输出。</param>
/// <param name="visitedObjects">用于检测循环引用的已访问对象集合。</param>
/// <param name="indent">当前缩进字符串。</param>
private void PrintObjectProperties(object obj, int currentDepth, StringBuilder sb, HashSet<object> visitedObjects, string indent)
{
2025-11-05 21:34:21 +08:00
if (obj == null)
{
2025-11-05 21:34:21 +08:00
sb.AppendLine("null");
return;
}
2025-11-05 21:34:21 +08:00
Type type = obj.GetType();
2025-11-05 21:34:21 +08:00
// 1. 处理基本类型、字符串、枚举
// 注意string是引用类型但行为上通常被视为值类型其ToString是其自身
if (type.IsPrimitive || obj is string || type.IsEnum || obj is decimal || obj is DateTime)
{
2025-11-05 21:34:21 +08:00
sb.AppendLine($"({type.Name}) {obj}"); // 显示类型名,更清晰
return;
}
2025-11-05 21:34:21 +08:00
// 2. 处理常见的Unity值类型如Vector3, Quaternion, Color, Rect等
// 这些类型通常有很好的ToString()方法,且不应过度深入反射其内部字段
if (obj is Vector2 || obj is Vector3 || obj is Vector4 || obj is Quaternion ||
obj is Color || obj is Color32 || obj is Rect || obj is Bounds ||
obj is AnimationCurve || obj is LayerMask || obj is Matrix4x4)
{
2025-11-05 21:34:21 +08:00
sb.AppendLine($"({type.Name}) {obj}"); // 显示类型名,更清晰
return;
}
2025-11-05 21:34:21 +08:00
// 3. 处理UnityEngine.Object类型但不是Component或GameObject本身
// 例如 Material, Texture, ScriptableObject等通常只打印其名称或ToString()就足够
if (typeof(UnityEngine.Object).IsAssignableFrom(type) && !(obj is Component) && !(obj is GameObject))
{
2025-11-05 21:34:21 +08:00
// 对于这些Unity对象ToString()通常会返回对象名和类型,足够了
sb.AppendLine($"({type.Name}) {obj.ToString()}");
return;
}
2025-11-05 21:34:21 +08:00
// 4. 检查最大反射深度
if (currentDepth >= MAX_REFLECTION_DEPTH)
{
2025-11-05 21:34:21 +08:00
sb.AppendLine($"{indent}... (达到最大反射深度)");
return;
}
2025-11-05 21:34:21 +08:00
// 5. 检查循环引用(仅对引用类型有效,且不是字符串那种特殊引用类型)
if (!type.IsValueType && !type.IsPrimitive && !(obj is string))
{
2025-11-05 21:34:21 +08:00
if (visitedObjects.Contains(obj))
{
2025-11-05 21:34:21 +08:00
sb.AppendLine($"{indent}... (检测到循环引用: {type.Name})");
return;
}
2025-11-05 21:34:21 +08:00
visitedObjects.Add(obj); // 标记为已访问
}
2025-11-05 21:34:21 +08:00
// 6. 处理集合类型数组、List、Dictionary等但不包括字符串
if (obj is IEnumerable enumerable)
{
// 对于字典直接打印IEnumerable会导致键值对混乱需要特殊处理
if (obj is IDictionary dictionary)
{
2025-11-05 21:34:21 +08:00
sb.AppendLine($"({type.Name}) Count={dictionary.Count} {{");
int count = 0;
foreach (DictionaryEntry entry in dictionary)
{
2025-11-05 21:34:21 +08:00
if (count >= MAX_COLLECTION_ELEMENTS_TO_PRINT)
{
2025-11-05 21:34:21 +08:00
sb.AppendLine($"{indent + " "}...(已截断,显示了{MAX_COLLECTION_ELEMENTS_TO_PRINT}对键值)");
break;
}
2025-11-05 21:34:21 +08:00
sb.Append($"{indent + " "}[Key]: ");
PrintObjectProperties(entry.Key, currentDepth + 1, sb, visitedObjects, indent + " "); // 额外的缩进
sb.Append($"{indent + " "}[Value]: ");
PrintObjectProperties(entry.Value, currentDepth + 1, sb, visitedObjects, indent + " "); // 额外的缩进
count++;
}
2025-11-05 21:34:21 +08:00
sb.AppendLine($"{indent}}}");
}
else // 普通的IEnumerableList, Array等
{
int elementCount = 0;
if (obj is ICollection collection)
elementCount = collection.Count;
else if (obj is Array array)
elementCount = array.Length;
else // 对于无法直接获取Count的IEnumerable需要遍历统计
{
2025-11-05 21:34:21 +08:00
var list = new List<object>();
foreach (var item in enumerable) list.Add(item);
elementCount = list.Count;
enumerable = list; // 重新赋值为可以重复遍历的list
}
2025-11-05 21:34:21 +08:00
sb.AppendLine($"({type.Name}) Count={elementCount} [");
int count = 0;
foreach (var item in enumerable)
{
if (count >= MAX_COLLECTION_ELEMENTS_TO_PRINT)
{
sb.AppendLine($"{indent + " "}...(已截断,显示了{MAX_COLLECTION_ELEMENTS_TO_PRINT}个元素)");
break;
}
sb.Append($"{indent + " "}- ");
PrintObjectProperties(item, currentDepth + 1, sb, visitedObjects, indent + " ");
count++;
}
sb.AppendLine($"{indent}]");
}
// 集合本身通常不直接导致循环引用其自身,其内部元素才可能。
// 故在处理集合后可以从visitedObjects中移除集合对象防止它阻止其他路径对它的访问。
if (!type.IsValueType && !type.IsPrimitive && !(obj is string)) visitedObjects.Remove(obj);
return;
}
2025-11-05 21:34:21 +08:00
// 7. 处理一般对象(类或结构体)的字段和属性
sb.AppendLine($"({type.Name}) {{");
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
// 字段
FieldInfo[] fields = type.GetFields(flags);
foreach (FieldInfo field in fields)
{
2025-11-05 21:34:21 +08:00
if (field.IsStatic) continue; // 跳过静态字段
if (field.IsDefined(typeof(ObsoleteAttribute), true)) continue; // 跳过过时字段
string propertyIndent = indent + " ";
sb.Append($"{propertyIndent}{field.Name}: ");
try
{
object fieldValue = field.GetValue(obj);
PrintObjectProperties(fieldValue, currentDepth + 1, sb, visitedObjects, propertyIndent);
}
catch (Exception e)
{
sb.AppendLine($"<无法获取值: {e.Message}>");
}
}
2025-11-05 21:34:21 +08:00
// 属性
PropertyInfo[] properties = type.GetProperties(flags);
foreach (PropertyInfo prop in properties)
{
2025-11-05 21:34:21 +08:00
// 跳过特殊名称属性如Unity内部的hideFlags、不可读属性、带索引器属性、过时属性
if (prop.IsSpecialName || !prop.CanRead || prop.GetIndexParameters().Length > 0 ||
prop.IsDefined(typeof(ObsoleteAttribute), true)) continue;
string propertyIndent = indent + " ";
sb.Append($"{propertyIndent}{prop.Name}: ");
try
{
object propValue = prop.GetValue(obj);
PrintObjectProperties(propValue, currentDepth + 1, sb, visitedObjects, propertyIndent);
}
catch (Exception e)
{
sb.AppendLine($"<无法获取值: {e.Message}>");
}
}
2025-11-05 21:34:21 +08:00
sb.AppendLine($"{indent}}}");
// 在对象处理完毕后,从已访问集合中移除(如果它是引用类型),
// 这允许在对象图的不同路径中再次遇到它(如果需要),但防止当前路径的循环。
if (!type.IsValueType && !type.IsPrimitive && !(obj is string)) visitedObjects.Remove(obj);
}
2025-11-01 15:18:34 +08:00
/// <summary>
2025-11-05 21:34:21 +08:00
/// 获取指定深度的缩进字符串。
2025-11-01 15:18:34 +08:00
/// </summary>
2025-11-05 21:34:21 +08:00
private string GetIndent(int depth)
2025-11-01 15:18:34 +08:00
{
2025-11-05 21:34:21 +08:00
return new string(' ', depth * 2);
2025-11-01 15:18:34 +08:00
}
}
2025-11-05 21:34:21 +08:00
}