Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. We have updated the language to the Editor Terms based on feedback from our employees and community. Learn more.
    Dismiss Notice

Question Array of custom struct with property drawer only shows element0 repeatedly

Discussion in 'Scripting' started by Tom_Olsen, Oct 18, 2022.

  1. Tom_Olsen

    Tom_Olsen

    Joined:
    Oct 11, 2020
    Posts:
    7
    I have a custom Struct (simplyfied):

    Code (CSharp):
    1. using System;
    2.  
    3. [System.Serializable]
    4. public struct Tensor2x2
    5. {
    6.     public float xx, xy;
    7.     public float yx, yy;
    8.  
    9.     public float this[int indexI, int indexJ]
    10.     {
    11.         get
    12.         {
    13.             switch (2 * (indexI - 1) + (indexJ - 1))
    14.             {
    15.                 case 0: return xx;
    16.                 case 1: return xy;
    17.                 case 2: return yx;
    18.                 case 3: return yy;
    19.                 default:
    20.                     throw new IndexOutOfRangeException("Invalid Tensor2x2 index!");
    21.             }
    22.         }
    23.  
    24.         set
    25.         {
    26.             switch (2 * (indexI - 1) + (indexJ - 1))
    27.             {
    28.                 case 0: xx = value; break;
    29.                 case 1: xy = value; break;
    30.                 case 2: yx = value; break;
    31.                 case 3: yy = value; break;
    32.                 default:
    33.                     throw new IndexOutOfRangeException("Invalid Tensor2x2 index!");
    34.             }
    35.         }
    36.     }
    37.  
    38.     // Setter:
    39.     public Tensor2x2(float value)
    40.     {
    41.         this.xx = value; this.xy = value;
    42.         this.yx = value; this.yy = value;
    43.     }
    44. }
    with a custom property drawer:

    Code (CSharp):
    1. using UnityEditor;
    2. using UnityEngine;
    3.  
    4. [CustomPropertyDrawer(typeof(Tensor2x2))]
    5. public class Tensor2x2Editor : PropertyDrawer
    6. {
    7.     // Properties:
    8.     string propertyName;
    9.     string[,] subPropertyNames = new string[2, 2];
    10.     SerializedProperty[,] subProperties = new SerializedProperty[2, 2];
    11.  
    12.     // Formatting:
    13.     float height = EditorGUIUtility.singleLineHeight; //18f
    14.     float buffer = 2f;
    15.     bool cache = false;
    16.  
    17.     // Set hight of Property Drawer:
    18.     public override float GetPropertyHeight(SerializedProperty property, GUIContent label)
    19.     {
    20.         if (property.isExpanded)
    21.             return 3f * height + 3f * buffer;
    22.         else
    23.             return height;
    24.     }
    25.  
    26.     public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    27.     {
    28.         // Get Tensor2x2 and property name and initial values:
    29.         if (!cache)
    30.         {
    31.             propertyName = property.displayName;
    32.             for (int i = 0; i < 2; i++)
    33.                 for (int j = 0; j < 2; j++)
    34.                 {
    35.                     property.Next(true);
    36.                     subPropertyNames[i, j] = property.name;
    37.                     subProperties[i, j] = property.Copy();
    38.                 }
    39.             cache = true;
    40.         }
    41.  
    42.         // Setup content drawer:
    43.         position.height = height;   // height of input fields
    44.         Rect contentPosition = new Rect(position.x, position.y, position.width, EditorGUIUtility.singleLineHeight);
    45.         float drawerWidth = contentPosition.width / 2;
    46.         EditorGUIUtility.labelWidth = 22f;
    47.         contentPosition.width *= 1f / 2f;
    48.  
    49.         // Add Dropdown arrow:
    50.         property.isExpanded = EditorGUI.Foldout(contentPosition, property.isExpanded, propertyName);
    51.  
    52.         // Draw subProperties:
    53.         if (property.isExpanded)
    54.         {
    55.             contentPosition.y += height;
    56.             float x0 = contentPosition.x;
    57.             for (int i = 0; i < 2; i++)
    58.             {
    59.                 for (int j = 0; j < 2; j++)
    60.                 {
    61.                     EditorGUI.BeginProperty(contentPosition, label, subProperties[i, j]);
    62.                     {
    63.                         EditorGUI.BeginChangeCheck();
    64.                         float newVal = EditorGUI.FloatField(contentPosition, new GUIContent(subPropertyNames[i, j]), subProperties[i, j].floatValue);
    65.                         if (EditorGUI.EndChangeCheck())
    66.                             subProperties[i, j].floatValue = newVal;
    67.                     }
    68.                     EditorGUI.EndProperty();
    69.                     contentPosition.x += drawerWidth;
    70.                 }
    71.                 contentPosition.x = x0;
    72.                 contentPosition.y += contentPosition.height + buffer;
    73.             }
    74.         }
    75.     }
    76. }
    This works totaly fine and gives me an expandable dropdown to reveal the contents of a serialized Tensor2x2, which can be edited as expected:



    However, looking at the array of Tensor2x2 objects you can see that each entry sais "Element 0".
    Editing any of the three "xx" components in the array changes the "xx" components of all three array entries. This means, instead of showing three seperat Tensor2x2 objects only the first is listed, but three times.

    What am I doing wrong?

    PS: Im not good with Editor GUI customisation yet. Any advice is appreciated.
     
    Last edited: Oct 26, 2022
  2. Tom_Olsen

    Tom_Olsen

    Joined:
    Oct 11, 2020
    Posts:
    7
  3. SisusCo

    SisusCo

    Joined:
    Jan 29, 2019
    Posts:
    1,135
    The same PropertyDrawer instance is used for drawing each element in the array, so all the information you're caching is always pulled from the first element and reused when drawing all the other elements as well. At least this is my understanding of how the PropertyDrawer system works under the hood based on my observations over the years.

    There are two possible solutions:
    1. Remove all caching and always refetch things like propertyName and subProperties with each OnGUI call from the SerializedProperty argument.
    2. Modify the caching so that a different state cache is kept separately for each array element.
    You can implement the latter using a Dictionary with an int key and a value that contains all the cached state for a single array element.
    Code (CSharp):
    1. private readonly Dictionary<int, PropertyData> cachedPropertyData = new Dictionary<int, PropertyData>();
    2.  
    3. private class PropertyData
    4. {
    5.     public readonly string name;
    6.     public readonly string[,] subPropertyNames;
    7.     public readonly SerializedProperty[,] subProperties;
    8.  
    9.     public PropertyData(string name, string[,] subPropertyNames, SerializedProperty[,] subProperties)
    10.     {
    11.         this.name = name;
    12.         this.subPropertyNames = subPropertyNames;
    13.         this.subProperties = subProperties;
    14.     }
    15. }
    16.  
    17. public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    18. {
    19.     GetData(out string propertyName, out string[,] subPropertyNames, out SerializedProperty[,] subProperties);
    20.     ...
    21. }
    22.  
    23. private void GetData(out string propertyName, out string[,] subPropertyNames, out SerializedProperty[,] subProperties)
    24. {
    25.     int index = property.GetArrayElementIndex();
    26.     if(cachedPropertyData.TryGetValue(index, out PropertyData propertyData))
    27.     {
    28.         propertyName = propertyData.name;
    29.         subPropertyNames = propertyData.subPropertyNames;
    30.         subProperties = propertyData.subProperties;
    31.         return;
    32.     }
    33.  
    34.     propertyName = property.displayName;
    35.     subPropertyNames = new string[2, 2];
    36.     subProperties = new SerializedProperty[2, 2];
    37.     for(int i = 0; i < 2; i++)
    38.     {
    39.         for(int j = 0; j < 2; j++)
    40.         {
    41.             property.Next(true);
    42.             subPropertyNames[i, j] = property.name;
    43.             subProperties[i, j] = property.Copy();
    44.         }
    45.     }
    46.  
    47.     propertyData = new PropertyData(propertyName, subPropertyNames, subProperties);
    48.     cachedPropertyData.Add(index, state);
    49. }
    You'll also need a little extension method to help you figure out the array element index of a SerializedProperty:
    Code (CSharp):
    1. public static class SerializedPropertyExtensions
    2. {
    3.     public static int GetArrayElementIndex(this SerializedProperty serializedProperty)
    4.     {
    5.         string propertyPath = serializedProperty.propertyPath;
    6.         int i = propertyPath.LastIndexOf('[');
    7.         if(i == -1)
    8.         {
    9.             return -1;
    10.         }
    11.  
    12.         int start = i + 1;
    13.         int end = propertyPath.IndexOf(']', start);
    14.         string indexString = propertyPath.Substring(start, end - start);
    15.         return int.TryParse(indexString, out int index) ? index : -1;
    16.     }
    17. }