9
\$\begingroup\$

I created a Point struct:

[System.Serializable]
public struct Point
{
    public int x;
    public int y;
}

I am trying to create a property drawer for it, so that when it shows in the inspector, it is shown in a Vector2 field. I have the following script in the "Editor" folder:

using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(Point))]
public class PointEditor : PropertyDrawer
{
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        EditorGUI.Vector2Field(position, label, property.vector2Value);
    }
}

It shows up in the inspector the way I want, but the values are locked at 0. Basically, I am trying to access the Point instance from inside the OnGUI method, but this could be the completely wrong approach. How can I make this so that when the values are changed in the inspector, the respective Point is also changed?

\$\endgroup\$

3 Answers 3

15
+25
\$\begingroup\$

The reason that you can't change any of the values is because the Point struct isn't a Vector2 data type. A Vector2 contains 2 floats where Point has 2 ints, meaning that the types are incompatible. The other problem was that you couldn't get the data from the struct in the way that Unity's inspector code wants you too.

Point.cs

[System.Serializable]
public struct Point
{
    public int X;
    public int Y;
}

It took me a while to format it the same way that Vector2, it's a lot of code but is mostly there to get the correct formatting.

PointDrawer.cs:

using UnityEditor;
using UnityEngine;

[CustomPropertyDrawer(typeof(Point))]
public class PointDrawer : PropertyDrawer
{
    SerializedProperty X, Y;
    string name;
    bool cache = false;

    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        if (!cache)
        {
            //get the name before it's gone
            name = property.displayName;

            //get the X and Y values
            property.Next(true);
            X = property.Copy();
            property.Next(true);
            Y = property.Copy();

            cache = true;
        }

        Rect contentPosition = EditorGUI.PrefixLabel(position, new GUIContent(name));

        //Check if there is enough space to put the name on the same line (to save space)
        if (position.height > 16f)
        {
            position.height = 16f;
            EditorGUI.indentLevel += 1;
            contentPosition = EditorGUI.IndentedRect(position);
            contentPosition.y += 18f;
        }

        float half = contentPosition.width / 2;
        GUI.skin.label.padding = new RectOffset(3, 3, 6, 6);

        //show the X and Y from the point
        EditorGUIUtility.labelWidth = 14f;
        contentPosition.width *= 0.5f;
        EditorGUI.indentLevel = 0;

        // Begin/end property & change check make each field
        // behave correctly when multi-object editing.
        EditorGUI.BeginProperty(contentPosition, label, X);
        {
            EditorGUI.BeginChangeCheck();
            int newVal = EditorGUI.IntField(contentPosition, new GUIContent("X"), X.intValue);
            if (EditorGUI.EndChangeCheck())
                X.intValue = newVal;
        }
        EditorGUI.EndProperty();

        contentPosition.x += half;

        EditorGUI.BeginProperty(contentPosition, label, Y);
        {
            EditorGUI.BeginChangeCheck();
            int newVal = EditorGUI.IntField(contentPosition, new GUIContent("Y"), Y.intValue);
            if (EditorGUI.EndChangeCheck())
                Y.intValue = newVal;
        }
        EditorGUI.EndProperty();
    }

    public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    {
        return Screen.width < 333 ? (16f + 18f) : 16f;
    }
}

It looks like this when compared to the Vector2 type (as requested). Just swap the relevant parts of your code with this and it will let you change the values and keep them persistent throughout their use :)

enter image description here

\$\endgroup\$
11
  • 1
    \$\begingroup\$ You can avoid making changes to the Point class by using SerializedProperty.Next(true) to step through the x and y int members directly. \$\endgroup\$
    – DMGregory
    Commented Jun 9, 2016 at 3:29
  • \$\begingroup\$ I tried that but when saving it to a local copy it shadow copies, so you only get a reference, when you then go to the next variable the first one gets changed too. \$\endgroup\$ Commented Jun 9, 2016 at 12:18
  • 1
    \$\begingroup\$ I've verified this works with var childProperty = property.Copy(); chilProperty.Next(true);... then we keep our original property as-is for when we want to write any changes back to it. \$\endgroup\$
    – DMGregory
    Commented Jun 9, 2016 at 12:52
  • \$\begingroup\$ ahh, .copy That would solve the it. I'll change the code around and update the answer \$\endgroup\$ Commented Jun 9, 2016 at 14:01
  • \$\begingroup\$ I didn't find I needed SerializeField on the x & y - in my tests they came through just for being public members of a Serializable struct. Also, in a quick test this doesn't seem to work correctly for multi-object editing at the moment. Throwing an EditorGUI.BeginProperty / BeginChangeCheck around each of the int fields and assigning only if EndChangeCheck returns true seems to fix it. I can add that in an edit if you like. \$\endgroup\$
    – DMGregory
    Commented Jun 10, 2016 at 2:40
2
\$\begingroup\$

I tried using the solution in the accepted answer but was having some issues with it, particularly when it was being used within a list. After messing around with things a bit and trying out different approaches, this is what I ended up settling on. I'm using the same property drawer for several different vector type structures (works well for any class with 2-4 fields each having a 1 character name).

/// <summary>
/// A custom property drawer for vectors type structures.
/// </summary>
/// <seealso cref="UnityEditor.PropertyDrawer" />
[CustomPropertyDrawer(typeof(Vector2i))]
[CustomPropertyDrawer(typeof(Vector2d))]
[CustomPropertyDrawer(typeof(Vector3i))]
[CustomPropertyDrawer(typeof(Vector3d))]
[CustomPropertyDrawer(typeof(Vector4d))]
[CustomPropertyDrawer(typeof(Quaterniond))]
public class VectorPropertyDrawer : PropertyDrawer
{
    /// <summary>
    /// A dictionary lookup of field counts keyed by class type name.
    /// </summary>
    private static Dictionary<string, int> _fieldCounts = new Dictionary<string, int>();

    /// <summary>
    /// Called when the GUI is drawn.
    /// </summary>
    /// <param name="position">Rectangle on the screen to use for the property GUI.</param>
    /// <param name="property">The SerializedProperty to make the custom GUI for.</param>
    /// <param name="label">The label of this property.</param>
    public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    {
        var fieldCount = GetFieldCount(property);

        Rect contentPosition = EditorGUI.PrefixLabel(position, label);

        EditorGUIUtility.labelWidth = 14f;
        float fieldWidth = contentPosition.width / fieldCount;
        bool hideLabels = contentPosition.width < 185;
        contentPosition.width /= fieldCount;

        using (var indent = new EditorGUI.IndentLevelScope(-EditorGUI.indentLevel))
        {
            for (int i = 0; i < fieldCount; i++)
            {
                if (!property.NextVisible(true))
                {
                    break;
                }

                label = EditorGUI.BeginProperty(contentPosition, new GUIContent(property.displayName), property);
                EditorGUI.PropertyField(contentPosition, property, hideLabels ? GUIContent.none : label);
                EditorGUI.EndProperty();

                contentPosition.x += fieldWidth;
            }
        }
    }

    /// <summary>
    /// Gets the field count for the specified property.
    /// </summary>
    /// <param name="property">The property for which to get the field count.</param>
    /// <returns>The field count of the property.</returns>
    private static int GetFieldCount(SerializedProperty property)
    {
        int count;
        if (!_fieldCounts.TryGetValue(property.type, out count))
        {
            var children = property.Copy().GetEnumerator();
            while (children.MoveNext())
            {
                count++;
            }

            _fieldCounts[property.type] = count;
        }

        return count;
    }
}
\$\endgroup\$
0
-2
\$\begingroup\$

Have you tried doing this?

property.vector2Value = EditorGUI.Vector2Field(position, label, property.vector2Value);

Basically, EditorGUI.Vector2Field() is a function that returns a value of the "position". By setting "position" variable to EditorGUI.Vector2Field, you'll be able to change the "position" variable.

Hope this helps!

\$\endgroup\$
2
  • 1
    \$\begingroup\$ What do you mean? Does the script above return a compile error? Also, the first code snippet in your question section, is that all there is to it? Or there is something else? \$\endgroup\$ Commented Jun 6, 2016 at 15:37
  • 1
    \$\begingroup\$ That's all there is to my Point struct. There is no Vector2 value. \$\endgroup\$
    – Evorlor
    Commented Jun 8, 2016 at 12:52

You must log in to answer this question.

Not the answer you're looking for? Browse other questions tagged .