#Reflection vs. Source Generators

1 messages ยท Page 1 of 1 (latest)

narrow pelican
#
 [Serializable]
    public class LocalFloat
    {
        private FieldInfo FieldInfo => _fieldInfo ??= _getFieldInfo(); 
        private FieldInfo _fieldInfo;
        private Expression ControllerExpression => _controllerExpression ??= Expression.Constant(Controller);
        private Expression _controllerExpression;
        private Expression FieldExpression => _fieldExpression ??= Expression.Field(ControllerExpression, FieldInfo);
        private Expression _fieldExpression;
    
        [Tooltip("The controller that contains the float.")]
        public MonoBehaviour Controller;
        public string FieldName;

        private Func<float> _getter;
        private Func<float, float> _setter;
        
        public float Get()
        {
            _getter ??= _createGetter();
            return _getter();
        }

        public float Set(float value)
        {
            _setter ??= _createSetter();
            return _setter(value);
        }
        
        private Func<float> _createGetter()
        {
            return Expression.Lambda<Func<float>>(FieldExpression).Compile();
        }

        private Func<float, float> _createSetter()
        {
            var param = Expression.Parameter(typeof(float), "newValue");
            var expression = Expression.Assign(FieldExpression, param);
            return Expression.Lambda<Func<float, float>>(expression, param).Compile();
        }

        private FieldInfo _getFieldInfo()
        {
            var type = Controller ? Controller.GetType() : throw new ArgumentException("No component selected.");
            return type.GetField(FieldName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new ArgumentException($"Unknown field {FieldName}.");
        }
    }```
#

tl;dr of the code:
It stores a reference to a float field in another component

#

And compiles a getter and setter for it

#

(It should be mentioned that there is a propertydrawer for this type not shown here)

#

I just don't see how source generators can accomplish something like this

umbral pebble
#

what is your target to do with that?

knotty harbor
#

It stores a reference to a float field?

narrow pelican
#

hang on, ill show a slight demo

umbral pebble
#

how does your IDE react to your funcky reflection?

#

does it find the getters and setters?

narrow pelican
#

It doesn't need to

#

It compiles it into the delegates

#

Then I just call the delegates

#

Here's the usage

umbral pebble
#

and what is the achievement ?

narrow pelican
#

It integrates my code into the user's code which is unknown to me

#

So it basically allows the user to inject their controller's values into my tool

umbral pebble
#

just make your game a serializer of json/xml

#

when you want your game to be modifyable

narrow pelican
#

This isn't for a game

#

this is a system I'm selling on the asset store once it gets approved

#

so it allows the client to inject their values into the system

umbral pebble
#

i dont see the sense behind it

#

what can i do when i put my values into it?

narrow pelican
#

You don't have to do anything else - my code uses those values

umbral pebble
#

and what does your code do?

#

with the values?

narrow pelican
#

The system itself is just built to remove boilerplate code in a typical combat system

#

The workflow of:

high canopy
#

reflection is fast ok

#

get used to it

#

it's a 1 time thing to grab/generate the memberinfo/methodinfo

narrow pelican
#
  • Have damage values
  • Have health values
  • On collision, subtract damage from health
high canopy
#

then it's a simple call like any other calls

#

a set of instructions

#

invoked

umbral pebble
#

we dont talk about generation

high canopy
#

no it's not

umbral pebble
#

timing

high canopy
#

it can even be wrapped with a simple assignment to a delegate

narrow pelican
#

In this case, I don't believe source generation can do the same thing

umbral pebble
knotty harbor
high canopy
#

bla bla

#

proof

narrow pelican
#

Can you explain that a bit? What type ambiguity? I'm lost

#

I've done profiles btw and there's little to no overhead

umbral pebble
#

it has to check if the value is given

#

if its not there it cant decide what to call

narrow pelican
#

What is it in this context?

#

Are you referring to the constant null checks?

#

Those are negligible

knotty harbor
umbral pebble
#
public void A(Box ob)
publi void A (NOBox ob)

xyz.A(null); <-- ambiguity conflict
high canopy
narrow pelican
high canopy
#

he's using the wrapper as a field, if you've noticed

narrow pelican
#

From what I understand, this would require a separate component for each class it references

#

Which simply is not viable

knotty harbor
narrow pelican
#

The added value is that I don't have to know about the field I'm referencing at the time of writing

knotty harbor
#

I was replying to lyraxis

high canopy
#

he means me, as a person

#

am useless in this topic xD

knotty harbor
#

Did not say that

high canopy
#

oh

narrow pelican
#

I'm still confused about the ambiguity thing you mentioned

umbral pebble
#

nice now

#

everyone is confused

narrow pelican
#

alright in that case, let's circle back to source generators

#

how could you do logic like this in a source generator?

umbral pebble
#

and whats the problem of doing it with the OOP way ?

narrow pelican
#

I don't understand the question

#

What would be the OOP way?

umbral pebble
#

to not use reflection

#

or generators

narrow pelican
#

I'm writing this as a tool for others to implement into their games

#

I don't know what code it will be interacting with

#

So I need reflection or source generation

#

It simply can't work without it

knotty harbor
high canopy
#

can be basically plugged into any component for anyone to work with

#

and can be instantiated and used by any other objects that may need it's functionality

narrow pelican
#

It defers the value referenced to the end user (the other developer)

umbral pebble
#

why do you not just let them subscribe with their actions?

narrow pelican
#

Because it's not event-based

umbral pebble
#

actions are delegates

#

event is a special delegate

#

dont let me search it agian

narrow pelican
#

I'm not exposing these delegates

#

No one is subscribing to them

high canopy
#

I would only complain how most of these methods are not static and once-per-runtime only lol

narrow pelican
#

They're simply there to hold the runtime-compiled logic

narrow pelican
umbral pebble
#

gj

#

lets go back and learn "how to create an object"

narrow pelican
#

To give everyone more context, this is a system which handles combat logic, aimed at designers or devs with less experience. The purpose of the reflection here it to allow the user to select float fields from another component (that I don't know about) as damage and health values.

umbral pebble
#

why do you not let them inherit from a dmgscript

#

which has those 2 values

#

just as a base class

#

and use that

narrow pelican
#

Inheritance opens up room for roadblocks

umbral pebble
#

a what

narrow pelican
#

ECS > Inheritance in most cases

umbral pebble
#

idk what you mean

#

can you say it in java ?

narrow pelican
#

Sorry let me reword that

#

it's not an ECS issue

#

Composition > Inheritance in most cases

umbral pebble
#

doesnt mean inheritance is bad

narrow pelican
#

Also, my target audience is potentially people who don't know what inheritance is

umbral pebble
#

if its necessary it has to be

narrow pelican
#

The purpose of this discussion was to see if we can use source generators here

umbral pebble
#

lul

knotty harbor
#

better not make a base class but simply an interface. then you can even do simple dependency injection logic and presto

umbral pebble
#

or that

narrow pelican
#

that doesn't make it simpler though

#

Maybe on my end, but I can understand it fine

high canopy
#

well for doing it via a source generator you could even manually write the string via StringBuilder

narrow pelican
#

define write the string

high canopy
#

and THAT's not OOP ๐Ÿ˜›

umbral pebble
#

its fascinating how you all just ignore : SOLID principles, KISS, MVVM

#

it aint that hard to implement a fucking interface

#

the IDE tells you what to do

high canopy
#

it'd take time

umbral pebble
#

if they are too stupid to do what the read lines say

#

idk

narrow pelican
#

An interface can't implement collision logic

high canopy
#

why do that when you just add a plug and play component to do that

narrow pelican
#

I think this discussion has run its course

umbral pebble
#

it never had one

knotty harbor
#

an interface can implement any logic

narrow pelican
#

I was open to being converted to source generators, but I believe that my current approach is the best barring a possible source generator approach I don't know about

umbral pebble
#

default methods ?

high canopy
umbral pebble
#

sometimes yes, sometimes no

knotty harbor
narrow pelican
#

true

high canopy
#

I gave it some thought I couldn't see how

#

you need to get the fields of a target's type

knotty harbor
#

which you can easily do from the syntax tree

high canopy
#

..with reflection ๐Ÿ˜›

narrow pelican
#

My current approach is incredibly simple for the user and only has overhead of one small compile to create getters and setters. The only possible improvement would be to move that compile from runtime to design time, aka to use a source generator instead

high canopy
#

oh..

knotty harbor
high canopy
#

oh.. yeah that makes sense

#

so how could I access a type's syntax tree? ๐Ÿ˜›

#

say, of my MonoBehaviour target

narrow pelican
#

It's complicated, but I promise the syntax tree would give you anything reflection would in this context

#

I've written source generators before, but I don't think it fits this use case

high canopy
#

well in that case it could become more performant, if you wrote methods via string builder

#

could even go a little wild on the if statements, wouldn't hurt

narrow pelican
#

I'm going to need a concrete example

#

writing methods via string builder is fine and all, until it needs to be inside a property that can be in any monobehaviour, that needs to reference another monobehaviour's field

knotty harbor
high canopy
narrow pelican
narrow pelican
high canopy
#

well, you'd still have to generate it on the spot after you got the 'target'

knotty harbor
#

i'm not even on my pc rn

narrow pelican
#

Even if you just give me a vague description, I can explore it myself

#

The problem I'm seeing with source generation is that each individual instance of the component uses different code internally, generated at runtime

#

So to replicate that in source generation, it would take outputting a different component for each instance

#

My approach stores the dynamic code as a delegate, but source generation doesn't have that luxury

#

It has to be concrete to get the benefits of source generation (no runtime overhead)

#

(leaving this channel for now, if anyone continues the conversation, please ping me)

knotty harbor
#

repost so i can look at the code

 [Serializable]
    public class LocalFloat
    {
        private FieldInfo FieldInfo => _fieldInfo ??= _getFieldInfo(); 
        private FieldInfo _fieldInfo;
        private Expression ControllerExpression => _controllerExpression ??= Expression.Constant(Controller);
        private Expression _controllerExpression;
        private Expression FieldExpression => _fieldExpression ??= Expression.Field(ControllerExpression, FieldInfo);
        private Expression _fieldExpression;
    
        [Tooltip("The controller that contains the float.")]
        public MonoBehaviour Controller;
        public string FieldName;

        private Func<float> _getter;
        private Func<float, float> _setter;
        
        public float Get()
        {
            _getter ??= _createGetter();
            return _getter();
        }

        public float Set(float value)
        {
            _setter ??= _createSetter();
            return _setter(value);
        }
        
        private Func<float> _createGetter()
        {
            return Expression.Lambda<Func<float>>(FieldExpression).Compile();
        }

        private Func<float, float> _createSetter()
        {
            var param = Expression.Parameter(typeof(float), "newValue");
            var expression = Expression.Assign(FieldExpression, param);
            return Expression.Lambda<Func<float, float>>(expression, param).Compile();
        }

        private FieldInfo _getFieldInfo()
        {
            var type = Controller ? Controller.GetType() : throw new ArgumentException("No component selected.");
            return type.GetField(FieldName, BindingFlags.Instance | BindingFlags.Static | BindingFlags.Public | BindingFlags.NonPublic) ?? throw new ArgumentException($"Unknown field {FieldName}.");
        }
    }
knotty harbor
#

@narrow pelican in your gif there's a dropdown, here there is only string fieldname

narrow pelican
#

It finds all valid floats in the class and provides a dropdown to select one

knotty harbor
#

ah. then why are you ?? throw new ArgumentException($"Unknown field {FieldName}."); on the last line?

narrow pelican
#

Safety

#

If the field is removed from the user's code

knotty harbor
#

but then it doesn't show up in the dropdown anymore either

narrow pelican
#

But it will still be serialized as the old value

#

Or if the user doesn't select a value

knotty harbor
#

ok