1. 项目概述与核心价值在Unity开发中调试和运行时数据查看一直是个痛点。编辑器模式下的Inspector窗口功能强大但一旦打包发布所有组件和属性的实时状态就变成了一个“黑盒”。你只能依赖日志输出或者自己费劲地写一堆UI控件来暴露关键参数既繁琐又难以维护。这就是为什么当我第一次接触到UnityRuntimeInspector这个插件时感觉像是打开了一扇新的大门。它本质上是一个可以在游戏运行时Runtime激活的Inspector和Hierarchy面板让你能在真机、模拟器或者编辑器播放模式下像在编辑器中一样查看、搜索甚至修改场景中的对象和组件属性。这个工具的核心价值远不止“免费”这么简单。对于独立开发者和小团队来说它极大地降低了调试和现场问题排查的门槛。想象一下你的游戏在测试人员的手机上崩溃了或者某个NPC的行为出现了异常。传统方式下你可能需要反复构建带调试日志的版本让测试人员复现并截图。而有了RuntimeInspector你可以在游戏内直接呼出一个调试面板实时查看问题对象的Transform位置、脚本的私有变量值、材质球的颜色参数甚至能直接修改它们来验证修复方案。这不仅仅是效率的提升更是开发流程的一种革新。它让“运行时调试”变得可视化、可交互尤其适合开发带有复杂逻辑、AI行为树、动态生成内容或需要大量数据配置的游戏和应用。2. 核心功能与设计思路拆解UnityRuntimeInspector的设计目标非常明确在运行时复现Unity编辑器Inspector的核心体验。但这并非简单的UI复制其背后需要考虑性能、泛用性、可扩展性以及跨平台兼容性等一系列工程挑战。2.1 运行时反射与动态UI生成插件最核心的技术点在于利用C#的反射Reflection机制。在运行时它通过反射读取任意GameObject上挂载的组件以及组件上所有标记为[System.Serializable]的字段和属性。这个过程与Unity编辑器序列化数据的过程类似但完全发生在运行时环境中。反射虽然强大但性能开销较大尤其是频繁调用时。RuntimeInspector对此做了深度优化缓存机制首次检查一个类型时会缓存其字段、属性、特性等元数据信息避免后续重复反射。按需刷新Inspector面板并非每帧更新而是有一个可配置的“刷新间隔”Refresh Interval。默认每秒刷新4次这对于大多数调试场景已经足够实时同时大幅减少了不必要的计算和GC垃圾回收压力。值类型装箱优化读取值类型如int, float, Vector3的字段必然涉及“装箱”boxing操作这会产生GC Alloc。插件无法避免这一点但通过降低刷新频率和池化UI元素将影响降到最低。2.2 双面板协同Inspector与Hierarchy插件提供了两个核心预制体RuntimeInspector和RuntimeHierarchy。它们既可以独立工作也可以相互关联形成一个完整的调试环境。RuntimeHierarchy负责展示场景中所有的GameObject树状结构。它不仅能显示当前加载的所有Unity场景还支持创建“伪场景”Pseudo-Scenes让你可以自定义分类管理对象比如把所有UI元素归到一个伪场景把所有敌人归到另一个。这对于管理大型或动态生成的场景非常有用。RuntimeInspector负责展示在Hierarchy中选中的对象的具体信息。它可以展示Unity内置类型如Transform, Rigidbody, Material的几乎所有可序列化字段也支持自定义的[System.Serializable]类和结构体甚至支持一维数组和ListT。两者的连接是单向可选的。你可以将Inspector赋值给Hierarchy的Connected Inspector属性这样在Hierarchy中选择不同对象时Inspector会自动切换查看目标。反之你也可以将Hierarchy赋值给Inspector的Connected Hierarchy属性当你在Inspector中高亮一个对象引用字段时Hierarchy中的选中项会同步跳转。这种松耦合设计允许你在一个界面上放置多个独立或关联的面板适应不同的调试布局需求。2.3 可扩展的绘制器Drawer系统这是插件高级功能的基础。并非所有类型都适合用默认的文本框、数字框来显示。比如你想在运行时修改一个Color最好有个颜色选择器修改一个Layer最好有个下拉菜单。RuntimeInspector通过一套“绘制器”Drawer系统来解决这个问题。每个字段类型都对应一个特定的UI绘制器如BoolField用于布尔值ColorField用于颜色。这套系统是开放的允许你为自定义数据类型创建专属的绘制器。有两种主要方式创建绘制器预制体这是最灵活的方式。你可以创建一个继承自InspectorField或ExpandableInspectorField、ObjectReferenceField的MonoBehaviour预制体并将其注册到Inspector的设置Settings中。这种方式适合需要复杂自定义UI的字段比如为一个Matrix4x4类型创建一个4x4的网格输入框。实现IRuntimeInspectorCustomEditor接口这种方式更简单无需创建预制体。你只需创建一个类用[RuntimeInspectorCustomEditor(typeof(YourType))]标记并在GenerateElements方法中描述如何用已有的内置绘制器来组合显示你的类型。例如你可以为Camera组件创建一个自定义编辑器根据“正交投影”复选框的勾选状态动态显示“正交大小”或“视野”字段。3. 安装、配置与基础使用3.1 多种安装方式插件的作者提供了极其友好的多种安装路径这也是其受欢迎的原因之一。UnityPackage导入最直接从GitHub Releases页面下载最新的.unitypackage文件在Unity编辑器中通过Assets - Import Package - Custom Package导入。Git URL推荐用于版本管理在Unity的Package Manager窗口中点击“”号选择“Add package from git URL”然后输入https://github.com/yasirkula/UnityRuntimeInspector.git。这种方式可以方便地更新到特定分支或标签。OpenUPM包管理器用户如果你安装了openupm-cli在项目目录下执行命令openupm add com.yasirkula.runtimeinspector即可。Asset Store支持作者你也可以在Unity Asset Store中搜索“Runtime Inspector Hierarchy”进行购买这是支持作者持续开发的最好方式。注意如果项目使用了Unity的新输入系统New Input System并且在Unity 2019.2.5或更早版本上需要在Player Settings的Scripting Define Symbols中添加ENABLE_INPUT_SYSTEM编译指令。否则输入事件可能无法正确触发。3.2 快速上手创建你的第一个调试面板安装后在项目的Plugins/RuntimeInspector/Prefabs文件夹下可以找到核心预制体。创建Canvas如果你的场景还没有UI Canvas先创建一个。拖入预制体将RuntimeHierarchy和RuntimeInspector预制体拖入Canvas下。建立连接可选在Hierarchy对象的Inspector面板上找到Runtime Hierarchy组件将其Connected Inspector字段拖拽赋值为你刚放入的RuntimeInspector对象。这样在Hierarchy中选择对象时Inspector内容就会自动切换。调整皮肤预制体默认使用一种皮肤。你可以在Skins文件夹下找到LightSkin和DarkSkin两种预设直接拖拽到对应面板的Skin属性上即可切换。你也可以通过Assets/Create/yasirkula/RuntimeInspector/UI Skin创建自定义皮肤调整字体、颜色、间距等。运行测试点击Play你应该能看到Hierarchy面板中列出了场景中的所有对象点击任一对象右侧Inspector面板就会显示其详细信息。你可以尝试修改Transform的位置、物体的名称甚至是一些脚本的公开变量。3.3 关键配置参数解析为了让工具更贴合你的项目了解几个核心配置很重要对于RuntimeInspectorRefresh Interval刷新间隔。增大此值如从0.25改为0.5能显著减少因值类型装箱产生的GC提升性能但会降低数据更新的实时性。在移动端调试时建议适当调高。Expose Fields/Properties控制暴露哪些成员。默认是“Serializable Only”即只显示标记了[SerializeField]或public的字段。如果你需要在运行时查看私有字段进行深度调试可以改为“All”但要小心性能和安全问题。Nest Limit嵌套限制。当检查一个包含自引用如链表节点的对象时这个值决定了递归展开的深度防止无限循环导致崩溃。Settings这里是高级自定义的入口。你可以创建自己的Settings资源来隐藏特定类型的某些变量或为特定类型指定自定义的绘制器。对于RuntimeHierarchyExpose Unity Scenes是否显示Unity场景。如果你只想用伪场景来管理特定对象可以关闭此选项。Can Reorganize Items是否允许重组项。启用后你可以通过拖拽Hierarchy中的对象来改变它们的父子层级关系就像在编辑器里一样。这是一个非常强大的实时编辑功能。Pointer Long Press Action长按操作。设置在移动设备上长按一个对象时的行为比如创建拖拽引用项方便赋值。4. 高级功能与脚本API实战基础使用只是开始RuntimeInspector真正的威力在于其丰富的脚本API允许你以编程方式深度集成调试功能。4.1 动态控制检查与选择你可以在代码中随时改变Inspector正在查看的对象或者控制Hierarchy中的选中项。using UnityEngine; using RuntimeInspectorNamespace; public class DebugController : MonoBehaviour { public RuntimeInspector runtimeInspector; public RuntimeHierarchy runtimeHierarchy; public GameObject targetObject; void Start() { // 让Inspector开始检查targetObject if (runtimeInspector ! null targetObject ! null) { runtimeInspector.Inspect(targetObject); } // 让Hierarchy选中targetObject并滚动到视图中心 if (runtimeHierarchy ! null targetObject ! null) { runtimeHierarchy.Select(targetObject.transform, RuntimeHierarchy.SelectOptions.FocusOnSelection); } } void Update() { // 例如按F1键切换检查玩家对象 if (Input.GetKeyDown(KeyCode.F1)) { GameObject player GameObject.FindGameObjectWithTag(Player); if (player ! null) { runtimeInspector.Inspect(player); } } } }4.2 自定义过滤与拦截你可以在对象被检查或显示前进行过滤实现更精细的控制。void SetupCustomFilters() { // 1. 过滤Hierarchy中显示的对象隐藏所有标签为“Ignore”的对象 runtimeHierarchy.GameObjectFilter (Transform obj) { return !obj.CompareTag(Ignore); }; // 2. 过滤Inspector中显示的组件隐藏所有“MeshCollider”组件 runtimeInspector.ComponentFilter (GameObject gameObject, ListComponent components) { components.RemoveAll(comp comp is MeshCollider); }; // 3. 拦截并改变要检查的对象只允许检查带有Renderer的对象 runtimeInspector.OnInspectedObjectChanging (previousObj, newObj) { GameObject go newObj as GameObject; if (go ! null go.GetComponentRenderer() ! null) { return newObj; // 允许检查 } // 拒绝检查其他类型或无Renderer的对象 return null; }; }4.3 使用伪场景Pseudo-Scenes组织对象伪场景是独立于Unity场景的逻辑分组非常适合用于调试。void ManagePseudoScenes() { // 获取所有敌人和道具 GameObject[] enemies GameObject.FindGameObjectsWithTag(Enemy); GameObject[] items GameObject.FindGameObjectsWithTag(Item); // 将敌人添加到“Enemies”伪场景 runtimeHierarchy.AddToPseudoScene(Enemies, enemies.Select(e e.transform)); // 将道具添加到“Items”伪场景 runtimeHierarchy.AddToPseudoScene(Items, items.Select(i i.transform)); // 也可以手动创建空伪场景 runtimeHierarchy.CreatePseudoScene(DebugTools); // 从伪场景中移除某个对象 runtimeHierarchy.RemoveFromPseudoScene(Enemies, someEnemy.transform, true); // true表示如果场景为空则自动删除 }4.4 为自定义类型添加Inspector按钮通过[RuntimeInspectorButton]特性你可以为自定义组件的Inspector面板添加功能按钮。using UnityEngine; using RuntimeInspectorNamespace; public class MyHealthComponent : MonoBehaviour { [SerializeField] private float currentHealth 100f; [SerializeField] private float maxHealth 100f; // 这个按钮会在Inspector中显示点击后调用HealFull方法 [RuntimeInspectorButton(Heal to Full, false, ButtonVisibility.InitializedObjects)] public void HealFull() { currentHealth maxHealth; Debug.Log(${gameObject.name} healed to full!); } // 这个按钮仅在currentHealth为null时显示对于引用类型这里仅作示例 // [RuntimeInspectorButton(Initialize Health, true, ButtonVisibility.UninitializedObjects)] // public HealthData InitHealth() { return new HealthData(); } }4.5 创建简单的自定义绘制器IRuntimeInspectorCustomEditor假设我们有一个自定义的数据结构CharacterStats我们想在Inspector中用更紧凑的方式显示它。[System.Serializable] public class CharacterStats { public int strength; public int agility; public int intelligence; public float criticalChance; // 0-1 } // 为CharacterStats类型创建自定义编辑器 [RuntimeInspectorCustomEditor(typeof(CharacterStats))] public class CharacterStatsEditor : IRuntimeInspectorCustomEditor { private NumberField strengthField, agilityField, intelField; private NumberField critField; public void GenerateElements(ObjectField parent) { // 获取正在被检查的CharacterStats实例 CharacterStats stats (CharacterStats)parent.Value; // 创建用于显示和编辑strength的字段并自定义显示名 strengthField (NumberField)parent.CreateDrawer( typeof(int), 力量(Str), () stats.strength, // Getter (value) stats.strength (int)value // Setter ); // 设置一些UI属性比如最小值 strengthField.MinValue 1; agilityField (NumberField)parent.CreateDrawer(typeof(int), 敏捷(Agi), () stats.agility, (v) stats.agility (int)v); agilityField.MinValue 1; intelField (NumberField)parent.CreateDrawer(typeof(int), 智力(Int), () stats.intelligence, (v) stats.intelligence (int)v); intelField.MinValue 1; // 创建暴击率字段以百分比形式显示 (0-100) critField (NumberField)parent.CreateDrawer( typeof(float), 暴击率(%), () stats.criticalChance * 100f, // Getter: 将0-1转换为0-100 (value) stats.criticalChance Mathf.Clamp01((float)value / 100f) // Setter: 将输入转换回0-1并限制范围 ); critField.MinValue 0; critField.MaxValue 100; critField.DecimalsToShow 1; // 显示一位小数 } public void Refresh() { // 在这个简单的例子里我们不需要在每次刷新时做特殊处理 // 因为Getter/Setter已经绑定了数据。但如果需要根据一个字段的值动态显示/隐藏另一个字段可以在这里处理。 } public void Cleanup() { // 清理任何非托管资源。在这个例子中没有需要手动清理的。 // 注意不要销毁通过CreateDrawer创建的字段父ObjectField会处理。 } }为了让RuntimeInspector识别这个自定义编辑器你需要在某个脚本的Awake或Start方法中确保在第一次使用Inspector之前注册包含这个编辑器的类型void Start() { RuntimeInspectorUtils.ExposedExtensionMethodsHolder typeof(CharacterStatsEditor); }5. 性能优化与实战避坑指南在实际项目中使用RuntimeInspector尤其是移动端项目性能是需要首要考虑的问题。以下是我在多个项目中总结的经验和避坑点。5.1 GC优化策略运行时反射和UI更新是主要的GC来源。调整刷新频率这是最有效的优化手段。在RuntimeInspector和RuntimeHierarchy组件上将Refresh Interval从默认的0.25秒每秒4次提高到0.5秒甚至1秒。对于变化不频繁的数据这能直接减少一半以上的GC分配。谨慎使用“Expose All”除非深度调试需要否则永远不要将Expose Fields和Expose Properties设置为“All”。这会导致大量私有字段和属性被反射其中很多可能是复杂的引用类型或结构体造成巨大的性能开销和不可预知的副作用。利用Settings隐藏变量对于你确定不需要在运行时查看的组件或变量如UnityEngine内部组件、网络组件等在Inspector的Settings资源中将它们添加到对应类型的Hidden Variables列表或直接使用通配符*隐藏该类型所有变量。这能从根本上减少反射计算量。对象池容量Inspector和Hierarchy内部使用对象池来复用UI元素。在Pool Capacity设置中可以根据你预期同时显示的最大字段数或列表项数来适当增加容量避免运行时动态扩容。5.2 移动端适配与输入处理在手机或平板设备上使用需要特别注意交互体验。长按与拖拽确保RuntimeHierarchy的Pointer Long Press Action设置合理。对于触屏设备推荐设置为Show Multi Selection Toggles Then Create Dragged Reference Item这样长按可以先显示多选复选框再次长按才创建拖拽项操作更清晰。滚动性能如果Hierarchy中对象非常多超过几百个滚动时可能会卡顿。可以考虑分页加载或者使用GameObjectFilter动态过滤只显示当前关心的对象。也可以关闭Show Horizontal Scrollbar以节省少量性能。输入系统兼容性如前所述使用新输入系统时注意添加编译指令。如果遇到点击无反应检查EventSystem是否存在且正常工作。5.3 常见问题排查实录下面是一个我在实际开发中遇到过的典型问题及其解决方案的速查表。问题现象可能原因解决方案Inspector面板一片空白不显示任何内容1. 检查的对象为null。2. 对象的类型没有任何[SerializeField]或public字段且Expose Fields未设置为“All”。3. 该类型的所有变量都被Settings中的Hidden Variables规则隐藏了。1. 确保Inspect()方法传入了一个有效的对象。2. 确认对象有可序列化的字段或调整Expose Fields设置。3. 检查Settings资源中的隐藏规则。修改数值后游戏逻辑没有生效1. 修改的是属性的值但该属性在set访问器中可能没有触发相关逻辑。2. 修改的是结构体struct的字段。RuntimeInspector通过反射修改的是结构体副本的值而非原始值。1. 对于属性确保其set访问器包含了必要的逻辑如事件触发、脏标记设置。2.这是关键坑点避免在运行时直接修改结构体字段。如果必须修改请通过一个方法如SetPosition(Vector3 newPos)来修改并将该方法通过[RuntimeInspectorButton]暴露为按钮。Hierarchy中找不到某个GameObject1. 该对象被GameObjectFilter过滤掉了。2. 该对象被添加到了RuntimeInspectorUtils.IgnoredTransformsInHierarchy这个静态HashSet中。3. 对象处于未激活状态而Hierarchy默认可能不显示未激活对象取决于设置。1. 检查runtimeHierarchy.GameObjectFilter委托。2. 检查代码中是否有地方将对象添加到了忽略列表。3. 确认对象激活状态或检查Hierarchy相关设置。自定义绘制器或按钮不显示1. 包含自定义编辑器或按钮方法的类未正确注册。2. 对于按钮ButtonVisibility设置与对象状态不匹配如对象非空却用了UninitializedObjects。3. 自定义绘制器预制体未正确添加到Inspector的Settings中。1. 对于IRuntimeInspectorCustomEditor确保在访问前通过RuntimeInspectorUtils.ExposedExtensionMethodsHolder注册了类型。2. 检查按钮特性的参数设置。3. 对于预制体绘制器确认已将其添加到对应类型的Standard Drawers或Reference Drawers列表中。在WebGL或移动端打包后功能异常1. 代码剥离Code Stripping可能移除了运行时通过反射访问的类或成员。2. 某些Unity API在特定平台不可用。1. 在Player Settings的Managed Stripping Level中尝试降低级别如从High改为Low或Minimal或使用[Preserve]特性标记必要的类。2. 使用平台依赖编译#if !UNITY_WEBGL ... #endif来包装可能不兼容的代码段。5.4 一个实用的调试管理器设计模式最后分享一个我常用的架构模式将RuntimeInspector集成到项目的调试系统中做到既强大又安全。using UnityEngine; using RuntimeInspectorNamespace; public class DebugManager : MonoBehaviour { public static DebugManager Instance { get; private set; } [Header(UI References)] public GameObject debugPanel; // 包含Inspector和Hierarchy的父物体 public RuntimeInspector inspector; public RuntimeHierarchy hierarchy; [Header(Settings)] public bool enableInDevelopmentBuild true; public KeyCode toggleKey KeyCode.BackQuote; // 通常用“~”键 private bool isDebugPanelActive false; void Awake() { if (Instance ! null Instance ! this) { Destroy(gameObject); return; } Instance this; DontDestroyOnLoad(gameObject); // 根据构建类型决定初始状态 #if DEVELOPMENT_BUILD || UNITY_EDITOR if (enableInDevelopmentBuild) { debugPanel.SetActive(true); isDebugPanelActive true; // 初始隐藏按快捷键显示 debugPanel.SetActive(false); isDebugPanelActive false; } else { debugPanel.SetActive(false); this.enabled false; // 禁用管理器 } #else // 发布版本完全禁用 debugPanel.SetActive(false); this.enabled false; Destroy(gameObject); #endif } void Update() { if (Input.GetKeyDown(toggleKey)) { ToggleDebugPanel(); } } public void ToggleDebugPanel() { isDebugPanelActive !isDebugPanelActive; debugPanel.SetActive(isDebugPanelActive); if (isDebugPanelActive) { // 面板激活时自动检查当前选中的对象 if (hierarchy.CurrentSelection ! null hierarchy.CurrentSelection.Count 0) { inspector.Inspect(hierarchy.CurrentSelection[0].gameObject); } } else { // 面板关闭时停止检查以释放资源 inspector.StopInspect(); } } // 提供一个静态方法供其他脚本快速记录对象到调试面板例如点击屏幕上的物体 public static void InspectObject(GameObject obj) { if (Instance ! null Instance.isDebugPanelActive Instance.inspector ! null) { Instance.inspector.Inspect(obj); // 同时让Hierarchy选中它 if (Instance.hierarchy ! null) { Instance.hierarchy.Select(obj.transform, RuntimeHierarchy.SelectOptions.FocusOnSelection); } } } }这个管理器做了几件关键事情1) 使用单例模式确保全局可访问2) 通过DEVELOPMENT_BUILD宏控制功能仅在开发版本启用发布版本自动移除3) 提供快捷键开关4) 封装了常用的检查逻辑。你可以通过DebugManager.InspectObject(someObj)在任何地方快速调出某个对象的详细信息这对于快速定位线上问题非常有帮助。经过多个项目的实践UnityRuntimeInspector已经从一个小众的调试工具变成了我开发工作流中不可或缺的一环。它节省的时间尤其是在处理那些“只在特定设备上偶现”的诡异Bug时是难以估量的。希望这篇详尽的指南能帮助你把它用好用活真正提升你的开发效率和项目质量。