Hello,
so I’ve been stuck with some really annoying bug for the last 2 days, helpless!
‘Basically’, what I have is a FSMTrigger
with a List
- The FSMTrigger
is a MonoBehaviour
that gets attached to a gameObject. It has a custom editor to add new states. FSMTriggerState
is also an MB. When I create a new state, I create a new GO as a child to the FSM and attach the state script on it.
Using serialized properties, everything works nice and cozy. I can add a state and then undo, the state gets removed from the list, and the created GO gets destroyed as well (using Undo.RegisterCreatedObjectUndo
)
The problem is when I remove a state - Just like adding a new state adds a new GO and a state to the list of states, removing a state does the opposite, it removes the state GO and removes the state entry for the list of states in the fsm.
Here’s the editor, in a very basic form (showing only what’s related):
[CustomEditor(typeof(Test))]
class TestEditor : Editor
{
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
serializedObject.Update();
var sp_states = serializedObject.FindProperty("states");
if (GUILayout.Button("Add")) {
var state = Utils.CreateGoWithMb<FSMTriggerState>("TEST STATE " + sp_states.arraySize, HideFlags.None, (target as Test).transform);
Undo.RegisterCreatedObjectUndo(state.gameObject, "Created new state");
sp_states.Add(state);
}
for (int i = 0; i < sp_states.arraySize; ) {
EditorGUILayout.BeginHorizontal();
{
var sp_STATE = sp_states.GetAt(i);
GUILayout.Label(sp_STATE.objectReferenceValue.name);
if (GUIHelper.RemoveButton("Remove")) {
Debug.Log("Size before removal: " + sp_states.arraySize);
foreach (SerializedProperty s in sp_states) {
Debug.Log(s.objectReferenceValue.name);
}
Undo.DestroyObjectImmediate(sp_STATE.gameObject());
sp_states.RemoveAt(i);
Debug.Log("Size after removal: " + sp_states.arraySize);
foreach (SerializedProperty s in sp_states) {
Debug.Log(s.objectReferenceValue.name);
}
continue;
}
}
EditorGUILayout.EndHorizontal();
i++;
}
serializedObject.ApplyModifiedProperties();
}
}
If I don’t destroy the state’s GO, everything works fine. The state gets removed from the list, I could undo/redo, etc no problems whatsoever.
However, destroying the state’s GO will bring all sorts of problems. First, notice the two debug logs that’s showing the arraySize before/after the destroy, it will display the same size before and after! And also NullRefs… - I’m pretty sure there’s no problem with the RemoveAt
method (that is, it ‘usually’ works fine), it does a double DeleteElementAtIndex
(I also tried another manual method) - but if you’re doubting it, here it is:
public static void RemoveAt(this SerializedProperty prop, int atIndex)
{
// Credits to Jamora @UA http://answers.unity3d.com/users/103522/Jamora.html
// he was asking me about my remove method (the code commented below, and then said:
/*"
* there is an alternative way, which I'm not sure if is a bug or not...
* doing SerializedProperty.DeleteArrayElementAtIndex to a null value removes the index
* and moves the remaining values down so nothing is lost.
"*/
// This is true only for reference types
var at = prop.GetAt(atIndex);
if (at.IsReferenceType() && at.objectReferenceValue != null)
prop.DeleteArrayElementAtIndex(atIndex);
prop.DeleteArrayElementAtIndex(atIndex);
// This is my old method
//for (int i = atIndex, size = prop.arraySize; i < size - 1; i++) {
// prop.SetObjectValueAt(i, prop.GetObjectValueAt(i + 1));
//}
//prop.arraySize--;
}
Funny thing is though, if I try the other removal method (the one commented), it actually works well - I don’t get a NullRefExc when I destroy the state’s object, and when I undo, I do get the state’s GO back AND the state entry at the list, but guess what? it’s null! the link is broken between the state reference in the list and the actual state MonoBehaviour!
I made a video about this whole thing, but had some very bad luck editing it, tried more than 4 editors. There was a problem along the way for each of them.
So I just uploaded the relevant parts to the problem, 3 parts. [First][1], [second][2], [third][3]. (At first I show that everything works fine if I don’t destroy, the rest parts shows what happens when I do Destroy…)
To recap, what I’m asking for is: If you have a list of MonoBehaviours
referenced by a serialized property, how do you get that to work nicely with undoing such that removing a list element removes the MB GO as well?
I really appreciate any help, thanks very much!
EDIT:
Test.cs
using UnityEngine;
using System.Collections.Generic;
public class Test : MonoBehaviour
{
public List<FSMTriggerState> states = new List<FSMTriggerState>();
}
EDIT:
It seems that the problem doesn’t have to do with SerializedProperties
, I just tried using target and modifying stuff directly and manually registering undo, same result! (as seen in the last video)
for (int i = 0; i < states.Count; ) {
isRemoved = false;
var state = states*;*
-
EditorGUILayout.BeginHorizontal();*
-
{*
-
GUILayout.Label(state.name);*
-
if (GUIHelper.RemoveButton("Remove")) {*
-
Debug.Log("Size before removal: " + states.Count);*
-
foreach (var s in states) {*
-
Debug.Log(s.name);*
-
}*
-
Undo.RecordObject(target, "Removed a state");*
-
Undo.DestroyObjectImmediate(state.gameObject);*
-
states.RemoveAt(i);*
-
Debug.Log("Size after removal: " + states.Count);*
-
foreach (var s in states) {*
-
if (s != null)*
-
Debug.Log(s.name);*
-
}*
-
isRemoved = true;*
-
}*
-
}*
-
EditorGUILayout.EndHorizontal();*
-
if (!isRemoved)*
-
i++;*
-
}*
It seems like it’s taking a Null snapshot of the state or something, not sure…
EDIT:
So with the great help of @whydoidoit (who mentioned the idea of delayCall
), and @immersiveGamer (who mentioned caching the GO to be destroyed) I was able to make progress, I got really close this time… So here’s what’s happening now: when I click on remove, the state element gets removed from the list, and the state’s GO gets destroyed successfully. If I undo now, sometimes everything gets back intact, other times it doesn’t, I don’t know why, I can’t really explain it…
Here’s what I have now:
-
for (int i = 0; i < sp_states.arraySize; ) {*
-
var sp_STATE = sp_states.GetAt(i);*
-
EditorGUILayout.BeginHorizontal();*
-
{*
-
GUILayout.Label(sp_STATE.objectReferenceValue.name);*
-
if (GUIHelper.RemoveButton("state")) {*
-
Debug.Log("Size before removal: " + sp_states.arraySize);*
-
currentGoToDestroy = sp_STATE.gameObject();*
-
remove = () =>*
-
{*
-
Undo.DestroyObjectImmediate(currentGoToDestroy);*
-
EditorApplication.delayCall -= remove;*
-
};*
-
EditorApplication.delayCall += remove;*
-
sp_states.RemoveAt(i);*
-
Debug.Log("Size after removal: " + sp_states.arraySize);*
-
continue;*
-
}*
-
}*
-
EditorGUILayout.EndHorizontal();*
-
i++;*
-
}*
I tried to reverse the order, i.e. call RemoveAt and then Destroy, both orders gave the same results. I tried other combinations, like putting both Destroy and RemoveAt inside the delegate, but that didn’t go too well - same as with Destroying outside, and RemoveAt inside.
[1]: - YouTube
[2]: - YouTube
[3]: - YouTube