- Home /
How can I make a SerializedProperty UnityEvent persist within a CustomInspector editing a ScriptableObject?
Question: With the following CustomInspector setup, the user can edit a UnityEvent. However once the inspected object is deselected in the hierarchy then reselected, the changes made by the user have been saved the ScriptableObject but are reset in the inspector. I need the changes to persist within the inspector.
I am going to use modified "Intractable" code from the comments because I think it best represents my setup:
Intractable.cs:
[Serializable]
public class Interactable : MonoBehaviour
{
[SerializeField]
public InteractableScriptableObject mySO;
}
InteractableScriptableObject:
[Serializable]
public class InteractableScriptableObject : ScriptableObject
{
public List<InteractableCustomType1> customType1List;
}
InteractableCustomType1:
[Serializable]
public class InteractableCustomType1 : IComparable<InteractableCustomType1>
{
public int myInt;
public List<InteractableCustomType2> myCustomType2List = new List<InteractableCustomType2>();
public InteractableCustomType1(int customInt, List<InteractableCustomType2> customType2List)
{
myCustomType2List = customType2List;
myInt = customInt;
}
//Required by IComparable.
public int CompareTo(InteractableCustomType1 other)
{
if (other == null)
{
return 1;
}
return 0;
}
}
InteractableCustomType2:
[Serializable]
public class InteractableCustomType2 : IComparable<InteractableCustomType2>
{
public string myString;
public UnityEvent myUnityEvent;
public InteractableCustomType2(string customString, UnityEvent customEvent)
{
myString = customString;
myUnityEvent = customEvent;
}
//Required by IComparable.
public int CompareTo(InteractableCustomType2 other)
{
if (other == null)
{
return 1;
}
return 0;
}
}
InteractableInspector:
[CustomEditor(typeof(Interactable))]
public class InteractableInspector : Editor
{
public override void OnInspectorGUI()
{
Interactable targetInteractable = (Interactable)target;
if(targetInteractable != null)
{
InteractableScriptableObject SO = targetInteractable.mySO;
if (GUILayout.Button("Create ScriptableObject"))
{
targetInteractable.mySO = CreateInstance<InteractableScriptableObject>();
}
if (SO.customType1List != null)
{
if (GUILayout.Button("Add a customType1"))
{
SO.customType1List.Add(new InteractableCustomType1(0, new List<InteractableCustomType2>()));
}
if (SO.customType1List.Count > 0)
{
for (int x = SO.customType1List.Count - 1; x >= 0; x--)
{
InteractableCustomType1 customType1 = SO.customType1List[x];//So it is easier to type out.
if (GUILayout.Button("Add a customType2"))
{
customType1.myCustomType2List.Add(new InteractableCustomType2("string", null));
}
if (customType1.myCustomType2List.Count > 0)
{
for (int y = customType1.myCustomType2List.Count - 1; y >= 0; y--)
{
SerializedObject serializedObject = new SerializedObject(targetInteractable.mySO);
SerializedProperty myCustomType1 = serializedObject.FindProperty("customType1List");
SerializedProperty customType1Element = myCustomType1.GetArrayElementAtIndex(x);
SerializedProperty targetCustomType2 = customType1Element.FindPropertyRelative("myCustomType2List").GetArrayElementAtIndex(y);
SerializedProperty myUnityEvent = targetCustomType2.FindPropertyRelative("myUnityEvent");
EditorGUILayout.PropertyField(myUnityEvent);
if (GUI.changed == true)
{
serializedObject.ApplyModifiedProperties();
}
}
}
}
}
}
else
{
SO.customType1List = new List<InteractableCustomType1>();
}
}
}
}
The code I'm working with is so complex that I think my previous examples were confusing, from here with the new example; what would I change to make the changes made to the UnityEvent persist so the properties that were applied to the ScriptableObject are displayed again on the UnityEvent when reselected in the hierarchy.
Well, UnityEvents are special classes which can handle and managed a couple of different delegate / method references. However not all of them are "serializable". The class actually differenciates between "PersistentCalls" and "RuntimeCalls". Persistant calls can only reference methods in object derived from UnityEngine.Object What exact changes do you apply to the event property? What UI do you actually use to provide the interface for the user? Just a PropertyField?
What object do you actually edit? (What object is that event property part of?) Is that object an object in the scene or an asset (prefab) in the project view?
We create the SerializableProperty from a property that is in a ScriptableObject. The full "path" would be like this:
-BaseClass
-ScriptableObject (property of the baseClass)
-List (property of the SO)
-List(property of customType1)
-UnityEvent (property of customType2)
That UnityEvent is displayed in a propertyField in OnInspectorGUI() that is drawing a custom inspector for the baseClass, I want the UnityEvent displayed by the propertyField to act like it would as a variable in a monobehaviour on a GameObject.
I just saw you've edited your comment ^^. Is your "customType1" a normal "serializable" class? In that case it should work just fine. Have you tried editing the ScriptableObject directly without your custom inspector? If something can't be serialized (and thus doesn't show up in the inspector) a custom inspector won't help.
Of course if the ScriptableObject is an asset, it can only have methods assigned from other assets. You can't reference an object in a scene from an asset. It only works the other way round. Assets are always available while objects in a scene are not.
@Adam-$$anonymous$$echtley Has answered similar threads regarding this, however that solution didn't work for me. https://answers.unity3d.com/questions/1270631/get-unityevent-reference-from-serializedproperty.html
frustrating that it doesn't link the other half of the username.
Hi! Any chance you can share a snippet of code? In particular, what does the code in your custom editor look like?
I have rephrased my original question and wrote some semi-pseudo code in a similar manner that I have set up. The actually editor script is around 850 lines right now and would be very hard to separate into the parts I need to show since there is so much stuff in between.
Answer by Adam-Mechtley · Mar 21, 2017 at 10:02 PM
Try this instead:
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;
[CustomEditor(typeof(Interactable))]
public class InteractableInspector : Editor
{
SerializedProperty m_InteractableScriptableObjectProp;
SerializedObject m_SerializedInteractableScriptableObject;
void OnEnable()
{
m_InteractableScriptableObjectProp = this.serializedObject.FindProperty("mySO");
if (m_InteractableScriptableObjectProp.objectReferenceValue != null)
{
m_SerializedInteractableScriptableObject =
new SerializedObject(m_InteractableScriptableObjectProp.objectReferenceValue);
}
}
public override void OnInspectorGUI()
{
this.serializedObject.Update();
if (m_InteractableScriptableObjectProp.objectReferenceValue == null)
{
if (GUILayout.Button("Create ScriptableObject"))
{
var newInstance =CreateInstance<InteractableScriptableObject>();
m_InteractableScriptableObjectProp.objectReferenceValue = newInstance;
Undo.RegisterCreatedObjectUndo(newInstance, "Create New ScriptableObject");
this.serializedObject.ApplyModifiedProperties();
m_SerializedInteractableScriptableObject = new SerializedObject(newInstance);
GUIUtility.ExitGUI();
}
}
else
{
m_SerializedInteractableScriptableObject.Update();
var customType1List = m_SerializedInteractableScriptableObject.FindProperty("customType1List");
if (GUILayout.Button("Add a customType1"))
{
++customType1List.arraySize;
customType1List.serializedObject.ApplyModifiedProperties();
customType1List.serializedObject.Update();
GUIUtility.ExitGUI();
}
for (int t1Idx = customType1List.arraySize - 1; t1Idx >= 0; --t1Idx)
{
var t1Element = customType1List.GetArrayElementAtIndex(t1Idx);
var customType2List = t1Element.FindPropertyRelative("myCustomType2List");
if (GUILayout.Button("Add a customType2"))
{
++customType2List.arraySize;
customType2List.serializedObject.ApplyModifiedProperties();
var newElement = customType2List.GetArrayElementAtIndex(customType2List.arraySize - 1);
newElement.FindPropertyRelative("myString").stringValue = "string";
customType2List.serializedObject.ApplyModifiedProperties();
customType2List.serializedObject.Update();
GUIUtility.ExitGUI();
}
for (int t2Idx = customType2List.arraySize - 1; t2Idx >= 0; --t2Idx)
{
var element = customType2List.GetArrayElementAtIndex(t2Idx);
var unityEvent = element.FindPropertyRelative("myUnityEvent");
EditorGUILayout.PropertyField(unityEvent);
}
}
m_SerializedInteractableScriptableObject.ApplyModifiedProperties();
}
}
}
It took a lot of modification to apply this to my own code but it worked in the end.
Answer by Bunny83 · Mar 17, 2017 at 03:09 AM
So if i understand that right you actually use a custom inspector of a different class(probably a MonoBehaviour?) to edit a completely different (referenced) ScriptableObject asset?
Well that's a problem because Unity's "SerializedObject" concept seems to have problems when the object that is altered isn't currently inspected. That's why you need to manually set it dirty by using.
EditorUtility.SetDirty( yourScriptableObject );
after you've applied some changes.
We had quite a few similar questions recently. Even it's a more edge-case problem it would be great when they could fix it.
No, this isn't the case. The customTypes I mentioned have many more variables than just the UnityEvent and I have had no problems changing those other variables and having them persist when the inspector isn't selected or a SO with the same data is used on another GameObject with the same base class. In fact I don't use EditorUtility.SetDirty() anywhere in my inspector and have no problems; I did however add it after I use serializedObject.Apply$$anonymous$$odifiedProperties(); (the serializedObject being the ScriptableObject refrenced on the base class.) and it had no effect.
I want the modified properties applied to the UnityEvent to persist or be able to turn the serializedProperty UnityEvent back into a regular one with the user's edits so I can save it like I do my other variables in the customTypes.
I think what @FirefightGI is trying to say is that he can't get the UnityEvent serialization to act the same in his custom inspector as it does in the default inspector.
Not sure where your problem stems from, this works 100% and never loses data.
[CustomEditor(typeof(Interactable))]
public class InteractableInspector : Editor
{
private Interactable interactable;
public override void OnInspectorGUI()
{
if (interactable == null)
{
interactable = target as Interactable;
}
SerializedObject serializedObject = new UnityEditor.SerializedObject(interactable);
SerializedProperty onInteract = serializedObject.FindProperty("OnInteract");
EditorGUILayout.PropertyField(onInteract);
}
}
is it possible that your inspected class is not set to [Serializable]? Also here is the class being inspected.
using System;
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEngine.Events;
[Serializable]
public class Interactable : $$anonymous$$onoBehaviour {
[SerializeField] public UnityEvent OnInteract;
public string ObjectName;
}
I noticed if an event is empty it deletes it, otherwise works fine. Also some extras that may be useful
serializedObject.Apply$$anonymous$$odifiedProperties();
serializedObject.Update();
Answer by RobAnthem · Mar 17, 2017 at 11:50 PM
After finally realizing what @FirefightGI was experiencing, I too could very easily replicate his problem. There is little-to-no documentation on this specific thing, and I actually found a solution. The problem wasn't solely a call to SetDirty, but the following code PROPERLY serializes the event data. So I do apologize for my initial commented answer about it working, that surely did not work. I even walked the entire event system with serializedProperty.Move(). Even altering the data from the actual input zones didn't help. SO here is a working solution sir.
[CustomEditor(typeof(Interactable))]
public class InteractableInspector : Editor
{
private Interactable interactable;
private SerializedProperty onInteract;
private SerializedObject serialObject;
public override void OnInspectorGUI()
{
if (interactable == null)
{
interactable = target as Interactable;
}
if (serialObject == null)
{
serialObject = new UnityEditor.SerializedObject(interactable);
}
if (onInteract == null)
{
onInteract = serialObject.FindProperty("OnInteract");
}
EditorGUILayout.PropertyField(onInteract);
onInteract.serializedObject.ApplyModifiedProperties();
EditorUtility.SetDirty(interactable);
onInteract.serializedObject.UpdateIfDirtyOrScript();
}
}
The actual problem? Well Unity was not serializing the event handler when the event was altered, only when added. So adding a new event and removing it, would serialize the event handler, but this is of course not optimal. So we have to force it to serialize every Update or it will completely miss changes made to the serialized child properties of each event Call. This may not be the most optimal solution, but it is certainly better than walking the entire SO and checking for changes.
I have adapted this the best I can to my own code, however I'm looping through all the items in a list variable that is in my scriptableObject and drawing UI in the inspector for the types in the list, one of those types is another list and I do the same for that list. That list contains a UnityEvent type I'm trying to draw UI for in the inspector and the only time I can check for an update or a property change is when the loop is looking at the element in the list for the UnityEvent being displayed.
The above code worked for the intractable example, but I think that because of the loops this wont work.
I added a SerializedProperty variable called eventProperty to the custom type that is being iterated through in the second loop.
if that is null then I find the UnityEvent and get it as a property to assign to eventProperty.
Note: if I don't create a new SerializedObject of the ScriptableObject then the SerializedProperty customType1 is null.
if (myCustomType1.myCustomType2List[i].eventProperty == null)
{
SerializedObject serializedObject = new SerializedObject(targetBase.operation);
SerializedProperty customType1 = serializedObject.FindProperty("myCustomType1List");
SerializedProperty customType2Element = customType1.GetArrayElementAtIndex(higherIndex);
SerializedProperty targetCustomType2 = customType2Element.FindPropertyRelative("customType2List").GetArrayElementAtIndex(i);
scorer.scoreConditions[i].eventProperty = targetCustomType2.FindPropertyRelative("customType2UnityEvent");
}
else
{
EditorGUILayout.PropertyField(myCustomType1.myCustomType2List[i].eventProperty);
serializedObject.Apply$$anonymous$$odifiedProperties();
EditorUtility.SetDirty(targetBase.myScriptableObject);
myCustomType1.myCustomType2List[i].eventProperty.serializedObject.UpdateIfDirtyOrScript();
}
//higherIndex being the current index of myCustomType1 being iterated through.
// i being the index of the list being iterated through within myCustomType1
If the loops are not keeping this from being called quick enough maybe I just don't understand your example. =/
Hmm before we proceed, lets clarify. You are iterating the entire object through the SerializedObject? If so, is this for the purpose of a universal custom inspector? If not then the only thing that needs a PropertyField is the event.
However, if you have a List or UnityEvent[] array, then again, I would iterate it differently. For example, after you acquire your element, you can use the GetCount method and iterate through the array by using the SerializedObject.Next() method, and displaying it after each next. Also, as a side note, Event lists aren't really good, as Events should be things you knowingly subscribe to, if all you are doing is running a for loop and doing myEvent[i].Invoke() then it would be no different than invoking a single event with multiple subscribed methods.
Perhaps the best thing for us to do is for you to describe what you would like to happen in the simplest way possible. Obviously this is a complex system you have, and the simplest way is usually the best.
Ok, Lets say I have a monobehaviour that takes a ScriptableObject reference, my ScriptableObject class has a List of a custom type (IComparable Class) that type contains a UnityEvent as one of its variables. (all scripts are marked as serializable)
In OnInspectorGUI() for the base monobehaviour's Editor I iterate through all the items in the ScriptableObject's list and draw UI for each variable in each custom type.
When it comes time to create UI for the UnityEvent variable we have to use PropertyField to display it. To do this we need to:
1) set a SerializedObject to our ScriptableObject.
2) find the List property of the ScriptableObject.
3) find the customType property in that list for the current iteration.
4) findPropertyRelative to get the UnityEvent out of that custom type.
5) then we can put that SerializedProperty into a property field
However the property does not act like it does if it was just a public variable on our ScriptableObject that we used one .FindProperty("myUnityEvent") to find, the data we edit in the inspector IS saved to the scriptableObject but it does not persist if we deselect the target object or create a new GameObject and create a reference to the scriptableObject; the data will look like it did before changes were made.
I have updated by question with a better example of what I'm experiencing relative to your intractable example.
Your answer
Follow this Question
Related Questions
Multiple Cars not working 1 Answer
Distribute terrain in zones 3 Answers
Slider Panel controled by slider 0 Answers
Selecting a newly created empty prefab by an editor script 0 Answers