#Sub Class Editor and Serialization

1 messages · Page 1 of 1 (latest)

keen fulcrum
#

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

  1. use reflection to get all the fields of the sub class
  2. display those fields in the editor since unity would only do so for the base class
  3. be able to edit those fields from the editor
  4. have all that subclass instance be serialized
keen fulcrum
#

okay, so what step are you at now?

sweet ore
keen fulcrum
#

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

sweet ore
#

Any sources, pointers, and tutorials you can point me towards for the case?

keen fulcrum
#

So here's how you do it...
you create a new class that'll look like this:

[CustomEditor(typeog(Effects))]
public class EffectEditor : Editor
{

}
sweet ore
keen fulcrum
#

yes

#

also, make it extend : Editor

sweet ore
#

Did that. Next step?

keen fulcrum
#

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

sweet ore
#

oh, no. It's not.

keen fulcrum
#

that's fine, just a small difference

sweet ore
#

And do tell about the property editor.

keen fulcrum
#

so that one is for properties like in your situation

#

give me a second, so I don't mislead you again

sweet ore
#

Oh take your time.

keen fulcrum
#

I'll pull up my own similar thing I made to have all the functional stuff laid out

sweet ore
#

Go for it.

keen fulcrum
#
[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

sweet ore
#

Check.

keen fulcrum
#

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

sweet ore
#

Did that. was wondering about the other properties myself.

keen fulcrum
#

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?

sweet ore
#

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.

keen fulcrum
#

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?

sweet ore
#

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.

keen fulcrum
#

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

sweet ore
#

Oh. apologies. Thanks for the refresher, by the way.

keen fulcrum
#

because we need to read all the fields in there, as soon as the instance is "constructed"

sweet ore
#

And this would be in OnGui(), or elsewhere?

#

I only just found out.

keen fulcrum
#

it would be in your EffectsEditor class that we want to get the fields

keen fulcrum
sweet ore
#

Well... I tried to at first, but...

keen fulcrum
#

it needs to have the same name as your class

sweet ore
#

Check.

keen fulcrum
#

awesome, now write your method to get all the fields and call it in the constructor

sweet ore
#

In EffectsEditor()?

keen fulcrum
#

it would be nicer if you made a new method to write all that code in, and then call that method in EffectsEditor()

sweet ore
#

Then ToComplete()!

keen fulcrum
#
private FieldInfo[] LoadFields()
{
  const BindingFlags bindingFlags = BindingFlags.Public | BindingFlags.Instance;
  FieldInfo[] fields = fieldType.GetFields(bindingFlags);
}```
sweet ore
#

All I just need now are the variables for the monoscripts I'm scanning from.

keen fulcrum
#

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

sweet ore
keen fulcrum
#

inside the EffectsEditor class

#

but do you understand the code? because that is important

sweet ore
#

Most of it. I just need to know about binding flags.

keen fulcrum
#

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

sweet ore
#

Check.

keen fulcrum
#

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

sweet ore
#

Well... I tried that.

keen fulcrum
#

no, it is fields = type.GetFields

sweet ore
#

After that.

keen fulcrum
#

exactly

#

now you completed step 1, we can move on to step 2

sweet ore
#

Oh, for certain.
And go for it. I'm ready.

keen fulcrum
#

the idea is to loop through each of the fields in your fields array and use the editor drawing functions

sweet ore
#

Yep.

keen fulcrum
#

so, to tell the editor to draw fields, you simply use the static methods of EditorGUI

sweet ore
#

Yep.

keen fulcrum
#

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

keen fulcrum
#

in your OnGUI method

sweet ore
#

I thought we were searching for that. You scared me for a second, there.

keen fulcrum
#

you don't see the methods pop up?

#

like this

sweet ore
#

OH! Like that! Yep!

keen fulcrum
#

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

sweet ore
#

For certain! Like water, they can be anything.

keen fulcrum
#

what?

#

anyways

#

so create another static dictionary

#

wait, actually, let me think this through first

sweet ore
# keen fulcrum what?

The classes. Some have enums, some have ints, some have floats, others have strings.

keen fulcrum
#

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>();```
sweet ore
#

Just did.

keen fulcrum
#

wait, hold on, this got more complicated

sweet ore
#

Take your time.

keen fulcrum
#

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

sweet ore
#

Let's go for it.

keen fulcrum
#

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?

sweet ore
#

And the other one?

keen fulcrum
#
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

sweet ore
#

Looks simpler, as well.

keen fulcrum
#

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?

sweet ore
#

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.

keen fulcrum
#

which line? in the code I sent?

sweet ore
#

Yep

keen fulcrum
#

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?

sweet ore
#

Yep.

keen fulcrum
#

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

sweet ore
#

I know now.

keen fulcrum
#

cool, let me know when you are ready for the next step

sweet ore
#

Ready as of now.

keen fulcrum
#

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?

sweet ore
#

Yep!

keen fulcrum
#

alright, give it a try

sweet ore
#

Also, an update, @keen fulcrum. Am I off base on anything?

keen fulcrum
#

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

sweet ore
keen fulcrum
sweet ore
#

Doing that while you’re at your meeting.

keen fulcrum
#

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

sweet ore
#

Like this?

keen fulcrum
#

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

sweet ore
#

Check.

keen fulcrum
#

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

sweet ore
#

Before

#

And after.

keen fulcrum
#

awesome, it worked!!!

#

wow, I can't even post gifs

sweet ore
#

... Bizarre.... Taken I do so all the time...

#

But what's next?

keen fulcrum
#

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

sweet ore
#

😅 Yeah... Apologies for that....

keen fulcrum
#

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

sweet ore
#

Well... I did those...

#

But...

keen fulcrum
#
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>

sweet ore
#

Did that!

keen fulcrum
#

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

sweet ore
#

Most numbers, in game, are going to be floats, anyways!

keen fulcrum
#

yeah, now time to see if it all works well in the unity inspector

sweet ore
#

The results.

keen fulcrum
#

show me your code around line 94

sweet ore
#

Over here.

#

I originally made it this.

keen fulcrum
#

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

sweet ore
#

Taken rect's really the position, BUT...

keen fulcrum
#

exaclty, okay try it out now

sweet ore
#

IT WORKS!

#

Kind of...

keen fulcrum
#

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

sweet ore
#

a foldout?

keen fulcrum
#

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;```
sweet ore
keen fulcrum
#

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

sweet ore
#

Well, I did that, but...

keen fulcrum
#

make it a class variable

sweet ore
keen fulcrum
#

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;

sweet ore
keen fulcrum
#

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

sweet ore
#

Check.

keen fulcrum
#

exactly

#

now let's see what that gives us

sweet ore
#

One step ahead.

keen fulcrum
#

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 -

sweet ore
#

Only one way to find out!

keen fulcrum
#

make sure to save your changes, eh

sweet ore
#

Yep! And as seen here!

keen fulcrum
#

nice, okay, seems we have to offset the foldout too

sweet ore
#

for certain, but also the debug. It should be buffEffects, the subclass, by the way.

keen fulcrum
#

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?

sweet ore
#

🤷‍♂️ Sure, go for it.