Search code examples
c#unity-game-engineunity3d-2dtoolsunity-editor

How to set SerializedProperty.propertyType in a CustomPropertyDrawer


I am using these two libraries:

Basically, the serializable dictionary looks at the propertyType to determine if the property can be expanded or not, with the following check:

static bool CanPropertyBeExpanded(SerializedProperty property)
{
    switch(property.propertyType)
    {
    case SerializedPropertyType.Generic:
    case SerializedPropertyType.Vector4:
    case SerializedPropertyType.Quaternion:
        return true;
    default:
        return false;
    }
}

However, it appears that Scene Reference is registered as an expandable property even though it isn't. This is because -apparently- Unity registers it as of type Generic.

I can solve this simply by setting the SerializedProperty.propertyType to a more meaningful type, but it is read-only.

So, how can I set the SerializedProperty.propertyType of a custom property drawer?


Solution

  • It isn't documented but the type Generic is automatically assigned for any custom class (like e.g. SceneReference). You can NOT change the propertyType since it is read-only ... and you can't tell the compiler to handle your custom class as something else ...

    Even if you could ... what would be a "more meaningful type"? The available types for SerializedPropertyType are limited and none of them is more meaningful for a custom class.


    The main "issue" here is:

    The SerializableDictionary drawer simply assumes that usually if you have a custom (Generic) class without a custom PropertyDrawer - so using the default drawer - it behaves exactly like the default drawer of Quaternion or Vector4:

    enter image description here

    • It has a label and foldout in the first line
    • fields/content are/is drawn below and only if the property is folded out

    Since the drawer for SceneReference doesn't implement this behavior it is drawn on top of the dictionary's key-field.


    So as simplest fix of course you can simply remove the

    case SerializedPropertyType.Generic:
    

    so the SceneAsset (and all other custom classes) is treated like a normal unfolded field - a matter of taste

    enter image description here


    Alternatively what you could do about it is change the PropertyDrawer of SceneReference to reflect the behavior of e.g. Quaternion:

    • Add an EditorGUI.Foldout with the label which changes the property.isExpaned value
    • Move any content one line below (and optionally intended)
    • Add one line to the property height and a condition for if(!property.isExpanded)

    Might look e.g. like:

    // Made these two const btw
    private const float PAD_SIZE = 2f;
    private const float FOOTER_HEIGHT = 10f;
    
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        // Move this up
        EditorGUI.BeginProperty(position, GUIContent.none, property);
        {
            // Here we add the foldout using a single line height, the label and change
            // the value of property.isExpanded
            property.isExpanded = EditorGUI.Foldout(new Rect(position.x, position.y, position.width, lineHeight), property.isExpanded, label);
    
            // Now you want to draw the content only if you unfold this property
            if (property.isExpanded)
            {
                // Optional: Indent the content
                //EditorGUI.indentLevel++;
                //{
    
                // reduce the height by one line and move the content one line below
                position.height -= lineHeight;
                position.y += lineHeight;
    
                var sceneAssetProperty = GetSceneAssetProperty(property);
    
                // Draw the Box Background
                position.height -= FOOTER_HEIGHT;
                GUI.Box(EditorGUI.IndentedRect(position), GUIContent.none, EditorStyles.helpBox);
                position = boxPadding.Remove(position);
                position.height = lineHeight;
    
                // Draw the main Object field
                label.tooltip = "The actual Scene Asset reference.\nOn serialize this is also stored as the asset's path.";
    
    
                var sceneControlID = GUIUtility.GetControlID(FocusType.Passive);
                EditorGUI.BeginChangeCheck();
                {
                    // removed the label here since we already have it in the foldout before
                    sceneAssetProperty.objectReferenceValue = EditorGUI.ObjectField(position, sceneAssetProperty.objectReferenceValue, typeof(SceneAsset), false);
                }
                var buildScene = BuildUtils.GetBuildScene(sceneAssetProperty.objectReferenceValue);
                if (EditorGUI.EndChangeCheck())
                {
                    // If no valid scene asset was selected, reset the stored path accordingly
                    if (buildScene.scene == null) GetScenePathProperty(property).stringValue = string.Empty;
                }
    
                position.y += paddedLine;
    
                if (!buildScene.assetGUID.Empty())
                {
                    // Draw the Build Settings Info of the selected Scene
                    DrawSceneInfoGUI(position, buildScene, sceneControlID + 1);
                }
    
                // Optional: If enabled before reset the indentlevel
                //}
                //EditorGUI.indentLevel--;
            }
        }
        EditorGUI.EndProperty();
    }
    
    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        var sceneAssetProperty = GetSceneAssetProperty(property);
        // Add an additional line and check if property.isExpanded
        var lines = property.isExpanded ? sceneAssetProperty.objectReferenceValue != null ? 3 : 2 : 1;
        // If this oneliner is confusing you - it does the same as
        //var line = 3; // Fully expanded and with info
        //if(sceneAssetProperty.objectReferenceValue == null) line = 2;
        //if(!property.isExpanded) line = 1;
    
        return boxPadding.vertical + lineHeight * lines + PAD_SIZE * (lines - 1) + FOOTER_HEIGHT;
    }
    

    Now it looks like e.g.

    [Serializable]
    public class TestDict : SerializableDictionary<string, SceneReference> { }
    
    public class Example : MonoBehaviour
    {
        public SceneReference NormalReference;
    
        public TestDict DictExample = new TestDict();
    }
    

    enter image description here