2016年1月29日 星期五

Unity: How to Serialize Dictionary

Background

    Change Language version of game is often pain in the ass, since each UI image and Audio clips should also be changed. Somebody use subversion (to treat different language version as different branches) to solve this problem. However, it might be inconvenient if you change language version very often.
    Instead, we use Data Center to keep the reference to each UI assets. This Data Center record a Key-Value-Pair in which the Key is the language-independent name of a certain UI, while the Value is the UI Prefab (which is language-dependent, since different language version can have different assets) of that UI.  In the game, if someone want to use that UI, he could send the key (the language-independent name of that UI) to Data Center, and ask for that Prefab. Finally, you can create different Data Center for different language version, and each Data Center can have the same Key while the relative Value different.
   For example, the following figure shows the Data Center for Chinese version.  The key (left-hand-side) "WarningPagePanel" will be universal for different language version Data Centers (so that you don't need to change the images in each scene when you want to change language version), and the value (right-hand-side) is the Chinese version prefab for "WarningPagePanel" (and for Chinese prefab, we have "_CN" in the suffix).
If we want to show some UI in the game, we can stuff the key to a certain script (we call DynamicLoadableUI) which can take the key to get the UI prefab from UI Data Center when we edit the scene.

   To implement such Data Center, I need a Serializable Dictionary that can stuff key-value in and can serialize its data into disk. However, Unity does not provide such Serializable Dictionary. This is what is article aiming for.

Solution

    At first, here is a solution posted by christophfranke123 .  When I implement the Serializable Dictionary by this method, however, I will get "IndexOutOfRangeException" if there're too many Key-Value-Pairs (13 in our case) and I want to add new pair.  I found this might due to the dictionary provide by C# is not thread safe and Unity may use different thread to modify and display the dictionary.  Thus I modify the SerializableDictionary provide by christophfranke123 as below:
[Serializable]
public abstract class SerializableDictionary : ISerializationCallbackReceiver
{
    private System.Object mutexForDictionary = new System.Object();
    private Dictionary dictionary = new Dictionary();

    [SerializeField] private List listOfKeys = new List();
    [SerializeField] private List listOfValues = new List();

#region Serialization Related
    public void OnBeforeSerialize()
    {
        lock (this.mutexForDictionary)
        {
            this.listOfKeys.Clear();
            this.listOfValues.Clear();

            foreach (KeyValuePair eachPair in this.dictionary)
            {
                this.listOfKeys.Add(eachPair.Key);
                this.listOfValues.Add(eachPair.Value);
            }
        }
    }

    public void OnAfterDeserialize()
    {
        lock (this.mutexForDictionary)
        {

            this.dictionary.Clear();
            checkIfKeyAndValueValid();

            for (int i = 0; i < this.listOfKeys.Count; ++i)
            {
                this.dictionary.Add(this.listOfKeys[i], this.listOfValues[i]);
            }
        }
    }
#endregion

#region Dictionary Interface
    public void Add(K _key, V _value)
    {
        lock (this.mutexForDictionary)
        {
            this.dictionary.Add(_key, _value);
        }
    }
    public V this[K _key]
    {
        get 
        {
            lock (this.mutexForDictionary)
            {
                return this.dictionary[_key];
            }
        }
        set 
        {
            lock (this.mutexForDictionary)
            {
                this.dictionary[_key] = value;
            }
        }
    }
    public void Remove(K _key)
    {
        lock (this.mutexForDictionary)
        {
            this.dictionary.Remove(_key);
        }
    }
    public KeyValuePair ElementAt(int _elementIndex)
    {
        lock (this.mutexForDictionary)
        {
            return this.dictionary.ElementAt(_elementIndex);
        }
    }
    public int Count
    {
        get
        {
            lock (this.mutexForDictionary)
            {
                return this.dictionary.Count;
            }
        }
    }

#endregion

    private void checkIfKeyAndValueValid()
    {
        int numberOfKeys = this.listOfKeys.Count;
    
&nbsp &nbsp &nbsp &nbsp int numberOfValues = this.listOfValues.Count;

        if (numberOfKeys != numberOfValues)
        {
            throw new System.ArgumentException("(nKey, nValue) = ("
                                        + numberOfKeys.ToString() + ", " 
                                        + numberOfValues.ToString() + ") are NOT Equal!");
        }
    }
}//End of class




Note

   If you want to customize the Editor for SerializableDictionary (such as what I did in UI_DataCenter), you should note that if you edit of SerializableDictionary (such as drag a new GameObject to its inspector), Unity will not be aware of that. When you leave the scene, Unity will not warn you to save scene after change. Therefore, you might find nothing is record when you enter that scene in the next time. Also, even if you save scene after you change the SerializableDictionary (by press "Ctrl+S", or by "File->Save Scene"), Unity will not actually save that scene (since it does not notice that something is changed).
   To solve this, you should call "EditorApplication.SaveScene();" in the same way as you call the "EditorUtility.SetDirty(this.target);":
 [CustomEditor(typeof(UI_DataCenter))]
 public class UI_DataCenterEditor : Editor
 {
   public override void OnInspectorGUI()
   {
      GUI.changed = false;

      DisplayEachElementOfSelectedDictionary();
      DisplayNewDictionaryElementCreatePanel();

      if (GUI.changed)
      {
         EditorUtility.SetDirty(this.target);
         EditorApplication.SaveScene();
      }

   }
 }


沒有留言:

張貼留言