Hi,
Suppose I declare an array variable for a scriptable object, and save it. Then I change my mind about the array - maybe it is the scope or the name - so I update the script and save changes. However, this change is not implemented in the editor, the original declaration is retained.
I have fixed this problem before i.e. make Unity “forget” the original array variable and load the new one. But I have forgotten how I fixed it. Now I have an example, it is a bit more complicated than my previous situations, hope you can please help me.
Example
This is a Register class, it has a dictionary variable and I’m trying to serialise it by using lists. The problem code is these two lines specifically.
public List _keys = new List();
public List _values = new List();
The problem is that I originally declared these two lists private, hence Unity would not serialise. I declared them as public, but Unity refuses to forget their original scope.
FYI I also write a class RegisterEditor, it is used to edit the Register. I do not paste here because it’s not relevant to the issue. Flag class contains two variables: string description and int quantity.
This is an adaptation of the Adventure Game Tutorial AllConditions scriptable object and editor, I wanted to use my own nomenclature and use dictionary instead of arrays to store and retrieve stuff.
using UnityEngine;
using System.Collections.Generic;
// Game flag saves the current quantity of each condition/item.
public class Register : ResettableScriptableObject {
public Dictionary<string, Flag> flagDict;
// This is the problem code
public List<string> _keys = new List<string>();
public List<Flag> _values = new List<Flag>();
public const int levelQuantity = 0;
public const string levelDescription = "Level";
// Static singleton
private static Register instance;
private const string loadPath = "Register";
public static Register Instance {
get {
if (!instance)
instance = FindObjectOfType<Register>();
if (!instance)
instance = Resources.Load<Register>(loadPath);
if (!instance)
Debug.Log("Register has not been created yet.");
return instance;
}
set { instance = value; }
}
public void OnBeforeSerialize() {
//Debug.Log("on before serialise");
_keys.Clear();
_values.Clear();
foreach (KeyValuePair<string, Flag> kvp in flagDict) {
_keys.Add(kvp.Key);
_values.Add(kvp.Value);
}
}
public void OnAfterDeserialize() {
//Debug.Log("on after serialise");
flagDict = new Dictionary<string, Flag>();
for (int i = 0; i < System.Math.Min(_keys.Count, _values.Count); i++)
flagDict.Add(_keys[i], _values[i]);
}
// This function will be called at Start once per run of the game. Resets all condition quantities to zero.
public override void Reset() {
flagDict = new Dictionary<string, Flag>();
}
public override void AddDefaults() {
if (!flagDict.ContainsKey(levelDescription))
flagDict.Add(levelDescription, Flag.CreateFlag(levelDescription, levelQuantity));
}
// flag is satisfied only if there is exact matching quantity.
public static bool HasMatchingFlag(Flag flag) {
Flag savedFlag;
Instance.flagDict.TryGetValue(flag.description, out savedFlag);
if (savedFlag == null) return false;
return savedFlag.quantity == flag.quantity;
}
// Updates the flag by a certain quantity
public bool AddQuantity(string description, int quantity) {
Flag savedFlag;
Instance.flagDict.TryGetValue(description, out savedFlag);
if (savedFlag == null) return false;
savedFlag.quantity += quantity;
return true;
}
// Updates the flag by a certain quantity
public bool SetQuantity(string description, int quantity) {
Flag savedFlag;
Instance.flagDict.TryGetValue(description, out savedFlag);
if (savedFlag == null) return false;
savedFlag.quantity = quantity;
return true;
}
// add some helper functions for the level flag
}
using UnityEditor;
using UnityEngine;
using System.Collections.Generic;
[CustomEditor(typeof(Register))]
public class RegisterEditor : Editor {
// These are the descriptions of the Flags. This is used for the Popups on the ConditionEditor.
public static string[] FlagDescriptions {
get {
if (flagDescriptions == null) UpdateFlagDescriptions();
return flagDescriptions;
}
private set { flagDescriptions = value; }
}
private static string[] flagDescriptions;
private FlagEditor[] flagEditors;
private Register register; // This is the editor target, cast to Register type
private string newFlagDescription = "New Flag";
private int newFlagQuantity = 0;
private const string creationPath = "Assets/Resources/Register.asset";
private const float addButtonWidth = 30f;
public string levelDescription = "Level";
public int levelQuantity = 0;
// Set up game flag conditions array and subeditors. Called whenever you click on Register asset.
private void OnEnable() {
register = (Register) target;
if (Register.Instance.flagDict == null)
Register.Instance.flagDict = new Dictionary<string, Flag>();
if (flagEditors == null) {
CreateEditors();
}
}
// Destroy subeditors. Called when you click away from Register asset.
private void OnDisable() {
for (int i = 0; i < flagEditors.Length; i++) {
DestroyImmediate(flagEditors[i]);
}
flagEditors = null;
}
// Update cache of condition descriptions.
public static void UpdateFlagDescriptions() {
FlagDescriptions = new string[TryGetFlagsLength()];
int i = 0;
foreach (string flag in Register.Instance.flagDict.Keys) {
FlagDescriptions[i] = flag;
i++;
}
}
// Draws the editor for Register asset.
// Lists all conditions and their editors via CreateEditors() call.
// Draws a + button for adding a new condition.
public override void OnInspectorGUI() {
if (flagEditors.Length != TryGetFlagsLength ()) {
for (int i = 0; i < flagEditors.Length; i++) {
DestroyImmediate(flagEditors[i]);
}
CreateEditors ();
}
for (int i = 0; i < flagEditors.Length; i++) {
flagEditors[i].OnInspectorGUI ();
}
if (TryGetFlagsLength () > 0) {
EditorGUILayout.Space ();
EditorGUILayout.Space ();
}
float width = EditorGUIUtility.currentViewWidth / 3f;
EditorGUILayout.BeginHorizontal ();
EditorGUI.indentLevel++;
EditorGUILayout.LabelField("Flag", GUILayout.Width(width));
EditorGUILayout.LabelField("Quantity", GUILayout.Width(width));
EditorGUILayout.EndHorizontal();
EditorGUILayout.BeginHorizontal();
newFlagDescription = EditorGUILayout.TextField (GUIContent.none, newFlagDescription, GUILayout.Width(width));
newFlagQuantity = EditorGUILayout.IntField(GUIContent.none, newFlagQuantity, GUILayout.Width(width));
if (GUILayout.Button ("+", GUILayout.Width (addButtonWidth)))
{
AddFlag(newFlagDescription, newFlagQuantity);
newFlagDescription = "New Flag";
newFlagQuantity = 0;
}
EditorGUI.indentLevel--;
EditorGUILayout.EndHorizontal ();
}
// Creates a Condition Editor for each condition, of editor type = Game Flag
// This condition editor is empty, and a subsequent call to ConditionEditor.OnInspectorGUI() for the RegisterEditor type will populate it with the edit fields.
private void CreateEditors () {
int length = TryGetFlagsLength();
flagEditors = new FlagEditor[length];
int i = 0;
foreach (KeyValuePair<string, Flag> entry in Register.Instance.flagDict) {
flagEditors[i] = CreateEditor(entry.Value) as FlagEditor;
flagEditors[i].editorType = ConditionEditor.EditorType.Group;
i++;
}
}
// Call this function when the menu item is selected.
[MenuItem("Assets/Create/Register")]
private static void CreateRegisterAsset() {
if (Register.Instance) return;
Register newInstance = CreateInstance<Register>();
AssetDatabase.CreateAsset(newInstance, creationPath);
Register.Instance = newInstance;
AddFlag(Register.levelDescription, Register.levelQuantity);
EditorUtility.SetDirty(Inventory.Instance);
UpdateFlagDescriptions();
}
// Define a condition (these definitions are shown as children of Register asset)
// and attach a copy (with zero quantity) to Register asset.
public static void AddFlag(string description, int quantity) {
if (!Register.Instance) {
Debug.LogError("Register asset has not been created yet.");
return;
}
if (Register.Instance.flagDict.ContainsKey(description)) {
Debug.Log("Register already contains flag: " + description + ". Flag not added.");
return;
}
Flag newFlag = FlagEditor.CreateFlag(description, quantity);
Undo.RecordObject(newFlag, "Created new Flag");
AssetDatabase.AddObjectToAsset(newFlag, Register.Instance);
AssetDatabase.ImportAsset(AssetDatabase.GetAssetPath(newFlag));
Register.Instance.flagDict.Add(newFlag.description, newFlag);
EditorUtility.SetDirty(Register.Instance);
UpdateFlagDescriptions ();
}
// Remove condition from game flag asset and destroy it.
public static void RemoveFlag(Flag flag) {
if (!Register.Instance) {
Debug.LogError("Register asset has not been created yet.");
return;
}
if (flag.description == Register.levelDescription) {
Debug.Log("Cannot remove Level flag");
return;
}
Undo.RecordObject(Register.Instance, "Removing flag");
Register.Instance.flagDict.Remove(flag.description);
DestroyImmediate(flag, true);
AssetDatabase.SaveAssets();
EditorUtility.SetDirty(Register.Instance);
UpdateFlagDescriptions ();
}
// Register.Instance.conditions[i]
public static Flag TryGetFlagAt (string description) {
Flag savedFlag;
Register.Instance.flagDict.TryGetValue(description, out savedFlag);
return savedFlag;
}
// Register.Instance.conditions.Length
public static int TryGetFlagsLength () {
if (Register.Instance == null || Register.Instance.flagDict == null) return 0;
return Register.Instance.flagDict.Count;
}
public static int TryGetFlagDescriptionIndex(Flag flag) {
for (int i = 0; i < FlagDescriptions.Length; i++) {
if (FlagDescriptions[i] == flag.description) return i;
}
return -1;
}
}
using UnityEngine;
public class Flag : Condition {
public static Flag CreateFlag(string description, int quantity) {
Flag flag = new Flag();
flag.description = description;
flag.quantity = quantity;
return flag;
}
}
using UnityEngine;
public abstract class Condition : ScriptableObject {
public string description; // Enables player to recognise item, but internally is not used
public int quantity; // This is the quantity saved in GameState. Do not use this for changing quantity - that should be passed as separate parameter.
}