Custom Inspector; adding to list not getting saved

Heya,

I'm trying to add actions to a list on a ScriptableObject, through a custom inspector. The list is of an abstract class (ActionBase), the things I want to add are derived from this class. The only way I found of achieving this is by adding these actions per button in the inspector.

Initially the button adds the action, and it's displayed in the inspector. However, whenever the inspector script changes; Unity's play button is pressed; or I reopen Unity, these actions are lost (all other data gets saved correctly).

I figure this is because of some serialization issue; but this is a topic I’ve never fully managed to wrap my head around, and after a lot of reading and searching I haven’t been able to find a solution that fixes this.

// The Scriptable Object; with a list of actions.
public class Ability : ScriptableObject {
	public List<ActionBase> eventActions = new List<ActionBase> ();
}

// The base action class
[System.Serializable]
public abstract class ActionBase {
	public virtual void DrawCustomInspector () {
		// Some stuff to draw relevant inspector stuff.
	}
}

// One of the derived classes we want to add to the list.
[System.Serializable]
public class ActionDamage : ActionBase {
	public override void DrawCustomInspector () {
		// Additional stuff to draw relevant inspector stuff.
	}
}

The Custom Inspector script: If a button is pressed, add a new Action to the list; then call the draw functions for each action.

[CustomEditor (typeof(Ability))]
public class AbilityCustomInspector : Editor {

    public override void OnInspectorGUI () {
        serializedObject.Update ();
		
		Ability ability = (Ability)target;
		if (GUILayout.Button ("Damage", GUILayout.Width (60))) { 
            ability.abilityActions.Add (new ActionDamage ()); 
        }
        for (int i = 0; i < ability.abilityActions.Count; i++) {
            ability.abilityActions *.DrawCustomInspector ();*

}

  •  serializedObject.ApplyModifiedProperties ();*
    

}
}

Note: In the actual script there’s another step in between; ability has an array of events, these events have a list of actions. These events don’t have a separate inspector class nor function. I don’t expect this is relevant to the issue so I left it out to keep the example simple. Mentioning it here, just in case.


Thank you very much in advance. :slight_smile:

There are a few things that I believe are causing you propblem. From my experience with custom editors, you are mixing two different systems. I believe the serializedObject.ApplyModifiedProperites only apply changes to that serializedObject and not to changes that effect the Editor target value. So you may need something similar to below:

public override void OnInspectorGUI () {
    serializedObject.Update();

    // Find the property off the serializedObject
    var abilities = serializedObject.FindProperty("eventActions");

    if (GUIlayout.Button("Damage")) {
        // Adds a new element at the end of the abilities array
        // this will copy the last element into the new index
        abilities.arraySize++;

        // Gets a serializedProperty for the new element
        var element = abilities.GetArrayElementAtIndex(abilities.arraySize - 1);

        // Assign the objectReference to the ActionDamage
        element.objectReferenceValue = new ActionDamage();
    }

    // Loop through all elements of the list
    for(var i = 0; i < abilities.arraySize; i++) {
        // Gets a serializedProperty for the element at the index
        var element = abilities.GetArrayElementAtIndex(i);

        // Use the serializedProperty and draw default property
        EditorGUILayout.PropertyField(element);
    }

    serializedObject.ApplyModifiedProperties();
}

There is one other change that you will need to make, since unity doesn’t defaultly serialized subclasses. You can make your ActionBase a ScriptableObject and then use the AssetDatabase to combine it with your Ability asset.

[System.Serializable]
public abstract class ActionBase : ScriptableObject {
    // action list
}

Then in the above create damage action button you can add the following

// Create an instance of the scriptableObject action
var asset = ScriptableObject.CreateInstance(typeof(ActionDamage));
// Assign the asset as the element's reference
element.objectReferenceValue = asset;
// Add the new action asset to the current object
AssetDatabase.AddObjectToAsset(serializedObject.targetObject, asset);

// Save the changes to the asset database
AssetDatabase.SaveAssets();
// Refresh to show changes in the editor
AssetDatabase.Refresh();

I hope that all works, I didn’t have unity when writing this up.

Heya,

Thank you very much for the answer. Ended up going with jkpenner's solution, and managed to get it to work. Posting my final result below, so in case someone stumbles upon this in the future they might get some use out it maybe. It's not the most elegant creation ever, but it works (mostly).

[System.Serializable]
public class Ability : ScriptableObject { public ActionBase[] actions; }

[System.Serializable]
public abstract class ActionBase : ScriptableObject { // Some variables }
[System.Serializable]
public class ActionDamage : ActionBase { // Some more variables }

And then the actual Editor script:

[CustomEditor (typeof (Ability))]
public class AbilityCustomInspector : Editor {

	AbilityData ability;

	public void OnEnable () {
		ability = (AbilityData) target;
	}

	public override void OnInspectorGUI () {
		serializedObject.Update ();

		// Get a reference to the actions array.
		SerializedProperty actions = serializedObject.FindProperty ("actions");

		// Way of increasing the size of the array. (arraySize++ caused some index issues between the SerializedProperty and target).
		GUIContent content = new GUIContent ("Number of Actions", "The number of actions related to this event.");
		EditorGUILayout.PropertyField (actions.FindPropertyRelative ("Array.size"), content, GUILayout.Width (200));
		if (ability.actions.Length < actions.arraySize) { // This is needed, because it copys the value of the previous index in the array, and we need it on null.
			for (int i = ability.actions.Length; i < actions.arraySize; i++) {
				actions.GetArrayElementAtIndex (i).objectReferenceValue = null;		 
			}
		}
	
		for (int i = 0; i < ability.actions.Length; i++) {
				SerializedProperty element = actions.GetArrayElementAtIndex (i);

				if (element.objectReferenceValue == null) {
					// If the action in the SerializedProperty array hasn't been set yet, show buttons to make actions of each type. 
					if (GUILayout.Button ("Damage", GUILayout.Width (60))) {
						element.objectReferenceValue = AddNewAsset<ActionDamage> (ref targetAction);
					}
				}
				else {
					// Otherwise simply show the property in the inspector normally.
					EditorGUILayout.PropertyField (element);
				}
			}
		serializedObject.ApplyModifiedProperties ();
	}

	// This creates a new asset of the given type, and returns the created asset so it can be set in the current array.
	private Object AddNewAsset<T> (ref ActionBase targetAction) where T : ActionBase {
		ScriptableObject asset = ScriptableObject.CreateInstance (typeof (T));
		targetAction = (T) asset;

		AssetDatabase.AddObjectToAsset (targetAction, ability);
		AssetDatabase.ImportAsset (AssetDatabase.GetAssetPath (targetAction));

		return asset;
	}
}

Thanks again for the help (you as well, Bonfire-Boy)! :slight_smile: