Unity中如何在Inspector显示不可变结构体的readonly字段?
我明白你现在的困境:Unity的序列化系统对readonly字段不友好,你自己写的DisplayReadOnly属性抽屉没法作用在readonly字段上——核心原因是Unity根本不会为readonly字段生成SerializedProperty,你的抽屉代码连被触发的机会都没有!下面给你分析问题本质,再提供几个实用的解决方案:
问题本质
Unity的序列化规则明确:只有非readonly的public字段,或者带[SerializeField]的非readonly私有字段才会被序列化。readonly字段不管访问权限如何,都会被序列化系统忽略,所以Inspector里默认不会显示它们,你的DisplayReadOnlyAttributeDrawer因为找不到对应的SerializedProperty,自然无法生效。
解决方案1:为自定义结构体写专属PropertyDrawer
如果你只是需要让Wind结构体的readonly字段在Inspector里显示,最优雅的方式是给Wind写一个自定义PropertyDrawer,直接通过反射读取结构体的readonly字段值,绕过Unity的序列化限制:
using UnityEngine; using UnityEditor; using System.Reflection; [CustomPropertyDrawer(typeof(Wind))] public class WindPropertyDrawer : PropertyDrawer { public override void OnGUI(Rect position, SerializedProperty property, GUIContent label) { // 通过反射获取目标对象中的Wind实例 var targetObj = property.serializedObject.targetObject; var fieldInfo = targetObj.GetType().GetField(property.propertyPath, BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); Wind windInstance = (Wind)fieldInfo.GetValue(targetObj); EditorGUI.BeginProperty(position, label, property); // 绘制字段标签 position = EditorGUI.PrefixLabel(position, GUIUtility.GetControlID(FocusType.Passive), label); // 设为只读状态,防止编辑 GUI.enabled = false; // 绘制Speed字段 Rect speedRect = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight); EditorGUI.Vector3Field(speedRect, new GUIContent("Speed [m/s]"), windInstance.Speed); // 绘制Rho字段,加上间距 Rect rhoRect = new Rect(position.x, position.y + EditorGUIUtility.singleLineHeight + EditorGUIUtility.standardVerticalSpacing, position.width, EditorGUIUtility.singleLineHeight); EditorGUI.FloatField(rhoRect, new GUIContent("Air Density [kg/m³]"), windInstance.Rho); GUI.enabled = true; EditorGUI.EndProperty(); } // 计算需要的Inspector高度(两行+间距) public override float GetPropertyHeight(SerializedProperty property, GUIContent label) { return EditorGUIUtility.singleLineHeight * 2 + EditorGUIUtility.standardVerticalSpacing; } }
使用方式:只要你的Wind字段是可序列化的(比如public或者带[SerializeField]),Inspector就会自动用这个Drawer显示其readonly的Speed和Rho字段,完全是只读状态,方便调试。
解决方案2:给目标脚本写自定义Editor
如果你的readonly字段分散在某个MonoBehaviour里,不想为每个结构体/类写Drawer,可以直接给脚本写自定义Editor,直接读取并显示readonly字段:
using UnityEngine; using UnityEditor; [CustomEditor(typeof(YourWindController))] // 替换成你的脚本名 public class YourWindControllerEditor : Editor { public override void OnInspectorGUI() { // 先绘制默认的序列化字段 DrawDefaultInspector(); // 读取目标脚本中的readonly字段 YourWindController targetScript = (YourWindController)target; // 分隔线+标题 EditorGUILayout.Space(); EditorGUILayout.LabelField("Read-Only Wind Data", EditorStyles.boldLabel); // 设为只读 GUI.enabled = false; EditorGUILayout.Vector3Field("Wind Speed [m/s]", targetScript.currentWind.Speed); EditorGUILayout.FloatField("Air Density [kg/m³]", targetScript.currentWind.Rho); GUI.enabled = true; } }
这种方式简单直接,适合单个脚本的场景。
解决方案3:通用readonly字段显示框架
如果多个脚本都需要显示readonly字段,可以写一个通用的Editor基类,自动识别带DisplayReadOnlyAttribute的字段并显示:
首先保留你的DisplayReadOnlyAttribute不变,然后写基类Editor:
using UnityEngine; using UnityEditor; using System.Reflection; public class ReadOnlyFieldEditor : Editor { public override void OnInspectorGUI() { // 先绘制默认序列化字段 DrawDefaultInspector(); // 遍历目标对象所有字段,查找带DisplayReadOnlyAttribute的字段 FieldInfo[] fields = target.GetType().GetFields(BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance); foreach (FieldInfo field in fields) { DisplayReadOnlyAttribute attr = field.GetCustomAttribute<DisplayReadOnlyAttribute>(); if (attr == null) continue; EditorGUILayout.Space(); object fieldValue = field.GetValue(target); string displayText = $"{field.Name}: {fieldValue ?? "null"}"; // 处理空值警告 if (attr.warningIfNull && (fieldValue == null || fieldValue.ToString() == "null")) { displayText += " <-This value should NOT be NULL!"; // 用黄色警告样式显示 Color originalColor = GUI.color; GUI.color = Color.yellow; EditorGUILayout.LabelField(displayText, EditorStyles.whiteLabel); GUI.color = originalColor; } else { EditorGUILayout.LabelField(displayText); } } } }
然后你的脚本Editor只要继承这个基类就行:
[CustomEditor(typeof(YourScript))] public class YourScriptEditor : ReadOnlyFieldEditor { // 无需额外代码,自动处理所有带DisplayReadOnly的字段 }
这样不管是结构体里的readonly字段,还是脚本里的普通readonly字段,只要加了[DisplayReadOnly],都会自动在Inspector里显示。
总结
- 自定义结构体优先用专属PropertyDrawer,复用性强;
- 单个脚本的少量readonly字段用自定义Editor,实现简单;
- 多脚本通用场景用通用Editor基类,减少重复代码。
内容的提问来源于stack exchange,提问作者Menyus777

