feat: 受击音效更新类别控制
This commit is contained in:
@@ -1,490 +1,465 @@
|
||||
using System;
|
||||
using System.Collections;
|
||||
using System.Collections.Generic;
|
||||
using System.IO;
|
||||
using System.Reflection;
|
||||
using System.Text;
|
||||
using UnityEngine;
|
||||
using UnityEngine.EventSystems;
|
||||
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 FOLDER_NAME = "GameObjectSnapshots";
|
||||
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)) CaptureAndPrintSceneInfo();
|
||||
if (Input.GetKeyDown(KeyCode.F2))
|
||||
{
|
||||
CaptureAndPrintSnapshot();
|
||||
}
|
||||
}
|
||||
|
||||
private void CaptureAndPrintSceneInfo()
|
||||
private void CaptureAndPrintSnapshot()
|
||||
{
|
||||
var desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
||||
var outputFolderPath = Path.Combine(desktopPath, FOLDER_NAME);
|
||||
// 获取桌面路径
|
||||
string desktopPath = Environment.GetFolderPath(Environment.SpecialFolder.Desktop);
|
||||
|
||||
// 1. 创建主文件夹 (如果不存在)
|
||||
string baseOutputPath = Path.Combine(desktopPath, BASE_FOLDER_NAME);
|
||||
try
|
||||
{
|
||||
if (!Directory.Exists(outputFolderPath))
|
||||
if (!Directory.Exists(baseOutputPath))
|
||||
{
|
||||
Directory.CreateDirectory(outputFolderPath);
|
||||
Debug.Log($"创建输出文件夹: {outputFolderPath}");
|
||||
Directory.CreateDirectory(baseOutputPath);
|
||||
Debug.Log($"已创建主快照文件夹: {baseOutputPath}");
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"创建文件夹失败: {outputFolderPath} - {ex.Message}");
|
||||
Debug.LogError($"无法创建主快照文件夹 {baseOutputPath}: {e.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
var activeSceneName = SceneManager.GetActiveScene().name;
|
||||
var timestamp = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||
var fileName = $"{activeSceneName}_FullSnapshot_{timestamp}.txt"; // 修改文件名以示区别
|
||||
var fullFilePath = Path.Combine(outputFolderPath, fileName);
|
||||
|
||||
var sb = new StringBuilder();
|
||||
|
||||
sb.AppendLine("=================================================");
|
||||
sb.AppendLine($"场景信息快照 - 活跃场景: {activeSceneName}");
|
||||
sb.AppendLine($"生成时间: {DateTime.Now}");
|
||||
sb.AppendLine("=================================================");
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("--- 鼠标位置对象信息 ---");
|
||||
AppendMouseHoverObjectInfo(sb);
|
||||
sb.AppendLine();
|
||||
|
||||
sb.AppendLine("--- 所有加载场景的活跃游戏对象层次结构及其组件 ---");
|
||||
|
||||
// 遍历所有已加载的场景
|
||||
for (var i = 0; i < SceneManager.sceneCount; i++)
|
||||
{
|
||||
var currentScene = SceneManager.GetSceneAt(i);
|
||||
|
||||
// 打印场景名称作为分割线
|
||||
sb.AppendLine($"\n===== 场景: {currentScene.name} ===== " +
|
||||
(currentScene == SceneManager.GetActiveScene() ? "(活跃场景)" : ""));
|
||||
|
||||
GameObject[] rootObjects = currentScene.GetRootGameObjects();
|
||||
if (rootObjects.Length == 0)
|
||||
sb.AppendLine(" - 该场景没有根游戏对象。");
|
||||
else
|
||||
foreach (var go in rootObjects)
|
||||
AppendGameObjectInfo(go, 0, sb);
|
||||
}
|
||||
|
||||
sb.AppendLine("=================================================");
|
||||
|
||||
// 2. 在主文件夹内创建带时间戳的子文件夹
|
||||
string timestampFolderName = DateTime.Now.ToString("yyyyMMdd_HHmmss");
|
||||
string currentSnapshotOutputPath = Path.Combine(baseOutputPath, timestampFolderName);
|
||||
|
||||
try
|
||||
{
|
||||
File.WriteAllText(fullFilePath, sb.ToString(), Encoding.UTF8);
|
||||
Debug.Log($"场景信息已成功保存到: {fullFilePath}");
|
||||
if (!Directory.Exists(currentSnapshotOutputPath))
|
||||
{
|
||||
Directory.CreateDirectory(currentSnapshotOutputPath);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
catch (Exception e)
|
||||
{
|
||||
Debug.LogError($"保存文件失败: {fullFilePath} - {ex.Message}");
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归地将游戏对象的名称、活跃状态、组件及其子对象的层次结构追加到StringBuilder。
|
||||
/// **注意:此方法只会处理活跃状态为 activeSelf 的对象。**
|
||||
/// </summary>
|
||||
/// <param name="go">要处理的游戏对象。</param>
|
||||
/// <param name="indentLevel">当前缩进级别。</param>
|
||||
/// <param name="sb">StringBuilder实例。</param>
|
||||
private void AppendGameObjectInfo(GameObject go, int indentLevel, StringBuilder sb)
|
||||
{
|
||||
// 只有当对象自身是激活状态时才处理和打印
|
||||
if (!go || !go.activeSelf) return;
|
||||
|
||||
var indent = new string(' ', indentLevel * 4); // 每个层级使用4个空格缩进
|
||||
|
||||
// 打印游戏对象名称和活跃状态
|
||||
sb.AppendLine(
|
||||
$"{indent}[{go.name}] (ActiveSelf: {go.activeSelf}, ActiveInHierarchy: {go.activeInHierarchy})");
|
||||
|
||||
// 打印所有组件
|
||||
var components = go.GetComponents<Component>();
|
||||
foreach (var comp in components)
|
||||
if (comp) // 某些组件可能在运行时被销毁
|
||||
sb.AppendLine($"{indent} - Component: {comp.GetType().Name}");
|
||||
|
||||
// 递归处理子对象
|
||||
foreach (Transform child in go.transform) AppendGameObjectInfo(child.gameObject, indentLevel + 1, sb);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 尝试检测鼠标位置下方的UI元素或场景对象,并将其路径和组件信息追加到StringBuilder。
|
||||
/// </summary>
|
||||
/// <param name="sb">StringBuilder实例。</param>
|
||||
private void AppendMouseHoverObjectInfo(StringBuilder sb)
|
||||
{
|
||||
// 首先尝试Raycast UI元素
|
||||
var eventDataCurrentPosition = new PointerEventData(EventSystem.current);
|
||||
eventDataCurrentPosition.position = new Vector2(Input.mousePosition.x, Input.mousePosition.y);
|
||||
var results = new List<RaycastResult>();
|
||||
if (EventSystem.current)
|
||||
{
|
||||
EventSystem.current.RaycastAll(eventDataCurrentPosition, results);
|
||||
}
|
||||
|
||||
if (results.Count > 0)
|
||||
{
|
||||
// UI元素优先级更高
|
||||
var uiObject = results[0].gameObject;
|
||||
var uiPath = GetGameObjectPath(uiObject);
|
||||
sb.AppendLine($"鼠标下方UI路径: {uiPath}");
|
||||
sb.AppendLine($" - 所在场景: {uiObject.scene.name}");
|
||||
|
||||
// 添加UI对象组件信息
|
||||
sb.AppendLine(" - UI对象组件信息:");
|
||||
AppendGameObjectComponentInfo(sb, uiObject, " "); // 增加缩进
|
||||
Debug.LogError($"无法创建快照子文件夹 {currentSnapshotOutputPath}: {e.Message}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 如果没有UI元素,尝试Raycast场景对象
|
||||
if (Camera.main != null)
|
||||
{
|
||||
var ray = Camera.main.ScreenPointToRay(Input.mousePosition);
|
||||
RaycastHit hit;
|
||||
if (Physics.Raycast(ray, out hit))
|
||||
{
|
||||
var sceneObjectPath = GetGameObjectPath(hit.collider.gameObject);
|
||||
sb.AppendLine($"鼠标下方场景对象路径: {sceneObjectPath}");
|
||||
sb.AppendLine($" - 所在场景: {hit.collider.gameObject.scene.name}");
|
||||
Debug.Log($"开始生成场景快照到: {currentSnapshotOutputPath}");
|
||||
|
||||
// 添加场景对象组件信息
|
||||
sb.AppendLine(" - 场景对象组件信息:");
|
||||
AppendGameObjectComponentInfo(sb, hit.collider.gameObject, " "); // 增加缩进
|
||||
return;
|
||||
// Part 1: 打印所有对象的对象树及其组件
|
||||
PrintAllGameObjectsTree(currentSnapshotOutputPath);
|
||||
|
||||
// Part 2: 打印鼠标位置对象的组件值
|
||||
PrintMouseHoveredObjectDetails(currentSnapshotOutputPath);
|
||||
|
||||
Debug.Log("场景快照生成完毕!");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打印所有场景对象的对象树(包括DontDestroyOnLoad)及其组件。
|
||||
/// </summary>
|
||||
private void PrintAllGameObjectsTree(string outputPath)
|
||||
{
|
||||
StringBuilder sb = new StringBuilder();
|
||||
sb.AppendLine("--- 所有激活场景对象树 ---");
|
||||
sb.AppendLine("--------------------------\n");
|
||||
|
||||
// 用于存储按场景分组的根对象
|
||||
var sceneRootGameObjects = new Dictionary<Scene, List<GameObject>>();
|
||||
// 用于存储 DontDestroyOnLoad 对象
|
||||
var dontDestroyOnLoadRoots = new List<GameObject>();
|
||||
|
||||
// 1. 遍历所有加载的场景,获取其根对象
|
||||
for (int i = 0; i < SceneManager.sceneCount; i++)
|
||||
{
|
||||
Scene scene = SceneManager.GetSceneAt(i);
|
||||
sceneRootGameObjects[scene] = new List<GameObject>(scene.GetRootGameObjects());
|
||||
}
|
||||
|
||||
// 2. 查找 DontDestroyOnLoad 对象
|
||||
// DontDestroyOnLoad 对象不属于任何通过 SceneManager.GetSceneAt 获取的“普通”场景
|
||||
// 它们通常在特殊的 "DontDestroyOnLoad" 场景中(在Unity编辑器中可见),但在运行时无法直接通过 SceneManager.GetSceneAt 访问。
|
||||
// 因此,我们遍历所有活跃的GameObject,找出那些是根对象但又不属于任何已知场景的。
|
||||
GameObject[] allActiveGameObjectsInHierarchy = FindObjectsOfType<GameObject>(); // 获取所有活跃的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<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)
|
||||
{
|
||||
// 检查是否已经有一个伪的 "DontDestroyOnLoad" 场景被 Unity 在某些情境下自动添加
|
||||
// 如果是,为了避免重复,且让输出更清晰,可以先尝试移除这些。
|
||||
// 但是在 FindObjectsOfType 之后再分组,这种方式更健壮,不用管它是否有“场景”
|
||||
|
||||
sb.AppendLine("\n=== DontDestroyOnLoad 对象 ===\n");
|
||||
foreach (GameObject root in dontDestroyOnLoadRoots.OrderBy(g => g.name))
|
||||
{
|
||||
PrintGameObjectRecursive(root, 0, sb, new HashSet<GameObject>());
|
||||
}
|
||||
}
|
||||
|
||||
string filePath = Path.Combine(outputPath, "SceneObjectTree.txt");
|
||||
File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8); // 使用UTF8编码以支持更多字符
|
||||
Debug.Log($"场景对象树已保存到: {filePath}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 递归打印GameObject及其子级和组件。
|
||||
/// </summary>
|
||||
private void PrintGameObjectRecursive(GameObject go, int depth, StringBuilder sb, HashSet<GameObject> 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<Component>();
|
||||
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在树中只有一个父级。
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 打印鼠标位置对象的组件值。
|
||||
/// </summary>
|
||||
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<Component>();
|
||||
foreach (Component comp in components)
|
||||
{
|
||||
if (comp == null) continue;
|
||||
sb.AppendLine($" === 组件: {comp.GetType().Name} ===");
|
||||
// 使用反射打印组件的字段和属性值
|
||||
PrintObjectProperties(comp, 0, sb, new HashSet<object>(), " "); // 初始缩进4个空格
|
||||
sb.AppendLine();
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
sb.AppendLine("警告: 场景中没有主摄像机(Camera.main)或未被标记为 'MainCamera'。无法检测鼠标下的场景对象。");
|
||||
sb.AppendLine("当前鼠标下方没有对象。");
|
||||
}
|
||||
|
||||
sb.AppendLine("鼠标位置处没有检测到UI元素或场景对象。");
|
||||
string filePath = Path.Combine(outputPath, "MouseHoverObjectDetails.txt");
|
||||
File.WriteAllText(filePath, sb.ToString(), Encoding.UTF8); // 使用UTF8编码以支持更多字符
|
||||
Debug.Log($"鼠标悬停对象详情已保存到: {filePath}");
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 为指定的GameObject追加其所有组件的信息。
|
||||
/// 获取鼠标下方的GameObject(优先UI,其次3D场景对象)。
|
||||
/// </summary>
|
||||
/// <param name="sb">StringBuilder实例。</param>
|
||||
/// <param name="obj">目标GameObject。</param>
|
||||
/// <param name="indent">当前的缩进字符串。</param>
|
||||
private void AppendGameObjectComponentInfo(StringBuilder sb, GameObject obj, string indent)
|
||||
private GameObject GetHoveredObject()
|
||||
{
|
||||
Component[] components = obj.GetComponents<Component>();
|
||||
if (components.Length == 0)
|
||||
// 优先检测UI对象
|
||||
if (EventSystem.current != null)
|
||||
{
|
||||
sb.AppendLine($"{indent} - 无组件");
|
||||
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
|
||||
{
|
||||
Debug.LogWarning("场景中没有EventSystem,无法检测UI对象。请确保场景中存在一个EventSystem GameObject。");
|
||||
}
|
||||
|
||||
// 如果没有UI对象,则检测3D场景对象
|
||||
if (Camera.main != null)
|
||||
{
|
||||
Ray ray = Camera.main.ScreenPointToRay(Input.mousePosition);
|
||||
// 仅检测默认层,或可配置的层
|
||||
if (Physics.Raycast(ray, out RaycastHit hit))
|
||||
{
|
||||
return hit.collider.gameObject;
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
Debug.LogWarning("场景中没有主摄像机(Tagged 'MainCamera'),无法进行3D射线检测。请确保主摄像机正确标记。");
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 使用反射递归打印对象的字段和属性值。
|
||||
/// </summary>
|
||||
/// <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)
|
||||
{
|
||||
if (obj == null)
|
||||
{
|
||||
sb.AppendLine("null");
|
||||
return;
|
||||
}
|
||||
|
||||
foreach (var component in components)
|
||||
Type type = obj.GetType();
|
||||
|
||||
// 1. 处理基本类型、字符串、枚举
|
||||
// 注意:string是引用类型,但行为上通常被视为值类型,其ToString是其自身
|
||||
if (type.IsPrimitive || obj is string || type.IsEnum || obj is decimal || obj is DateTime)
|
||||
{
|
||||
if (component == null)
|
||||
{
|
||||
sb.AppendLine($"{indent} - (空组件)");
|
||||
continue;
|
||||
}
|
||||
|
||||
// 尝试获取Behaviour的enabled状态
|
||||
string enabledStatus = (component is Behaviour behaviour) ? behaviour.enabled.ToString() : "N/A";
|
||||
sb.AppendLine($"{indent} - 组件: {component.GetType().Name} (Enabled: {enabledStatus})");
|
||||
AppendComponentPropertiesAndFields(sb, component, indent + " "); // 更深一层缩进,显示组件的属性/字段
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 利用反射为指定的组件追加其公共属性和字段的信息。
|
||||
/// </summary>
|
||||
/// <param name="sb">StringBuilder实例。</param>
|
||||
/// <param name="component">目标组件。</param>
|
||||
/// <param name="indent">当前的缩进字符串。</param>
|
||||
private void AppendComponentPropertiesAndFields(StringBuilder sb, Component component, string indent)
|
||||
{
|
||||
var bindingFlags = BindingFlags.Public | BindingFlags.Instance;
|
||||
var componentType = component.GetType();
|
||||
// 收集所有公共实例属性和字段
|
||||
var members = new List<MemberInfo>();
|
||||
members.AddRange(componentType.GetProperties(bindingFlags));
|
||||
members.AddRange(componentType.GetFields(bindingFlags));
|
||||
bool hasPrintedAnything = false;
|
||||
foreach (var member in members)
|
||||
{
|
||||
// 排除一些常见或特定组件上可能导致冗余或问题的属性/字段
|
||||
if (IsMemberToExclude(member, component)) continue;
|
||||
object value = null;
|
||||
string memberName = member.Name;
|
||||
try
|
||||
{
|
||||
if (member is PropertyInfo pi)
|
||||
{
|
||||
if (pi.CanRead) // 确保属性可读
|
||||
{
|
||||
value = pi.GetValue(component);
|
||||
}
|
||||
else
|
||||
{
|
||||
// 忽略不可读的属性
|
||||
continue;
|
||||
}
|
||||
}
|
||||
else if (member is FieldInfo fi)
|
||||
{
|
||||
value = fi.GetValue(component);
|
||||
}
|
||||
}
|
||||
catch (Exception ex)
|
||||
{
|
||||
// 捕获获取值时可能发生的异常
|
||||
sb.AppendLine($"{indent}- {memberName}: <获取失败: {ex.GetType().Name}>");
|
||||
hasPrintedAnything = true;
|
||||
continue;
|
||||
}
|
||||
|
||||
// 格式化值进行显示,初始深度为0
|
||||
string formattedValue = FormatValueForDisplay(value, 0);
|
||||
sb.AppendLine($"{indent}- {memberName}: {formattedValue}");
|
||||
hasPrintedAnything = true;
|
||||
sb.AppendLine($"({type.Name}) {obj}"); // 显示类型名,更清晰
|
||||
return;
|
||||
}
|
||||
|
||||
if (!hasPrintedAnything)
|
||||
// 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)
|
||||
{
|
||||
sb.AppendLine($"{indent}- (无公共属性或字段)");
|
||||
sb.AppendLine($"({type.Name}) {obj}"); // 显示类型名,更清晰
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 判断一个成员是否应该被排除在打印列表之外。
|
||||
/// 用于过滤掉冗余或可能导致深度递归的成员。
|
||||
/// </summary>
|
||||
/// <param name="member">要检查的MemberInfo。</param>
|
||||
/// <param name="component">该成员所属的组件实例。</param>
|
||||
/// <returns>如果应排除则返回true。</returns>
|
||||
private bool IsMemberToExclude(MemberInfo member, Component component)
|
||||
{
|
||||
// 排除从UnityEngine.Object继承的常见属性,这些通常是GameObject级别的元数据,
|
||||
// 或者可能导致不必要的递归。
|
||||
// 特别是gameObject和transform,它们的类型就是GameObject和Transform,递归它们没有意义,
|
||||
// 且其值就是组件所依附的GameObject和Transform,已经通过GetGameObjectPath显示了。
|
||||
switch (member.Name)
|
||||
|
||||
// 3. 处理UnityEngine.Object类型(但不是Component或GameObject本身)
|
||||
// 例如 Material, Texture, ScriptableObject等,通常只打印其名称或ToString()就足够
|
||||
if (typeof(UnityEngine.Object).IsAssignableFrom(type) && !(obj is Component) && !(obj is GameObject))
|
||||
{
|
||||
case "hideFlags": // Unity内部的标志,通常不需要显示
|
||||
case "name": // GameObject的名称,已在路径中显示
|
||||
case "tag": // GameObject的标签,可从GameObject直接获取
|
||||
case "layer": // GameObject的层,可从GameObject直接获取
|
||||
case "useGUILayout": // Unity内部GUI相关的,通常不作为组件值关心
|
||||
case "runInEditMode": // Unity编辑器模式相关,通常不作为组件值关心
|
||||
// 对于Component基类上的gameObject和transform属性,它们直接指向宿主对象和其Transform。
|
||||
// 打印它们本身就是重复的且可能误导(不是组件内部的独特“值”)。
|
||||
case "gameObject":
|
||||
case "transform":
|
||||
case "isStatic": // GameObject的isStatic状态
|
||||
return true;
|
||||
// 对于这些Unity对象,ToString()通常会返回对象名和类型,足够了
|
||||
sb.AppendLine($"({type.Name}) {obj.ToString()}");
|
||||
return;
|
||||
}
|
||||
|
||||
// 进一步过滤掉一些Unity内部或编辑器相关的属性,这些属性通常在运行时不提供有用的组件值信息。
|
||||
if (member.DeclaringType == typeof(Behaviour) || member.DeclaringType == typeof(MonoBehaviour))
|
||||
{
|
||||
switch (member.Name)
|
||||
{
|
||||
case "isActiveAndEnabled": // 行为体的激活状态,通常与enabled一起考虑
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化对象值以进行显示,特别是针对Unity的常见类型,以避免循环打印和提供简洁输出。
|
||||
/// </summary>
|
||||
/// <param name="value">要格式化的值。</param>
|
||||
/// <param name="currentDepth">当前的递归深度。</param>
|
||||
/// <returns>格式化后的字符串。</returns>
|
||||
private string FormatValueForDisplay(object value, int currentDepth = 0)
|
||||
{
|
||||
if (value == null) return "null";
|
||||
Type type = value.GetType();
|
||||
// 深度限制检查:如果超过最大深度,则返回提示信息
|
||||
|
||||
// 4. 检查最大反射深度
|
||||
if (currentDepth >= MAX_REFLECTION_DEPTH)
|
||||
{
|
||||
// 对于集合,提供更具体一些的信息
|
||||
if (value is Array array1) return $"Array (Count: {array1.Length}, Max Depth Reached)";
|
||||
if (value is IList list1) return $"List (Count: {list1.Count}, Max Depth Reached)";
|
||||
if (value is IDictionary dictionary) return $"Dictionary (Count: {dictionary.Count}, Max Depth Reached)";
|
||||
return $"[{type.Name} (Max Depth Reached)]";
|
||||
sb.AppendLine($"{indent}... (达到最大反射深度)");
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 基本类型、字符串、枚举:直接ToString()
|
||||
if (type.IsPrimitive || type == typeof(string) || type.IsEnum)
|
||||
// 5. 检查循环引用(仅对引用类型有效,且不是字符串那种特殊引用类型)
|
||||
if (!type.IsValueType && !type.IsPrimitive && !(obj is string))
|
||||
{
|
||||
return value.ToString();
|
||||
if (visitedObjects.Contains(obj))
|
||||
{
|
||||
sb.AppendLine($"{indent}... (检测到循环引用: {type.Name})");
|
||||
return;
|
||||
}
|
||||
visitedObjects.Add(obj); // 标记为已访问
|
||||
}
|
||||
|
||||
// 2. 常见的Unity结构体:特殊格式化,避免深度递归并提供简洁输出
|
||||
if (value is Vector2 vec2) return $"({vec2.x:F2}, {vec2.y:F2})";
|
||||
if (value is Vector3 vec3) return $"({vec3.x:F2}, {vec3.y:F2}, {vec3.z:F2})";
|
||||
if (value is Vector4 vec4) return $"({vec4.x:F2}, {vec4.y:F2}, {vec4.z:F2}, {vec4.w:F2})";
|
||||
|
||||
// Quaternion默认ToString()会显示x,y,z,w,但有时EulerAngles更直观。
|
||||
if (value is Quaternion q)
|
||||
return
|
||||
$"Q({q.x:F2}, {q.y:F2}, {q.z:F2}, {q.w:F2}) (Euler: {q.eulerAngles.x:F2}, {q.eulerAngles.y:F2}, {q.eulerAngles.z:F2})";
|
||||
|
||||
if (value is Color color) return $"RGBA({color.r:F2}, {color.g:F2}, {color.b:F2}, {color.a:F2})";
|
||||
if (value is Rect rect)
|
||||
return $"Rect(Pos:({rect.xMin:F2},{rect.yMin:F2}) Size:({rect.width:F2},{rect.height:F2}))";
|
||||
if (value is Bounds bounds)
|
||||
return
|
||||
$"Bounds(Center:({bounds.center.x:F2},{bounds.center.y:F2},{bounds.center.z:F2}) Extents:({bounds.extents.x:F2},{bounds.extents.y:F2},{bounds.extents.z:F2}))";
|
||||
if (value is LayerMask layerMask)
|
||||
// 6. 处理集合类型(数组、List、Dictionary等,但不包括字符串)
|
||||
if (obj is IEnumerable enumerable)
|
||||
{
|
||||
// LayerMask的值可能代表多个层,或一个单一层。LayerToName只能转换单一层。
|
||||
// 对于多个层,返回其原始值更有意义。
|
||||
return $"LayerMask(Value: {layerMask.value})";
|
||||
// 对于字典,直接打印IEnumerable会导致键值对混乱,需要特殊处理
|
||||
if (obj is IDictionary dictionary)
|
||||
{
|
||||
sb.AppendLine($"({type.Name}) Count={dictionary.Count} {{");
|
||||
int count = 0;
|
||||
foreach (DictionaryEntry entry in dictionary)
|
||||
{
|
||||
if (count >= MAX_COLLECTION_ELEMENTS_TO_PRINT)
|
||||
{
|
||||
sb.AppendLine($"{indent + " "}...(已截断,显示了{MAX_COLLECTION_ELEMENTS_TO_PRINT}对键值)");
|
||||
break;
|
||||
}
|
||||
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++;
|
||||
}
|
||||
sb.AppendLine($"{indent}}}");
|
||||
}
|
||||
else // 普通的IEnumerable(List, Array等)
|
||||
{
|
||||
int elementCount = 0;
|
||||
if (obj is ICollection collection)
|
||||
elementCount = collection.Count;
|
||||
else if (obj is Array array)
|
||||
elementCount = array.Length;
|
||||
else // 对于无法直接获取Count的IEnumerable,需要遍历统计
|
||||
{
|
||||
var list = new List<object>();
|
||||
foreach (var item in enumerable) list.Add(item);
|
||||
elementCount = list.Count;
|
||||
enumerable = list; // 重新赋值为可以重复遍历的list
|
||||
}
|
||||
|
||||
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;
|
||||
}
|
||||
|
||||
// 3. GameObject/Component引用:只打印名称或类型,避免无限递归
|
||||
if (value is GameObject go) return $"GameObject:'{go.name}'";
|
||||
if (value is Component comp)
|
||||
return $"Component:'{comp.GetType().Name}' on GameObject:'{comp.gameObject.name}'";
|
||||
// 7. 处理一般对象(类或结构体)的字段和属性
|
||||
sb.AppendLine($"({type.Name}) {{");
|
||||
|
||||
// 4. 集合类型:现在会打印内容,调用专门的辅助方法
|
||||
if (value is Array array)
|
||||
BindingFlags flags = BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance;
|
||||
|
||||
// 字段
|
||||
FieldInfo[] fields = type.GetFields(flags);
|
||||
foreach (FieldInfo field in fields)
|
||||
{
|
||||
return FormatCollectionForDisplay(array, currentDepth + 1);
|
||||
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}>");
|
||||
}
|
||||
}
|
||||
|
||||
if (value is IList list)
|
||||
// 属性
|
||||
PropertyInfo[] properties = type.GetProperties(flags);
|
||||
foreach (PropertyInfo prop in properties)
|
||||
{
|
||||
return FormatCollectionForDisplay(list, currentDepth + 1);
|
||||
// 跳过特殊名称属性(如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}>");
|
||||
}
|
||||
}
|
||||
|
||||
if (value is IDictionary dict)
|
||||
{
|
||||
return FormatCollectionForDisplay(dict, currentDepth + 1);
|
||||
}
|
||||
sb.AppendLine($"{indent}}}");
|
||||
|
||||
// 对于其他不可转换为Array/IList/IDictionary的IEnumerable,但又不是字符串的类型
|
||||
if (value is IEnumerable enumerable && !(value is string))
|
||||
{
|
||||
return FormatCollectionForDisplay(enumerable, currentDepth + 1);
|
||||
}
|
||||
|
||||
// 5. 其他复杂引用类型:只打印其类型名称,默认不深入其内部 (除非深度允许,但在深度限制前这里只会显示类型名)
|
||||
return type.Name; // 例如: Material, Texture2D等,只显示类型名
|
||||
// 在对象处理完毕后,从已访问集合中移除(如果它是引用类型),
|
||||
// 这允许在对象图的不同路径中再次遇到它(如果需要),但防止当前路径的循环。
|
||||
if (!type.IsValueType && !type.IsPrimitive && !(obj is string)) visitedObjects.Remove(obj);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 格式化集合对象以进行显示,支持深度限制和元素数量限制。
|
||||
/// 获取指定深度的缩进字符串。
|
||||
/// </summary>
|
||||
/// <param name="collection">要格式化的集合。</param>
|
||||
/// <param name="currentDepth">当前的递归深度。</param>
|
||||
/// <returns>格式化后的集合字符串。</returns>
|
||||
private string FormatCollectionForDisplay(IEnumerable collection, int currentDepth)
|
||||
private string GetIndent(int depth)
|
||||
{
|
||||
if (collection == null) return "null collection";
|
||||
Type collectionType = collection.GetType();
|
||||
StringBuilder sb = new StringBuilder();
|
||||
// 尝试获取集合的类型名,去除可能的反引号(用于泛型类型)
|
||||
string collectionTypeName = collectionType.IsGenericType
|
||||
? collectionType.Name.Substring(0, collectionType.Name.IndexOf('`'))
|
||||
: collectionType.Name.Replace("[]", "");
|
||||
sb.Append(collectionTypeName);
|
||||
|
||||
sb.Append(" [");
|
||||
int i = 0;
|
||||
foreach (var item in collection)
|
||||
{
|
||||
if (i >= MAX_COLLECTION_ELEMENTS_TO_PRINT)
|
||||
{
|
||||
sb.Append(", ...");
|
||||
break;
|
||||
}
|
||||
|
||||
if (i > 0) sb.Append(", ");
|
||||
if (item is DictionaryEntry entry) // 针对非泛型IDictionary
|
||||
{
|
||||
sb.Append(
|
||||
$"{{{FormatValueForDisplay(entry.Key, currentDepth + 1)}: {FormatValueForDisplay(entry.Value, currentDepth + 1)}}}");
|
||||
}
|
||||
else
|
||||
{
|
||||
Type itemType = item?.GetType();
|
||||
// 针对泛型IDictionary,其元素是KeyValuePair<TKey, TValue>
|
||||
if (itemType != null && itemType.IsGenericType &&
|
||||
itemType.GetGenericTypeDefinition() == typeof(KeyValuePair<,>))
|
||||
{
|
||||
// 使用反射获取Key和Value属性
|
||||
PropertyInfo keyProperty = itemType.GetProperty("Key");
|
||||
PropertyInfo valueProperty = itemType.GetProperty("Value");
|
||||
if (keyProperty != null && valueProperty != null)
|
||||
{
|
||||
object key = keyProperty.GetValue(item);
|
||||
object value = valueProperty.GetValue(item);
|
||||
sb.Append(
|
||||
$"{{{FormatValueForDisplay(key, currentDepth + 1)}: {FormatValueForDisplay(value, currentDepth + 1)}}}");
|
||||
}
|
||||
else
|
||||
{
|
||||
// Fallback in case Key/Value properties aren't found
|
||||
sb.Append(FormatValueForDisplay(item, currentDepth + 1));
|
||||
}
|
||||
}
|
||||
else
|
||||
{
|
||||
// 格式化普通集合元素
|
||||
sb.Append(FormatValueForDisplay(item, currentDepth + 1));
|
||||
}
|
||||
}
|
||||
|
||||
i++;
|
||||
}
|
||||
|
||||
// 尝试获取集合的实际数量
|
||||
string countInfo = "N/A";
|
||||
if (collection is ICollection c)
|
||||
{
|
||||
countInfo = c.Count.ToString();
|
||||
}
|
||||
else if (collection is Array a)
|
||||
{
|
||||
countInfo = a.Length.ToString();
|
||||
}
|
||||
|
||||
sb.Append($"] (Count: {countInfo})");
|
||||
return sb.ToString();
|
||||
return new string(' ', depth * 2);
|
||||
}
|
||||
|
||||
/// <summary>
|
||||
/// 获取给定游戏对象的完整层次路径。
|
||||
/// </summary>
|
||||
/// <param name="go">要获取路径的游戏对象。</param>
|
||||
/// <returns>游戏对象的完整路径,例如 "Parent/Child/Object"。</returns>
|
||||
private string GetGameObjectPath(GameObject go)
|
||||
{
|
||||
if (go == null) return "N/A";
|
||||
|
||||
var path = go.name;
|
||||
var currentTransform = go.transform;
|
||||
|
||||
while (currentTransform.parent != null)
|
||||
{
|
||||
currentTransform = currentTransform.parent;
|
||||
path = currentTransform.name + "/" + path;
|
||||
}
|
||||
|
||||
return path;
|
||||
}
|
||||
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -13,7 +13,7 @@ using System.Reflection;
|
||||
[assembly: System.Reflection.AssemblyCompanyAttribute("折纸的小箱子")]
|
||||
[assembly: System.Reflection.AssemblyConfigurationAttribute("Release")]
|
||||
[assembly: System.Reflection.AssemblyFileVersionAttribute("1.0.0")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+0206a83f56b5a794fe2f173b4a047cc4f0d4cd90")]
|
||||
[assembly: System.Reflection.AssemblyInformationalVersionAttribute("1.0.0+5d69efbc3f80a5422cef0884e02fb27adf20b467")]
|
||||
[assembly: System.Reflection.AssemblyProductAttribute("SceneSnapshot")]
|
||||
[assembly: System.Reflection.AssemblyTitleAttribute("SceneSnapshot")]
|
||||
[assembly: System.Reflection.AssemblyVersionAttribute("1.0.0")]
|
||||
|
||||
@@ -1 +1 @@
|
||||
532b9b8318b6010ae03f608127d55a2c59d0b50d9243b633f698b5d460668837
|
||||
4cb78221af3251625ca99f740da024b47f366123d3acdfd330074e35f359044e
|
||||
|
||||
Binary file not shown.
@@ -1 +1 @@
|
||||
17619862901687226
|
||||
17623343068138064
|
||||
Reference in New Issue
Block a user