- 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