You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Unity中如何在Inspector显示不可变结构体的readonly字段?

解决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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.08 10:27:51