- Home /
How do I add an undo functionality to my custom editor?
Hello,
I've been trying to add an undo functionality to my custom editor script, but in its current state if I change anything in the editor it's not registering in unity's undo/redo function. What do I have to change to make it work?
using UnityEngine;
using UnityEditor;
[CustomEditor(typeof(SpaceObject))]
public class SpaceObjectEditor : Editor
{
public override void OnInspectorGUI()
{
SpaceObject spaceObject = (SpaceObject)target;
base.OnInspectorGUI(); //Default Kram
EditorGUI.BeginChangeCheck();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel("Position of the Space-Object");
EditorGUIUtility.labelWidth = 10f;
double newX = EditorGUILayout.DoubleField("x: ", spaceObject.origin.x, GUILayout.MaxWidth(100f));
double newY = EditorGUILayout.DoubleField("y: ", spaceObject.origin.y, GUILayout.MaxWidth(100f));
double newZ = EditorGUILayout.DoubleField("z: ", spaceObject.origin.z, GUILayout.MaxWidth(100f));
EditorGUILayout.EndHorizontal();
if (EditorGUI.EndChangeCheck())
{
Undo.RecordObject(spaceObject, "Changed Position in Space");
spaceObject.origin.x = newX;
spaceObject.origin.y = newY;
spaceObject.origin.z = newZ;
}
}
}
Answer by Bunny83 · Oct 17, 2020 at 01:44 PM
Well I'm sure the issue is that you are mixing the new serializedobject stuff (by using base.OnInspectorGUI();
) with the old way using the target property. This most likely will interfer with your undo code afterwards. If you want to implement a proper editor you want to use the SerializedObject / SerializedProperty approach. Though if the type of your origin field is a custom struct, it would make more sense to implement a PropertyDrawer for your type (which also uses the SerializedProperty class). Your approach would display your custom struct twice unless you explicitly use [SerializeField, HideInInspector]
If you really want to roll your own custom inspector for your whole SpaceObject class, use an inspector like this:
[CustomEditor(typeof(SpaceObject))]
public class SpaceObjectEditor : Editor
{
SerializedProperty xProperty;
SerializedProperty yProperty;
SerializedProperty zProperty;
private void OnEnable()
{
xProperty = serializedObject.FindProperty("origin.x");
yProperty = serializedObject.FindProperty("origin.y");
zProperty = serializedObject.FindProperty("origin.z");
}
public override void OnInspectorGUI()
{
base.OnInspectorGUI();
EditorGUI.BeginChangeCheck();
EditorGUILayout.BeginHorizontal();
EditorGUILayout.PrefixLabel("Position of the Space-Object");
EditorGUIUtility.labelWidth = 10f;
xProperty.doubleValue = EditorGUILayout.DoubleField("x: ", xProperty.doubleValue, GUILayout.MaxWidth(100f));
yProperty.doubleValue = EditorGUILayout.DoubleField("y: ", yProperty.doubleValue, GUILayout.MaxWidth(100f));
zProperty.doubleValue = EditorGUILayout.DoubleField("z: ", zProperty.doubleValue, GUILayout.MaxWidth(100f));
EditorGUILayout.EndHorizontal();
if (EditorGUI.EndChangeCheck())
{
serializedObject.ApplyModifiedProperties();
}
}
}
Though as I said it usually makes more sense to create a property drawer for your custom type. For example If you have a struct like this:
[System.Serializable]
public struct DVector3
{
public double x;
public double y;
public double z;
}
With a property drawer like this:
[CustomPropertyDrawer(typeof(DVector3))]
public class DVector3Drawer : PropertyDrawer
{
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return base.GetPropertyHeight(property.FindPropertyRelative("x"), label);
}
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
var xProperty = property.FindPropertyRelative("x");
var yProperty = property.FindPropertyRelative("y");
var zProperty = property.FindPropertyRelative("z");
Rect r = EditorGUI.PrefixLabel(position, label);
float width = r.width / 3;
r.width = width;
EditorGUIUtility.labelWidth = 10f;
EditorGUI.PropertyField(r, xProperty);
r.x += width;
EditorGUI.PropertyField(r, yProperty);
r.x += width;
EditorGUI.PropertyField(r, zProperty);
}
}
You don't have to do anything special. So when you use this type in a class it will show up as specified by the drawer:
public class SpaceObject : MonoBehaviour
{
public DVector3 origin ;
// [ ... ]
}
Here you get multi object editing and undo support out of the box.
Thank you,
your property drawer script works really well. I actually used the hide in inspector option, but this is obviously better.
Anyways, I don't actually understand how it does what it does. Specifically, what does
public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
{
return base.GetPropertyHeight(property.FindPropertyRelative("x"), label);
}
actually do?
I also don't really understand where it gets the X,Y,Z from that it does display. Especially since the variable name are actually in lower case: x,y,z. Or is this something unity does automatically?
Well, by default the "combined" / parent property for the whole struct requires several "lines" of space to display. Since we want to display the struct in a single line, I simply get the property height of one of it's sub properties (which usually occupies a single line). AFAIK a single line is usually just "15f". Even though Unity itself uses hardcoded values like that from time to time I like to keep it more organised.
About the property names, Unity generally has a "beautify" method it applies to all field names it displays in the inspector. This generally converts any "camelCaseText"
to "Camel Case Text"
. So they always start the name with a capital letter and split the string at upper case letters into seperate words. This behaviour has often lead to confusion between users.
The "PropertyField" method actually just takes care of drawing a whole property. It may even call other PropertyDrawers or DecoratorDrawer if necessary. Of course depending on how the drawer is implemented they might not necessarily be compatible with each other.
I forgot to mention that "GetPropertyHeight" is actually called before the property is drawn to deter$$anonymous$$e how much vertical space the property needs. This space could be variable depending on many factors. A common factor is the "expanded" boolean value that every property has. It can be used to expand / collapse the property like the default behaviour for structs and sub fields.
Answer by Omti1990 · Oct 17, 2020 at 01:13 PM
Found the solution myself. The problem was that the struct I was trying to put in the interface wasn't made serializable yet.
I just needed to put a [System.Serializable]
in front of my struct declaration and it worked.
The code above works fine if that is considered.