using UnityEngine;
using UnityEngine.SceneManagement;
using UnityEngine.EventSystems; // 用于UI射线检测
using System;
using System.IO;
using System.Text;
using System.Reflection;
using System.Collections; // 用于 IEnumerable
using System.Collections.Generic; // 用于 HashSet, List, Dictionary
using System.Linq; // 用于 OrderBy
namespace SceneSnapshot
{
internal class PrintTool : MonoBehaviour
{
private const string BASE_FOLDER_NAME = "GameObjectSnapshots"; // 主文件夹名称
private const int MAX_REFLECTION_DEPTH = 3; // 最大反射深度,防止循环引用或过深的对象图
private const int MAX_COLLECTION_ELEMENTS_TO_PRINT = 5; // 集合最多打印的元素数量
private void Update()
{
if (Input.GetKeyDown(KeyCode.F2))
{
CaptureAndPrintSnapshot();
}
}
private void CaptureAndPrintSnapshot()
{
// 获取桌面路径
string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
// 1. 创建主文件夹 (如果不存在)
string baseOutputPath = Path.Combine(desktopPath, BASE_FOLDER_NAME);
try
{
if (!Directory.Exists(baseOutputPath))
{
Directory.CreateDirectory(baseOutputPath);
Debug.Log($"已创建主快照文件夹: {baseOutputPath}");
}
}
catch (Exception e)
{
Debug.LogError($"无法创建主快照文件夹 {baseOutputPath}: {e.Message}");
return;
}
// 2. 在主文件夹内创建带时间戳的子文件夹
string timestampFolderName = DateTime.Now.ToString("yyyyMMdd_HHmmss");
string currentSnapshotOutputPath = Path.Combine(baseOutputPath, timestampFolderName);
try
{
if (!Directory.Exists(currentSnapshotOutputPath))
{
Directory.CreateDirectory(currentSnapshotOutputPath);
}
}
catch (Exception e)
{
Debug.LogError($"无法创建快照子文件夹 {currentSnapshotOutputPath}: {e.Message}");
return;
}
Debug.Log($"开始生成场景快照到: {currentSnapshotOutputPath}");
// Part 1: 打印所有对象的对象树及其组件
PrintAllGameObjectsTree(currentSnapshotOutputPath);
// Part 2: 打印鼠标位置对象的组件值
PrintMouseHoveredObjectDetails(currentSnapshotOutputPath);
Debug.Log("场景快照生成完毕!");
}
///
/// 打印所有场景对象的对象树(包括DontDestroyOnLoad)及其组件。
///
private void PrintAllGameObjectsTree(string outputPath)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("--- 所有激活场景对象树 ---");
sb.AppendLine("--------------------------\n");
// 用于存储按场景分组的根对象
var sceneRootGameObjects = new Dictionary>();
// 用于存储 DontDestroyOnLoad 对象
var dontDestroyOnLoadRoots = new List();
// 1. 遍历所有加载的场景,获取其根对象
for (int i = 0; i < SceneManager.sceneCount; i++)
{
Scene scene = SceneManager.GetSceneAt(i);
sceneRootGameObjects[scene] = new List(scene.GetRootGameObjects());
}
// 2. 查找 DontDestroyOnLoad 对象
// DontDestroyOnLoad 对象不属于任何通过 SceneManager.GetSceneAt 获取的“普通”场景
// 它们通常在特殊的 "DontDestroyOnLoad" 场景中(在Unity编辑器中可见),但在运行时无法直接通过 SceneManager.GetSceneAt 访问。
// 因此,我们遍历所有活跃的GameObject,找出那些是根对象但又不属于任何已知场景的。
GameObject[] allActiveGameObjectsInHierarchy = FindObjectsOfType(); // 获取所有活跃的GameObject
foreach (GameObject go in allActiveGameObjectsInHierarchy)
{
if (go.transform.parent == null) // 这是一个根对象
{
bool foundInLoadedScene = false;
foreach (var kvp in sceneRootGameObjects)
{
if (kvp.Value.Contains(go))
{
foundInLoadedScene = true;
break;
}
}
if (!foundInLoadedScene)
{
// 如果它不是任何已加载场景的根对象,那么它可能是DontDestroyOnLoad对象
dontDestroyOnLoadRoots.Add(go);
}
}
}
// 3. 打印普通场景的对象树
foreach (var kvp in sceneRootGameObjects)
{
Scene currentScene = kvp.Key;
List 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());
}
}
// 4. 打印 DontDestroyOnLoad 对象的对象树
if (dontDestroyOnLoadRoots.Count > 0)
{
// 检查是否已经有一个伪的 "DontDestroyOnLoad" 场景被 Unity 在某些情境下自动添加
// 如果是,为了避免重复,且让输出更清晰,可以先尝试移除这些。
// 但是在 FindObjectsOfType 之后再分组,这种方式更健壮,不用管它是否有“场景”
sb.AppendLine("\n=== DontDestroyOnLoad 对象 ===\n");
foreach (GameObject root in dontDestroyOnLoadRoots.OrderBy(g => g.name))
{
PrintGameObjectRecursive(root, 0, sb, new HashSet());
}
}
string filePath = Path.Combine(outputPath, "SceneObjectTree.txt");
File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8); // 使用UTF8编码以支持更多字符
Debug.Log($"场景对象树已保存到: {filePath}");
}
///
/// 递归打印GameObject及其子级和组件。
///
private void PrintGameObjectRecursive(GameObject go, int depth, StringBuilder sb, HashSet visited)
{
// 防止循环引用或重复打印
if (visited.Contains(go))
{
sb.AppendLine($"{GetIndent(depth)}{go.name} (循环引用检测到!)");
return;
}
visited.Add(go);
string indent = GetIndent(depth);
sb.AppendLine($"{indent}GameObject: {go.name} (激活状态: {go.activeSelf}, 标签: {go.tag}, 层: {LayerMask.LayerToName(go.layer)})");
Component[] components = go.GetComponents();
foreach (Component comp in components)
{
if (comp == null) continue; // 避免NRE,尽管不常见
sb.AppendLine($"{indent} 组件: {comp.GetType().Name}");
}
for (int i = 0; i < go.transform.childCount; i++)
{
PrintGameObjectRecursive(go.transform.GetChild(i).gameObject, depth + 1, sb, visited);
}
// 重要:在递归完成后通常不需要从visited中移除GameObject,
// 因为一个GameObject在对象树中只会以唯一的路径出现一次
// (除非它以某种非常规方式被引用,但这不属于标准GameObject层级)。
// 对于值类型或简单引用,可以在PrintObjectProperties中在处理完后移除。
// 对于GameObject层级,一旦访问完其所有子节点,它在该“分支”的任务就完成了。
// 如果不同根节点下可能会有相同的GameObject引用(例如通过Inspector引用),
// 那visited集合的作用是防止在*当前递归路径*中再次遇到同一个GameObject,从而避免死循环。
// 对于整个场景树的打印,visited集合可以保持不变,因为我们不期望同一个GameObject作为不同根节点的子物体链中的一部分。
// visited.Remove(go); // 对于GameObject树结构,这通常是不必要的,因为每个GameObject在树中只有一个父级。
}
///
/// 打印鼠标位置对象的组件值。
///
private void PrintMouseHoveredObjectDetails(string outputPath)
{
StringBuilder sb = new StringBuilder();
sb.AppendLine("--- 鼠标悬停对象详细信息 ---");
sb.AppendLine("----------------------------\n");
GameObject hoveredObject = GetHoveredObject();
if (hoveredObject != null)
{
sb.AppendLine($"悬停的GameObject: {hoveredObject.name} (激活状态: {hoveredObject.activeSelf}, 标签: {hoveredObject.tag}, 层: {LayerMask.LayerToName(hoveredObject.layer)})");
sb.AppendLine($"组件及其值:\n");
Component[] components = hoveredObject.GetComponents();
foreach (Component comp in components)
{
if (comp == null) continue;
sb.AppendLine($" === 组件: {comp.GetType().Name} ===");
// 使用反射打印组件的字段和属性值
PrintObjectProperties(comp, 0, sb, new HashSet