- Home /
Help with editor serialization
Hello, guys, my problem is that I have a enemies database using a scriptable object. The database is a dictionary and since unity does not serialize dictionaries, then I'm implementing ISerializationCallbackReceiver
to serialize the dictionary, very simple using the exact same method the documentation about this interface tells you and it works....until I close the editor, look:
This is after changing the dictionary values which are split between 2 normal lists to use for serialization.
Here you can see that List<string> ED_Keys
and List<EnemyType> ED_Values
are being populated correctly with the dictionary contents when OnBeforeSerialize()
is called. and yes they are set to [SerializeField]
so they should be serialized so when`OnAfterDeserialize()` is called the dictionary will be populated with its values again.
But once I come back, then List<string> ED_Keys
and List<EnemyType> ED_Values
comeback to their former state and dont serialized.
Any help, please?
Source Code:
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine.Serialization;
using UnityEngine;
using System.Linq;
public class EnemiesDataBase : ScriptableObject, ISerializationCallbackReceiver
{
[SerializeField]
public List<string> ED_Keys = new List<string>();
[SerializeField]
public List<EnemyType> ED_Values = new List<EnemyType>();
private Dictionary<string, EnemyType> enemies = new Dictionary<string, EnemyType>();
/// <summary>
///
/// </summary>
public Dictionary<string, EnemyType> Enemies
{
get { return enemies; }
}
public GameObject RetrieveEnemyReference(string enemyName, int level)
{
var enemy = new EnemyType();
if(enemies.TryGetValue(enemyName, out enemy))
{
for (int i = 0; i < enemy.Levels.Count; i++)
{
if(i == level)
{
return enemy.Levels[i].gameObject;
}
}
}
else
{
return null;
}
return null;
}
public void AddEnemyType(string enemyName)
{
enemies.Add(enemyName, new EnemyType());
}
public void RemoveEnemyType(string name)
{
enemies.Remove(name);
}
public void AddEnemyReference(string name)
{
enemies[name].Levels.Add(null);
}
public void AddEnemyReference(string name, GameObject enemy)
{
enemies[name].Levels.Add(enemy);
}
public void RemoveEnemyReference(string name, int level)
{
enemies[name].Levels.RemoveAt(level);
}
public void OnBeforeSerialize()
{
ED_Keys.Clear();
ED_Values.Clear();
foreach (var item in enemies)
{
ED_Keys.Add(item.Key);
ED_Values.Add(item.Value);
}
}
public void OnAfterDeserialize()
{
enemies.Clear();
for (int i = 0; i < ED_Keys.Count; i++)
{
enemies.Add(ED_Keys[i], ED_Values[i]);
}
}
}
[System.Serializable]
public class EnemyType
{
[SerializeField]
private List<GameObject> levels = new List<GameObject>();
public List<GameObject> Levels
{
get { return levels; }
}
}
Source code for the custom inspector used to add and remove enemies to the dictionary of enemies:
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using System.Linq;
[CustomEditor(typeof(EnemiesDataBase))]
public class Inspector_EnemiesDataBase : Editor
{
EnemiesDataBase dataBaseScript;
int enemiesCount;
Vector2 scrollPos = new Vector2();
List<bool> foldOuts = new List<bool>();
string inputName;
private void OnEnable()
{
dataBaseScript = (EnemiesDataBase)target;
enemiesCount = dataBaseScript.Enemies.Count;
foldOuts = new List<bool>();
}
public override void OnInspectorGUI()
{
serializedObject.UpdateIfRequiredOrScript();
EditorGUILayout.BeginVertical();
EditorGUILayout.BeginHorizontal();
inputName = EditorGUILayout.TextField("New Type Name", inputName);
if (GUILayout.Button("Add", GUILayout.Width(50)))
{
if (dataBaseScript.Enemies.ContainsKey(inputName))
{
if (EditorUtility.DisplayDialog("Duplicate", "An enemy type with the name " + inputName + " already exist.", "Ok"))
{
}
}
else
{
dataBaseScript.AddEnemyType(inputName);
enemiesCount++;
foldOuts.Add(true);
if (dataBaseScript.Enemies[inputName].Levels.Count <= 0)
{
dataBaseScript.AddEnemyReference(inputName);
}
}
}
EditorGUILayout.EndHorizontal();
while (foldOuts.Count < enemiesCount)
{
foldOuts.Add(false);
}
EditorGUILayout.BeginHorizontal();
//Code kept for example on how to get a gui element to the right.
//GUILayout.Space(EditorGUIUtility.currentViewWidth - 40);
if (GUILayout.Button("⤴", GUILayout.Width(20)))
{
for (int i = 0; i < foldOuts.Count; i++)
{
foldOuts[i] = false;
}
}
if (GUILayout.Button("⤵", GUILayout.Width(20)))
{
for (int i = 0; i < foldOuts.Count; i++)
{
foldOuts[i] = true;
}
}
EditorGUILayout.EndHorizontal();
scrollPos = EditorGUILayout.BeginScrollView(scrollPos, "box"/*, GUILayout.Height(200)*/);
if (enemiesCount > 0)
{
int i = 0;
foreach (var key in dataBaseScript.Enemies.Keys.ToList())
{
EditorGUILayout.BeginHorizontal();
if (GUILayout.Button("", "OL Minus", GUILayout.Width(20)))
{
if (EditorUtility.DisplayDialog("Delete Enemy Type", "Are you sure you want to delete this enemy type?", "Delete", "Cancel"))
{
dataBaseScript.RemoveEnemyType(key);
enemiesCount--;
foldOuts.RemoveAt(i);
continue;
}
}
if (dataBaseScript.Enemies.Count > 0)
{
if (dataBaseScript.Enemies[key] != null)
{
GUILayout.Space(10);
foldOuts[i] = EditorGUILayout.Foldout(foldOuts[i], key, true);
if (GUILayout.Button("Add Level", GUILayout.Width(75)))
{
dataBaseScript.AddEnemyReference(key, null);
}
}
}
EditorGUILayout.EndHorizontal();
if (dataBaseScript.Enemies.Count > 0)
{
if (foldOuts[i] && dataBaseScript.Enemies[key] != null)
{
EditorGUILayout.BeginHorizontal();
GUILayout.Space(20);
EditorGUILayout.BeginVertical("box");
if (dataBaseScript.Enemies[key].Levels.Count < 2)
{
EditorGUI.BeginDisabledGroup(true);
GUILayout.Button("", "OL Minus", GUILayout.Width(20));
EditorGUI.EndDisabledGroup();
}
else
{
if (GUILayout.Button("", "OL Minus", GUILayout.Width(20)))
{
dataBaseScript.RemoveEnemyReference(key, dataBaseScript.Enemies[key].Levels.Count - 1);
}
}
for (int a = 0; a < dataBaseScript.Enemies[key].Levels.Count; a++)
{
if (dataBaseScript.Enemies.Count > 0)
{
dataBaseScript.Enemies[key].Levels[a] = (GameObject)EditorGUILayout.ObjectField("Lvl " + (a + 1).ToString(), dataBaseScript.Enemies[key].Levels[a], typeof(GameObject), false);
}
}
EditorGUILayout.EndVertical();
EditorGUILayout.EndHorizontal();
}
}
i++;
}
}
else
{
EditorGUILayout.HelpBox("No enemy types have been added.", MessageType.Warning);
}
EditorGUILayout.EndScrollView();
EditorGUILayout.EndVertical();
serializedObject.ApplyModifiedProperties();
}
[MenuItem("Tools/Enemies DataBase")]
public static void Init()
{
string[] lookFor = new string[] { "Assets/DataBase/Enemies" };
string[] result = AssetDatabase.FindAssets("EnemiesDataBase", lookFor);
if (result == null || result.Length <= 0)
{
ScriptableObjectUtility.CreateAsset<EnemiesDataBase>("Assets/DataBase/Enemies");
}
else
{
Selection.activeObject = AssetDatabase.LoadAssetAtPath<EnemiesDataBase>(AssetDatabase.GUIDToAssetPath(result[0]));
}
}
}
Please go ahead and test the code, but you need to create "Assets/DataBase/Enemies" folder so the database scriptable object is created.
Answer by IgorAherne · Nov 15, 2017 at 11:17 AM
I had issues when adding things through the custom editor, where the target
was not perceived as being modified. You have to explicitly tell unity "hey, i've altered this thing, you better save it soon"
For that, I had to call UnityEditor.EditorUtility.SetDirty(yourMonoBehaviorWithDictionary);
after I've modified the value in dictionary, else serialization functions are not called
you can also wrap it in pre-compile tags, and include it directly into your monobehavior
#if UNITY_EDITOR
using UnityEditor;
#endif
void AddEnemyType(){
#if UNITY_EDITOR
EditorUtility.SetDirty(this); //'this' is anything that inherits from UnityEnine.Object (scriptable obj, monobehaviour, etc)
#endif
}
it's quite cheap to call, because it's basically toggling a flag afaik
Ok, it seems like it worked!
I changed:
public void OnBeforeSerialize()
{
ED_$$anonymous$$eys.Clear();
ED_Values.Clear();
foreach (var item in enemies)
{
ED_$$anonymous$$eys.Add(item.$$anonymous$$ey);
ED_Values.Add(item.Value);
}
}
To:
public void OnBeforeSerialize()
{
ED_$$anonymous$$eys.Clear();
ED_Values.Clear();
foreach (var item in enemies)
{
ED_$$anonymous$$eys.Add(item.$$anonymous$$ey);
ED_Values.Add(item.Value);
}
EditorUtility.SetDirty(this);
}
But I have a complain, the Unity documentation does not mention this at all, look at it by yourself:
https://docs.unity3d.com/ScriptReference/ISerializationCallbackReceiver.html
if you include it directly into components, make sure to warp it in #if tags, like in my example, else unity will throw an error when compiling the actual final game
No, no, no ^^. Do not set your object dirty inside the serialization callback. Unity does call your callback when it's going to serialize your object to disk but also in many other situations. You should mark it dirty when you change something. So this has to be done by your custom editor, not by the serialization callback.
Also read the SetDirty documentation. You should use the Undo system in this case. Just a few hours ago i've written this answer which might be helpful.
Yes, the documentation often is outdated or lacks important information. For example the Editor documentation does show the two different approaches (SerializedObject vs "target") however they are missing the Undo.RecordObject in their "target" example. This was working fine prior Unity 5.3. However now you have to use Undo.RecordObject before you do any changes to your object.
Well, it works now but I will use your tip and refract my code :) thanks a lot.