6
\$\begingroup\$

I'm trying to build my own little ResourceManager in Unity that will allow me to tag resources (ranging from materials to prefabs) to a label in the editor, and then access them in game.

The ResourceManager itself is really easy to write- it's just a Dictionary. However the Unity Editor doesn't seem to be able to serialize (and thus render) Dictionaries.

To get around this, I created a list of key-value pairs which gets loaded into the dictionary by unity. That way Unity gets its list and I get my Dictionary. However, when I try and run the code shown below, I can only add a single entry to the list. Any idea what I'm doing wrong?

ResourceManager.cs

using UnityEngine;
using System.Collections.Generic;

public class ResourceManager : MonoBehaviour, ISerializationCallbackReceiver {
    private Dictionary<string, Object> resources = new Dictionary<string, Object>();

    #region Unity
    // Unity doesn't know how to deal with maps, so we give it a list to work with
    public List<ResourceEntry> entries;

    // Dictionary -> List
    public void OnBeforeSerialize() {
        entries.Clear();
        foreach(string key in resources.Keys) {
            entries.Add(new ResourceEntry(key, resources[key]));
        }
    }

    // List -> Dictionary
    public void OnAfterDeserialize() {
        resources = new Dictionary<string, Object>();
        foreach(ResourceEntry entry in entries) {
            resources[entry.key] = entry.value;
        }
    }

    [System.Serializable]
    public class ResourceEntry {
        public string key;
        public Object value;

        public ResourceEntry() {}

        public ResourceEntry(string key, Object value) {
            this.key = key;
            this.value = value;
        }
    }
    #endregion

    private static ResourceManager instance;
    public static Object Get(string category, string key) {
        instance = instance ?? GameObject.FindObjectOfType<ResourceManager>();
        if( instance.resources.ContainsKey(key)) {
            return instance.resources[key];
        } else {
            return null;
        }
    }
}

ResourceManagerEditor.cs

using UnityEngine;
using System.Collections.Generic;
using UnityEditor;
using UnityEditorInternal;

[CustomEditor(typeof(ResourceManager))]
public class ResourceManagerEditor : Editor {
    private ReorderableList list;

    public void OnEnable() {
        list = new ReorderableList(serializedObject, serializedObject.FindProperty("entries"), true, true, true, true);
        list.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) => {
            SerializedProperty element = list.serializedProperty.GetArrayElementAtIndex(index);
            rect.y += 2;
            EditorGUI.PropertyField(new Rect(rect.x, rect.y, 60, EditorGUIUtility.singleLineHeight), element.FindPropertyRelative("key"), GUIContent.none);
            EditorGUI.PropertyField(new Rect(rect.x + 70, rect.y, 160, EditorGUIUtility.singleLineHeight), element.FindPropertyRelative("value"), GUIContent.none);
        };
        list.drawHeaderCallback = (Rect rect) => {
            EditorGUI.LabelField(rect, "Resources");
        };
    }

    public override void OnInspectorGUI() {
        serializedObject.Update();
        list.DoLayoutList();
        serializedObject.ApplyModifiedProperties();
    }
}
\$\endgroup\$
  • \$\begingroup\$ list.DoLayoutList();??? \$\endgroup\$ – AgentFire Mar 1 '16 at 16:19
1
\$\begingroup\$

The issue is, that a dictionary cannot contain the same key multiple times, but when Unity creates a new entry of your list, whenever you resize it in the inspector, it will just copy the last item. This means, you are trying to add a key, which already exists to the dictionary. This doesn't work and could throw an exception, but in your case, you were simply replacing the existing first entry with the new one, therefore never growing bigger than one.

public void OnAfterDeserialize()
{
    // No need to create the dictionary again, just clear it out.
    resources.Clear();

    foreach (ResourceEntry entry in entries)
    {
        // This "works" but hides your mistake.
        // If resources[key] doesn't exist or already exists, 
        // this key will simply be created or overriden.
        resources[entry.key] = entry.value;

        // To show the mistake, make this line explicitly throw an error
        // when you try to add a key, that already exists:
        resources.Add(entry.key, entry.value);
    }
}

Now you will notice, that you are trying to add the same key twice, which throws an error. To work around this, you can easily use your existing custom editor and instead of copying the last item, create a new one with a unique key.

Here is the complete working code:

using UnityEngine;
using System.Collections.Generic;
using UnityEditor; // Remember to move editor code into Editor folder or #if UNITY_EDITOR
using UnityEditorInternal;

public class ResourceManager : MonoBehaviour, ISerializationCallbackReceiver
{
    private Dictionary<string, Object> resources = new Dictionary<string, Object>();

    // Hide this, because it will not work correctly without the custom editor.
    [HideInInspector]
    [SerializeField]
    private List<ResourceEntry> entries = new List<ResourceEntry>();

    // Dictionary -> List
    public void OnBeforeSerialize()
    {
        entries.Clear();
        // You can iterate a dictionary like this if you want.
        foreach (KeyValuePair<string, Object> kvp in resources)
        {
            entries.Add(new ResourceEntry(kvp.Key, kvp.Value));
        }
    }

    // List -> Dictionary
    public void OnAfterDeserialize()
    {
        resources.Clear();
        foreach (ResourceEntry entry in entries)
        {
            resources.Add(entry.key, entry.value);
        }
    }

    [System.Serializable]
    public class ResourceEntry
    {
        public string key;
        public Object value;

        public ResourceEntry() { }

        public ResourceEntry(string key, Object value)
        {
            this.key = key;
            this.value = value;
        }
    }
}

[CustomEditor(typeof(ResourceManager))]
public class ResourceManagerEditor : Editor
{
    private ReorderableList list;

    public void OnEnable()
    {
        list = new ReorderableList(serializedObject, serializedObject.FindProperty("entries"), true, true, true, true);
        list.drawElementCallback = (Rect rect, int index, bool isActive, bool isFocused) =>
        {
            SerializedProperty element = list.serializedProperty.GetArrayElementAtIndex(index);
            rect.y += 2;
            EditorGUI.PropertyField(new Rect(rect.x, rect.y, 60, EditorGUIUtility.singleLineHeight), element.FindPropertyRelative("key"), GUIContent.none);
            EditorGUI.PropertyField(new Rect(rect.x + 70, rect.y, 160, EditorGUIUtility.singleLineHeight), element.FindPropertyRelative("value"), GUIContent.none);
        };

        list.onAddCallback = (ReorderableList list) =>
        {
            int index = list.serializedProperty.arraySize;
            list.serializedProperty.arraySize++;
            list.index = index;

            // Important! When adding a new element to the list (and dictionary),
            // the newly created key must be unique.
            SerializedProperty element = list.serializedProperty.GetArrayElementAtIndex(index);
            element.FindPropertyRelative("key").stringValue = System.Guid.NewGuid().ToString(); // Create some kind of unique key, like a GUID or adding a number to the last one.
        };
    }

    public override void OnInspectorGUI()
    {
        serializedObject.Update();
        list.DoLayoutList();
        serializedObject.ApplyModifiedProperties();
    }
}

The only change that matters to your specific question is the "onAddCallback" in the custom editor. There you can change the way Unity creates a new list element and give it a unique key at initialization. Remember, you can still edit the value in the inspector, but should make sure that the user doesn't enter a key, that already exists somewhere else.

| improve this answer | |
\$\endgroup\$
0
\$\begingroup\$

I believe your problem here is that you are clearing out your list in OnBeforeSerialize() and recreating it in onAfterDeserialized(). I think your problem is that you are expecting OnAfterDeserialized() to be called after OnBeforeSerialize() and after you have added a new item to your list in the custom inspector (a reasonable assumption).

However this does not seem to be how Unity works. OnBeforeSerialize() gets called fairly frequently but OnAfterDeserialized() does not always get called afterwards. If you add some debug log messages to both these methods you will be able to see this issue yourself in the editor.

This unfortunately means that you end up clearing out your list in the first method, and then it gets cleared out again before the second method has had a chance to recreate the dictionary from your list.

There is another question posted here discussing this issue and it points to a Unity blog post here discussing the Serialization system in Unity.

Hope that helps

| improve this answer | |
\$\endgroup\$
  • \$\begingroup\$ Just wanted to clear this up: The order of execution is correct and works fine when not overriding the first existing key every time a new key is added. While OnBeforeSerialize is called more often than OnAfterDeserialize, it is still always called in the right order when something changes. \$\endgroup\$ – Xarbrough Jan 10 '17 at 23:21

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Not the answer you're looking for? Browse other questions tagged or ask your own question.