- Home /
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 [i].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. :)
First thing to note is that you want to top-and-tail your OnInspectorGUI function with code to make it update and save, something like this
SerializedObject serializedObj = new SerializedObject(target);
OnEnable()
{
serializedObj = new SerializedObject(target);
}
public override void OnInspectorGUI()
{
serializedObj.Update();
// do stuff
serializedObj.Apply$$anonymous$$odifiedProperties();
}
First off, thanks for the reply! :D Actually already had Update and Apply$$anonymous$$odifiedProperties in my code; had forgotten to include those in the example I gave (updated that as well). So it's closer to this:
[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 [i].DrawCustomInspector ();
}
serializedObject.Apply$$anonymous$$odifiedProperties ();
}
}
Don't know if there's a benefit to making a new SerializedObject, as the built-in Editor.serializedObject seems to be the same thing? Regardless; the OnInspectorGUI edits a whole bunch of things correctly (that do get saved) such as the ability's name and description. The only thing that doesn't get saved correctly is specifically the new AbilityAction in the list.
Ok, well then the next thing I'd consider is your approach to serialisation of members of a class hierarchy. I'm not sure that what you're trying to do can work. You're serialising using the base class - how can it know what type to deserialise into?
I suspect that there is a solution to that, which would involve doing more than implementing an Inspector.
But I've tended to handle this a different way, which is to make the Action classes also ScriptableObjects. It makes it easy to change the configuration, makes it possible to share Actions between Abilities, and makes the serialisation side of things more straightforward.
When I do this, I often have a toggle in the enclosing type's inspector, that lets me switch between looking at a simple list of the actions (which are just fields referencing the action assets), and a more detailed view in which those actions are displayed in detail allowing me to edit them from there.
Answer by jkpenner · Feb 14, 2020 at 05:52 AM
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.
Answer by Epicepicness · Feb 15, 2020 at 10:11 PM
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)! :)
Your answer
Follow this Question
Related Questions
Scriptable Object's Data Gets Lost After Re-opening Unity!!! 1 Answer
List of ScriptableObjects lost on project reload 1 Answer
ScriptableObject with Custom Editor resetting data in inspector 1 Answer
Unable to serialize my list in a Unity Custom Editor script. 1 Answer
Difference between assigning a value in inspector and with a custom editor script 1 Answer