What is a serializable asset type for folder?

I noticed that I could declare UnityEngine.Object in my ScriptableObject and drag a folder to that field from the Project panel. So I could do AssetDatabase.GetAssetPath on it and do some preprocessing stuff. I copied this approach from SpriteAtlas’s packing box where I can drag a folder to it.

However I can drag anything that has a .meta file on that box. Is there a type other than UnityEngine.Object that allows only folder on it? I have look through the side bar of UnityEngine namespace in the docs and found nothing that is likely to be that. Thanks.

Unfortunately I don’t think so. The way I’ve handle this before is either using UnityEditor.DefaultAsset type (which will still accept some .meta files, but will filter out a lot of them) or custom property drawer where you handle the check for the type. Here’s a quick example of how I’m using it right now:

public class EntityHolder : ScriptableObject
{
    [System.Serializable]
    public struct Foo
    {
        public UnityEngine.Object obj;
    }

    public Foo test;
}

[CustomPropertyDrawer(typeof(EntityHolder.Foo))]
public class IngredientDrawer : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        SerializedProperty value = property.FindPropertyRelative("obj");
        UnityEngine.Object prevValue = value.objectReferenceValue;
        EditorGUI.BeginChangeCheck();
        EditorGUI.PropertyField(position, value, true);

        if (EditorGUI.EndChangeCheck())
        {
            if (value.objectReferenceValue != null && !System.IO.Directory.Exists(AssetDatabase.GetAssetPath(value.objectReferenceValue)))
                value.objectReferenceValue = prevValue;
        }
    }
}

You could probably handle the drawing method with an attribute on the field instead of wrapping it on a struct/class, but for my code that is how I’m doing it right now.

Edit: Forgot to include the null check for setting back none on the field

1 Like

Hello ! I just finished this, I hope it helps :

using System.IO;
using UnityEditor;
using UnityEngine;

namespace EditorUtils
{
    [System.Serializable]
    public class FolderReference
    {
        public string GUID;

        public string Path => AssetDatabase.GUIDToAssetPath(GUID);
    //    public DefaultAsset Asset => AssetDatabase.LoadAssetAtPath<DefaultAsset>(Path);
    }

    [CustomPropertyDrawer(typeof(FolderReference))]
    public class FolderReferencePropertyDrawer : PropertyDrawer
    {
        private bool initialized;
        private SerializedProperty guid;
        private Object obj;

        private void Init(SerializedProperty property)
        {
            initialized = true;
            guid = property.FindPropertyRelative("GUID");
            obj = AssetDatabase.LoadAssetAtPath<Object>(AssetDatabase.GUIDToAssetPath(guid.stringValue));
        }
       
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            if (!initialized) Init(property);

            GUIContent guiContent = EditorGUIUtility.ObjectContent(obj, typeof(DefaultAsset));

            Rect r = EditorGUI.PrefixLabel(position, label);

            Rect textFieldRect = r;
            textFieldRect.width -= 19f;

            GUIStyle textFieldStyle = new GUIStyle("TextField")
            {
                imagePosition = obj ? ImagePosition.ImageLeft : ImagePosition.TextOnly
            };

            if (GUI.Button(textFieldRect, guiContent, textFieldStyle) && obj)
                EditorGUIUtility.PingObject(obj);

            if (textFieldRect.Contains(Event.current.mousePosition))
            {
                if (Event.current.type == EventType.DragUpdated)
                {
                    Object reference = DragAndDrop.objectReferences[0];
                    string path = AssetDatabase.GetAssetPath(reference);
                    DragAndDrop.visualMode = Directory.Exists(path) ? DragAndDropVisualMode.Copy : DragAndDropVisualMode.Rejected;
                    Event.current.Use();
                }
                else if (Event.current.type == EventType.DragPerform)
                {
                    Object reference = DragAndDrop.objectReferences[0];
                    string path = AssetDatabase.GetAssetPath(reference);
                    if (Directory.Exists(path))
                    {
                        obj = reference;
                        guid.stringValue = AssetDatabase.AssetPathToGUID(path);
                    }
                    Event.current.Use();
                }
            }

            Rect objectFieldRect = r;
            objectFieldRect.x = textFieldRect.xMax + 1f;
            objectFieldRect.width = 19f;

            if (GUI.Button(objectFieldRect, "", GUI.skin.GetStyle("IN ObjectField")))
            {
                string path = EditorUtility.OpenFolderPanel("Select a folder", "Assets", "");
                if (path.Contains(Application.dataPath))
                {
                    path = "Assets" + path.Substring(Application.dataPath.Length);
                    obj = AssetDatabase.LoadAssetAtPath(path, typeof(DefaultAsset));
                    guid.stringValue = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(obj));
                }
                else Debug.LogError("The path must be in the Assets folder");
            }
        }
    }
}
8 Likes

Works perfectly thanks

1 Like

For anyone stumbling across this in 2023 and is having the same problems as I did (“Stack Empty” when closing the folder select menu, and/or the value isn’t saved when leaving the inspector that uses the folder reference), I found the answer to the error in this comment:
https://discussions.unity.com/t/691628/5
you should change this (line 75):

            if (GUI.Button(objectFieldRect, "", GUI.skin.GetStyle("IN ObjectField")))
            {
                string path = EditorUtility.OpenFolderPanel("Select a folder", "Assets", "");
                if (path.Contains(Application.dataPath))
                {
                    path = "Assets" + path.Substring(Application.dataPath.Length);
                    obj = AssetDatabase.LoadAssetAtPath(path, typeof(DefaultAsset));
                    guid.stringValue = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(obj));
                }
                else Debug.LogError("The path must be in the Assets folder");
            }

to this:

            if (GUI.Button(objectFieldRect, "", GUI.skin.GetStyle("IN ObjectField")))
            {
                string path = EditorUtility.OpenFolderPanel("Select a folder", "Assets", "");
                if (path.Contains(Application.dataPath))
                {
                    path = "Assets" + path.Substring(Application.dataPath.Length);
                    obj = AssetDatabase.LoadAssetAtPath(path, typeof(DefaultAsset));
                    guid.stringValue = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(obj));
                }
                else Debug.LogError("The path must be in the Assets folder");
                GUIUtility.ExitGUI();
            }

The answer to the second problem (value not being saved) I figured by myself :roll_eyes:
To get rid of the error you need to add “GUIUtility.ExitGUI();” after you get the value from the folder panel to properly close the panel. You might need to find another similar solution if you wish to keep a different GUI window open at the same time, as this line will close every open GUI as far as I can tell.
You also need to add “guid.serializedObject.ApplyModifiedProperties();” after setting guid.stringValue so the property is updated on the source object, since local values on property drawers aren’t saved between uses.
Works perfectly after that. A great help with Addressable folder paths, btw.
I’m using Unity version 2021.3.16f1, but it might be relevant to other versions.

1 Like

This is the modified version of Djyap’s solution that also supports arrays/lists

using System.IO;
using UnityEditor;
using UnityEngine;

namespace EditorUtils
{
    [System.Serializable]
    public class FolderReference
    {
        [SerializeField] private string name;

        public string GUID;

        public string Path
        {
            get => AssetDatabase.GUIDToAssetPath(GUID);
            set => GUID = AssetDatabase.AssetPathToGUID(value);
        }
        public FolderReference(string path) => Path = path;
    }

    [CustomPropertyDrawer(typeof(FolderReference))]
    public class FolderReferencePropertyDrawer : PropertyDrawer
    {
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            var guid = property.FindPropertyRelative("GUID");
            var obj = AssetDatabase.LoadAssetAtPath<Object>(AssetDatabase.GUIDToAssetPath(guid.stringValue));

            GUIContent guiContent = EditorGUIUtility.ObjectContent(obj, typeof(DefaultAsset));

            Rect r = EditorGUI.PrefixLabel(position, label);

            Rect textFieldRect = r;
            textFieldRect.width -= 19f;

            GUIStyle textFieldStyle = new GUIStyle("TextField")
            {
                imagePosition = obj ? ImagePosition.ImageLeft : ImagePosition.TextOnly
            };

            if (GUI.Button(textFieldRect, guiContent, textFieldStyle) && obj)
                EditorGUIUtility.PingObject(obj);

            if (textFieldRect.Contains(Event.current.mousePosition))
            {
                if (Event.current.type == EventType.DragUpdated)
                {
                    Object reference = DragAndDrop.objectReferences[0];
                    string path = AssetDatabase.GetAssetPath(reference);
                    DragAndDrop.visualMode = Directory.Exists(path) ? DragAndDropVisualMode.Copy : DragAndDropVisualMode.Rejected;
                    Event.current.Use();
                }
                else if (Event.current.type == EventType.DragPerform)
                {
                    Object reference = DragAndDrop.objectReferences[0];
                    string path = AssetDatabase.GetAssetPath(reference);
                    if (Directory.Exists(path))
                    {
                        obj = reference;
                        guid.stringValue = AssetDatabase.AssetPathToGUID(path);
                    }
                    Event.current.Use();
                }
            }

            Rect objectFieldRect = r;
            objectFieldRect.x = textFieldRect.xMax + 1f;
            objectFieldRect.width = 19f;

            if (GUI.Button(objectFieldRect, "", GUI.skin.GetStyle("IN ObjectField")))
            {
                string path = EditorUtility.OpenFolderPanel("Select a folder", "Assets", "");
                if (path.Contains(Application.dataPath))
                {
                    path = "Assets" + path.Substring(Application.dataPath.Length);
                    obj = AssetDatabase.LoadAssetAtPath(path, typeof(DefaultAsset));
                    guid.stringValue = AssetDatabase.AssetPathToGUID(AssetDatabase.GetAssetPath(obj));
                }
                else
                    Debug.LogError("The path must be in the Assets folder");
            }
        }
    }
}
3 Likes