- Home /
 
Finding property with serializedObject on script with a generic
Context
I am working on a tower defense game where towers and enemies have types and elements. The script consists of an array with a type or element, where each one has another array that contains the versus type or element and a multiplier value. So it's basically a 2D array. I wanted to be a bit fancy and also try some stuff out so I decided to make a custom editor for it. Because this is basically a matrix where one of the items is compared to the other I wanted to make it look like the Layer Collision Matrix, found under Project Settings > Physics. Here is what I got:
What I have
After finishing the custom editor I quickly ran into some problems namely, whenever I go to another scene or close unity the data is not serialized. First I tried working around this by saving the matrix to a JSON file and loading it back in when I need it. This works fine when I play from the editor and if I make the script load the JSON file in the Start method is works when building as well. But ultimately I would like the script to just remember the values of the fields as I edit them. I looked up how to do this and I found out I have to use serializedObject to get the property I want and then apply the changed properties at the end. I tried doing this multiple times but couldn't manage to get it to work, serializedObject would never actually find the property. I thought it may have been a bug with my Unity version so I decided to quickly make a similar setup in other versions, but I found the actual problem instead. When the script that contains the array has a generic that needs to be supplied serializedObject returns null everytime. When I tried without the generic I got back the property no problem. 
Problem
Now the issue is that I put that generic there so I could make a base class instead of two very similar classes. The classes for the types matrix and elements matrix are indectical except for the enum they use. Now, my question is: there is any way I can get the property mainArray in my case, from this base class that has the generic? If this is not possible, is there a way I can remove the generic and keep the base class?
 
If there isn't another way about this I will just have to deal with the way I have it set up now I guess. If you got all the way here I would like to thank you for at least taking the time to read this post.
Scripts
Generics meaning:
 1. DM: Damage matrix implementation script.
 2. TE: "Type" or "Element" enum from the TypesAndElements script. 
 
Matrix Editor Base
 /// <summary>
 /// Base class for the custom editor of the damage matrices.
 /// </summary>
 public class DamageMatrixBaseEditor<TE, DM> : Editor where TE : Enum where DM : DamageMatrixBase<TE>
 {
     private int enumLength = Enum.GetNames(typeof(TE)).Length;
     private DM myTarget;
 
     private void OnEnable()
     {
         // The target will be a DamageMatrix class (ie. DamageMatrixTypes or DamageMatrixElements).
         myTarget = target as DM;
     }
 
     public override void OnInspectorGUI()
     {
         #region Styling
         // Styling for the different matrix elements.
         GUIStyle table = new GUIStyle("box")
         {
             stretchHeight = true,
             stretchWidth = true,
             padding = new RectOffset(0, 0, 0, 0)
         };
 
         GUIStyle cornerLabel = new GUIStyle("box")
         {
             fixedHeight = 20,
             fixedWidth = (EditorGUIUtility.currentViewWidth - 25) / (enumLength + 1),
             alignment = TextAnchor.LowerRight,
             margin = new RectOffset(0, 0, 0, 0)
         };
 
         GUIStyle colLabel = new GUIStyle("box")
         {
             fixedHeight = 20,
             fixedWidth = (EditorGUIUtility.currentViewWidth - 25) / (enumLength + 1),
             alignment = TextAnchor.LowerCenter,
             margin = new RectOffset(0, 0, 0, 0)
         };
 
         GUIStyle rowLabel = new GUIStyle("box")
         {
             fixedHeight = 20,
             fixedWidth = (EditorGUIUtility.currentViewWidth - 25) / (enumLength + 1),
             alignment = TextAnchor.MiddleRight,
             margin = new RectOffset(0, 0, 0, 0)
         };
 
         GUIStyle floatStyle = new GUIStyle("box")
         {
             fixedHeight = 20,
             fixedWidth = (EditorGUIUtility.currentViewWidth - 25) / (enumLength + 1),
             alignment = TextAnchor.MiddleCenter,
             margin = new RectOffset(0, 0, 0, 0)
         };
         #endregion
 
         #region Matrix visualization
         EditorGUILayout.BeginVertical(table);
 
         // First for-loop is for the length of the table.
         for (int y = -1; y < enumLength; y++)
         {
             EditorGUILayout.BeginHorizontal();
 
             // Second for-loop is for the width of the table.
             for (int x = -1; x < enumLength; x++)
             {
                 // Top-Left corner label should be empty.
                 if (y == -1 && x == -1)
                 {
                     EditorGUILayout.LabelField("", cornerLabel);
                 }
                 // When the y is increasing so we are going down (row labels).
                 else if (y > -1 && x == -1)
                 {
                     EditorGUILayout.LabelField(Enum.GetName(typeof(TE), y), rowLabel);
                 }
                 // When the x is increasing so we are going right (column labels).
                 else if (y == -1 && x > -1)
                 {
                     EditorGUILayout.LabelField(Enum.GetName(typeof(TE), x), colLabel);
                 }
                 // When both the x and y are increasing so we are in the cross section of the matrix (input labels).
                 else if (y > -1 && x > -1)
                 {
                     // The array is populated however the correct type or element hasn't been assigned yet.
                     myTarget.GetArray()[y].typeElement = EnumConverter<TE>.Convert(y);
                     myTarget.GetArray()[y].versus[x].typeElement = EnumConverter<TE>.Convert(x);
 
                     // Set the multiplier value to whatever the input is at the cross.
                     myTarget.GetArray()[y].versus[x].multiplier = EditorGUILayout.FloatField(myTarget.GetArray()[y].versus[x].multiplier, floatStyle);
                 }
             }
 
             EditorGUILayout.EndHorizontal();
         }
 
         #region Load/Save buttons
         EditorGUILayout.BeginHorizontal();
 
         if (GUILayout.Button("Load Matrix"))
         {
             myTarget.LoadMatrix();
         }
 
         if (GUILayout.Button("Save Matrix"))
         {
             myTarget.SaveMatrix();
         }
 
         EditorGUILayout.EndHorizontal();
         #endregion
 
         EditorGUILayout.EndVertical();
         #endregion
     }
 
     /// <summary>
     /// Converter class to help going from a int to a generic enum field
     /// </summary>
     static class EnumConverter<TEnum> where TEnum : Enum
     {
         public static readonly Func<int, TEnum> Convert = GenerateConverter();
 
         private static Func<int, TEnum> GenerateConverter()
         {
             var parameter = Expression.Parameter(typeof(int));
             var dynamicMethod = Expression.Lambda<Func<int, TEnum>>(
                 Expression.ConvertChecked(parameter, typeof(TEnum)),
                 parameter);
 
             return dynamicMethod.Compile();
         }
     }
 }
 
               
 
Editor Implementation
 [CustomEditor(typeof(DamageMatrixTypes))]
 public class DamageMatrixTypesEditor : DamageMatrixBaseEditor<TypesAndElements.Types, DamageMatrixTypes>
 {
 }
 
 [CustomEditor(typeof(DamageMatrixElements))]
 public class DamageMatrixElementsEditor : DamageMatrixBaseEditor<TypesAndElements.Elements, DamageMatrixElements>
 {
 }
 
                
Matrix Base
 public class DamageMatrixBase<TE> : MonoBehaviour where TE : Enum
     {
         // The path will be the name of the files. The name of the file will end in either of the enums in TypesAndElements.
         private string filePath = "DamageMatrix" + typeof(TE).ToString().Split('+')[1] + ".json";
         public MainArray[] mainArray = new MainArray[Enum.GetNames(typeof(TE)).Length];
         private bool hasPopulated = false;
 
         private void Start()
         {
             LoadMatrix();
         }
 
         /// <summary>
         /// This method is called to get a multiplier value for the damage.
         /// Intended use: tower vs enemy.
         /// </summary>
         /// <param name="towerTypeElement">The type or element of the tower.</param>
         /// <param name="enemyTypeElement">The type or element of the enemy.</param>
         public virtual float Multiplier(TE towerTypeElement, TE enemyTypeElement)
         {
             return mainArray[Convert.ToInt16(towerTypeElement)].versus[Convert.ToInt16(enemyTypeElement)].multiplier;
         }
 
         /// <summary>
         /// Used to get the main array of types/elements that contains everything.
         /// </summary>
         public virtual MainArray[] GetArray()
         {
             Populate();
             return mainArray;
         }
 
         /// <summary>
         /// This will populate all the versus arrays for each item in the main array.
         /// </summary>
         public void Populate()
         {
             if (!hasPopulated)
             {
                 for (int i = 0; i < mainArray.Length; i++)
                 {
                     mainArray[i].versus = new VersusArray[Enum.GetNames(typeof(TE)).Length];
                 }
 
                 hasPopulated = true;
             }
         }
 
         /// <summary>
         /// Load the matrix file from the StreamingAssets folder.
         /// </summary>
         public virtual void LoadMatrix()
         {
             Populate();
             MatrixLoadSaveHelper<MainArray, TE>.LoadMatrix(mainArray, filePath, Enum.GetNames(typeof(TE)).Length);
         }
 
         /// <summary>
         /// Save the current matrix to the StreamingAssets (will override previous matrix).
         /// </summary>
         public virtual void SaveMatrix()
         {
             MatrixLoadSaveHelper<MainArray, TE>.SaveMatrix(mainArray, filePath);
         }
 
         [System.Serializable]
         public struct MainArray
         {
             [JsonConverter(typeof(StringEnumConverter))]
             public TE typeElement;
             public VersusArray[] versus;
         }
 
         [System.Serializable]
         public struct VersusArray
         {
             [JsonConverter(typeof(StringEnumConverter))]
             public TE typeElement;
             public float multiplier;
         }
     }
 
               Matrix Implementation
 public class DamageMatrixTypes : DamageMatrixBase<TypesAndElements.Types>
 {
 }
 
 public class DamageMatrixElements : DamageMatrixBase<TypesAndElements.Elements>
 {
 }
 
               Script with enums
 public class TypesAndElements : ScriptableObject
     {
         public enum Types
         {
             Light,
             Medium,
             Heavy
         }
 
         public enum Elements
         {
             Normal,
             Earth,
             Fire,
             Water,
             Air
         }
     } 
 
              Your answer
 
             Follow this Question
Related Questions
Error when trying to Serialize a field that is in a class 0 Answers
How to update serializedField showed on the inspector by script? 1 Answer
ApplyModifiedProperties() for UnityEvent ignores method name 0 Answers
Calling a method on a serialized property (Custom Editor with Reorderable List) 0 Answers
Custom Inspector: Using 'serializedObject' to access inherited members 1 Answer