#Sub Class Editor and Serialization
1 messages · Page 1 of 1 (latest)
Help me understand a lil better where you are at with this and what you are trying to do
because from what I understood, you wanted to
- use reflection to get all the fields of the sub class
- display those fields in the editor since unity would only do so for the base class
- be able to edit those fields from the editor
- have all that subclass instance be serialized
Yep on all of those.
okay, so what step are you at now?
https://gdl.space/dizuhiqola.cs
And the script updated. the forloop is where I am at.
okay, but like we said, you need to create a custom editor to display the information
and all that code should actually be in that custom editor
Any sources, pointers, and tutorials you can point me towards for the case?
So here's how you do it...
you create a new class that'll look like this:
[CustomEditor(typeog(Effects))]
public class EffectEditor : Editor
{
}
And in a different script, right?
Did that. Next step?
perfect
now, in here, create a method that will get all of the subclass fields
wrap it nicely in a method so that it is clean and make it return your list of fields
wait wait, actually I just realized your Effect class is not a unity object
so what you actually need to use is the Property Editor
oh, no. It's not.
that's fine, just a small difference
And do tell about the property editor.
so that one is for properties like in your situation
give me a second, so I don't mislead you again
Oh take your time.
I'll pull up my own similar thing I made to have all the functional stuff laid out
Go for it.
[CustomPropertyDrawer(typeof(Ingredient))]
public class IngredientDrawer : PropertyDrawer
this is the unity example
so just change the attribute CustomEditor to CustomPropertyDrawer
and the extension Editor to PropertyDrawer
Check.
perfect, now the PropertyDrawer has a method called OnGUI that you can override to complete
so if you type "override" inside the class, Visual Studio should list you all the methods you can override and you should see OnGUI on the list
just selected it and hit enter and Visual Studio will write the empty method as it needs to be for you
public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
{
}
it should look like this
Did that. was wondering about the other properties myself.
so, the arguments you get in this method are as follow:
Rect position is simply the position of the ui in your editor
SerializedProperty is the property Effect that unity has serialized in whatever class is containing it there
and finally the GUIContent which is just the label
it is in that method that you tell unity what field to draw in the editor's ui
but first we need to know ourselves what the fields are
so what I suggest you do first is add the constructor
you know how to do that, right?
Refresh my memory. I optimally want to drag and drop the monoscript onto the slot in the SO to extract the class and its properties from it, instantiating it onto the SO to edit.
It would also be great for testing on the scene, as well.
a constructor is what you use to instantialize classes
so, for example
in your effects class, you could have
public Effects(float track)
{
this.toTrack = track
}```
so that when you create a new Effects from script, you would do Effects effect = new Effects(track: 10)
if you don't have any constructor, which is your case at the moment, the program will act as if there is an empty constructor
public Effects()
{
}```
which is what you want for your Effects class anyways, since unity will actually create an instance of it to display it in the editor afterwards
now if you had a constructor that took parameters in, unity would freak out because it does not know what parameters to give in
still, you can make it do things inside of the constructor if you want to "initialize" some data when the object is created
you follow?
Yep. The problem is that this could be of different parameters, hence why I was also looking at reflectors, to grab them from the fly to put onto this new object, like a base to mold with extra things.
that's another thing, I was just explaining the constructors to make sure you knew how that worked
point is, we want a constructor for your Property Drawer class
Oh. apologies. Thanks for the refresher, by the way.
because we need to read all the fields in there, as soon as the instance is "constructed"
it would be in your EffectsEditor class that we want to get the fields
does this mean you had something else mess with serialization?
Well... I tried to at first, but...
make sure to call it EffectsEditor
it needs to have the same name as your class
Check.
awesome, now write your method to get all the fields and call it in the constructor
In EffectsEditor()?
it would be nicer if you made a new method to write all that code in, and then call that method in EffectsEditor()
Then ToComplete()!
private FieldInfo[] LoadFields()
{
const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance;
FieldInfo[] fields = fieldType.GetFields(bindingFlags);
}```
All I just need now are the variables for the monoscripts I'm scanning from.
nah, you don't need it
you already have it
you know what, you should actually make a
private static Dictionary<Type, FieldInfo[]> subTypeFields = new Dictionary<Type, FieldInfo[]>();```
so that you only load the fields once, then your method would be
private FieldInfo[] LoadFields()
{
const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance;
//Get the type from the fieldInfo
//this fieldInfo is already given to you by PropertyDrawer
Type type = fieldInfo.FieldType;
if(!subTypeFields.TryGetValue(type, out FieldInfo[] fields)
{
//if you don't already have the fields, go get them
fields = fieldType.GetFields(bindingFlags);
//Make a list so we can easily remove not serialized fields
List<FieldInfo> fieldsList = new List<FieldInfo>();
for(int i = 0; i < fields.Length; i++)
{
FieldInfo field = fields[i];
if(!field.IsNotSerializable) fieldList.Add(field);
}
//now that we have all the fields we want to display in out list, we just save it in our static dictionary
fields = fieldsList.ToArray();
subTypeFields.Add(type, fields);
}
//Finally, if we already had the fields in our dictionary or just added them, return our fields
return fields;
}```
voila
And where would I put this? Inside or outside the class?
inside the EffectsEditor class
but do you understand the code? because that is important
Most of it. I just need to know about binding flags.
binding flags is sort of a filter for which fields to include
just fix up the typos I made and you should be set for that part
Check.
for line 26, you can just use "type" since we already have it from line 22
yes nice, so now you are caching your fields instead of getting them each time
because you have to keep in mind that using reflection is actually slow
and so using it every frames in complicated manner can impact your frame rate a lot
so these simply things like saving it in a dictionary will help
Well... I tried that.
no, it is fields = type.GetFields
After that.
Oh, for certain.
And go for it. I'm ready.
the idea is to loop through each of the fields in your fields array and use the editor drawing functions
Yep.
so, to tell the editor to draw fields, you simply use the static methods of EditorGUI
Yep.
try it out, inside your OnGUI methods
type in EditorGUI.field and you should see the methods list pop up
you have Vector fields for vectors, Toggle fields for booleans, Text fields for strings, Popups (dropdown) for enums and lists, all your number fields such as long, float, double, decimal
Type that in where?
I thought we were searching for that. You scared me for a second, there.
OH! Like that! Yep!
this way you can find the right method for each field type you are drawing from your list
you know what, I just got an idea
we could save the methods into another dictionary so you don't have to constantly make if checks to compare the type of the field you want to draw
For certain! Like water, they can be anything.
what?
anyways
so create another static dictionary
wait, actually, let me think this through first
The classes. Some have enums, some have ints, some have floats, others have strings.
yeah, okay, we were talking about the same thing
okay, this is gonna be easy
we are going to use more reflection
so, create another static dictionary
private static Dictionary<Type, MethodInfo> methodTypes = new Dictionary<Type, MethodInfo>();```
Just did.
wait, hold on, this got more complicated
Take your time.
because, I'd rather not use GetMethod(name) since we already know exactly which static method we want
and we can't store Func delegates in the dictionary because they all have different parameter types and return types
so storing the MethodInfo would be the best bet so that we can then just do MethodInfo.Invoke to easily call them
Let's go for it.
I found 2 solutions but I am looking for the best one
var methodInfo = SymbolExtensions.GetMethodInfo(() => Program.Main());
we have this one, but it seems to also call the method, which would be bad?
And the other one?
MethodInfo info1 = ((Func<int, int>)Math.Abs).Method;
// or double ?
MethodInfo info2 = ((Func<double, double>)Math.Abs).Method;```
this one involves casting
here if you want to read on it
yeah, I think the second is best
Looks simpler, as well.
we can make ourselves a little static method that would make the casting automatically
public static MethodInfo GetMethodInfo<T, U, TResult>(Func<T, U, TResult> fun) => fun.Method;
like this answer
since I think they all have like 2 inputs and 1 return type
public static MethodInfo GetMethodInfo<T, U, TResult>(Func<T, U, TResult> fun) => fun.Method;
private static Dictionary<Type, MethodInfo> methodTypes = new Dictionary<Type, MethodInfo>()
{
{ typeof(int), GetMethodInfo<Rect, int, int>(EditorGUI.IntField) },
{ typeof(Enum), GetMethodInfo<Rect, Enum, Enum>(EditorGUI.EnumPopup) }
};```
there, that should work nicely, just have to complete it
you get the process? how it works?
The first line would be getting all the methods of the class to reconstruct them.
The second is the same declaration, but this time, to make specific variables that appear in the class.
which line? in the code I sent?
Yep
okay, so the first line is a Method
it is equivalent to...
public static MethodInfo GetMethodInfo<T, U, TResult>(Func<T, U, TResult> fun)
{
return fun.Method;
}```
so it takes in as parameter a Func
a Func is a type of delegate, like Action
System.Func and System.Action are, in a way, variables that contain a Method
which is what we call delegates
the difference between the two is that Action does not have input parameters, while Func does
and so that is what these Generics are
<T, U, TResult> are generics, you can name them whatever you want when you make a Method or class that uses Generics
these are simply indication of what types we want something to be
so for example Func<ParameterType1, ParameterType2, ReturnType>
this will tell us that this Func is a delegate that can be called as a Method that takes in 2 parameters, one of type ParameterType1 and another of type ParameterType2
and that is returns us a value of type ReturnType
you follow?
Yep.
so, by putting that Method as a parameter of this static Method we created here, it pretty much just gets casted as a Func
and then, we can simply get the MethodInfo value from the Func
so to make sure the casting of the Method into a Func goes well, we have to specify those Generics so that is known which Func generics to cast it to
the second thing is our Dictionary, it just stores those MethodInfos
you know how dictionaries work in programming, right?
each entry consists of a Key and a Value, the key can be used as index to get our value
this way we use the type of the field we want to draw to get the paired method
so then you just have to pair those in your dictionary, then, this will make it incredibly easy to call each method
I know now.
cool, let me know when you are ready for the next step
Ready as of now.
yeah? you found the Methods for all the types?
well, anyways, you can add them later
right, so now in your on gui, get your fields array
loop thought it
use the field's FieldType to get the Method from that dictionary, use TryGetValue so that if there is none for that type, you can Debug.Error("Missing Method") and add it
then, when you have the MethodInfo, use the invoke function to invoke it with the right parameters
so how the EditorGUI methods work, if that you give in the Rect position as your first argument, then the current value of your Effect
to get the current value, use the GetValue method on your field
and then, the EditoGUI method will return you the new value if it was changed in the editor, otherwise, the same value
so you'd want to then perform a field.SetValue to set the new value you just got back
got it?
Yep!
alright, give it a try
Also, an update, @keen fulcrum. Am I off base on anything?
very
your line 53 should be
FieldInfo[] fields = LoadFields()
okay, that's really not it lol
private void FieldEditorGUI(FieldInfo field)
{
}```
first, let's make this Method that will handle the editor gui stuff for a given field info
private void FieldEditorGUI(FieldInfo field, Rect rect)
{
//First, get the method we want for the field gui
if(!methodTypes.TryGetValue(field.FieldType, out MethodInfo method)
{
//if it does not exist, we can't actually just find it since here is no simple relation from our field to the method we need
Debug.Error($"No Method for Field Type {field.FieldType}");
//We will display this error so that we know to add this method
//But there is nothing else we cam effectively do that won't complicate thing a lot
//So we will just return to end this method
return;
}
//Other wise, if we do have the method, we can continue
//First we will get the current value of the field
object value = field.GetValue();
}```
I have a meeting so I will be back in a bit
Go for it, but where would I place that, before you go?
it is a new method inside your editor class
Doing that while you’re at your meeting.
okay so I'm not sure for this step, I'd need to test out this code but I don't have the environment for that right now
private object GetPropertyValue(SerializedProperty prop)
=> fieldInfo.GetValue(prop.serializedObject.targetObject);```
I'm not sure if this would work right
private object GetPropertyValue(SerializedProperty prop)
{
object value = fieldInfo.GetValue(prop.serializedObject.targetObject);
Debug.Log(value.GetType());
return value;
}```
okay, place this second method in your editor class too
and in before your line 52 in OnGUI write object value = GetPropertyValue(prop);
then check what is written in your console
Like this?
yeah, now change FieldEditorGUI(FieldInfo field, Rect rect) for FieldEditorGUI(FieldInfo field, object prop, Rect rect)
because we will need the property to get it's class fields
an inside the field.GetValue(), it should be that prop we are putting in field.GetValue(prop)
because the object we put into the method GetValue must be the instance of the Effects class that contains that FieldInfo
Check.
exactly, now we have to test if these objects are the good ones because I am honestly not sure if my GetPropertyValue method is right
so save that and run it to see what the console returns
in line 57, give in object value as the prop parameter
okay, now we have the data we need, let's check that error you got
oh right, that's from your older code... which is really a mess lol
you can remove or just comment out lines 61 to 72
😅 Yeah... Apologies for that....
hey, attempts have their own reward. If you really try, you learn things
anyways, now we can finally draw the damn gui
so, line 91 gets us the current value of that field for the Effects field of the instance we are inspecting
now, we want to call the MethodInfo from the dictionary
object value = field.GetValue(prop);
//We invoke the GUI method which will return us the value currently in the inspector gui for this field
object newValue = method.Invoke(rect, field.Name, value)
//We give in the rect for its position and our current field value
//Now we can verify is the new value is different
//If they are the same, we won't continue. We use the Equals Method for more thorough comparison
if(value.Equals(newValue)) return;
//If they are different, we set the field value for the new value
field.SetValue(prop, newValue);
try it out
I made an edit to the Invoke btw
wait, actually, we need to make a modification to something else
public static MethodInfo GetMethodInfo<T, TResult>(Func<Rect, string, T, TResult> fun)
{
return fun.Method;
}```
modify this function
this will simplify things
and we also want the string parameter as it is the label that contains the name of the field
public static MethodInfo GetMethodInfo<T, TResult>(Func<Rect, string, T, TResult> fun) => fun.Method;```
sorry, had a typo, don't forget the T after string
actually, no, wait
public static MethodInfo GetMethodInfo<T>(Func<Rect, string, T, T> fun) => fun.Method;
there, the T and TResult were the same anyways
and so, in your dictionary, just remove the Rect and leave it as GetMethodInfo<int> and GetMethodInfo<Enum>
Did that!
awesome, that should be it
give it a try
by the way System.Single is float so might want to add the float gui method too
Most numbers, in game, are going to be floats, anyways!
yeah, now time to see if it all works well in the unity inspector
The results.
show me your code around line 94
right, so it should be
new object[] { field.Name, value }
no wait, add rect as the first one too
okay, yeah, idk why I did that
method.Invoke(null, new object[] { rect, field.Name, value });
there, that's it
the first argument should be null because it represent the instance that owns that field
but in this case it is Static, so there is none
because these methods are all EditorGui static methods
Taken rect's really the position, BUT...
exaclty, okay try it out now
aw, it's overlapping tho
we just have to add to the rect because that is the position
so that would be in the loop, after we call the method
you should make a foldout actually
a foldout?
mhm
okay so right before the loop, let's make a bit of polishing
after line 54, add
//if we have no serialized fields, we skip everything
if(fields.Length == 0) return;```
Check
then, after that, start a foldout by writing
EditorGUI.BeginFoldoutHeaderGroup(position, isFolded, value.GetType().Name);```
then, after the loop, at line 61
EditorGUI.EndFoldoutHeaderGroup();```
oh, actually the first one would be
isFolded = EditorGUI.BeginFoldoutHeaderGroup(position, isFolded, value.GetType().Name);
because we want it to return us the changes of the fold or not folded
then make yourself the new bool variable isFolded to store that
and after that, we just have to offset the y values of the Rect position
Well, I did that, but...
make it a class variable
Check
a class variable is a variable that belongs to the whole class
for example, your dictionaries are static class variables
we want isFolded to be a non static class variable
so it needs to be outside of the method
at line 20 for example
private bool isFolded;
Check
yes exactly
isFolded = EditorGUI.BeginFoldoutHeaderGroup(position, isFolded, value.GetType().Name);```
now make sure this returns the value to isFolded so that when we fold it, it stays folded
Check.
One step ahead.
okay, we still have to offset the positions
in the loop, change it to this:
//Offset the y by the height * (i + 1)
Rect fieldPosition = new Rect(position.x, position.y + ((i+1) * position.height), position.width, position.height)
FieldEditorGUI(field, value, fieldPosition);
I'm not sure if it should be + or -, but we will see in the inspector
if it is higher, then it means it should be a -
Only one way to find out!
make sure to save your changes, eh
Yep! And as seen here!
nice, okay, seems we have to offset the foldout too
for certain, but also the debug. It should be buffEffects, the subclass, by the way.
well, that doesn't matter too much
but you can get the actual class instead of the base class
oh wait, it's only getting the base class fields?
would you prefer is we made a voice chat so you could share your screen?
🤷♂️ Sure, go for it.