0

What I am trying to do: I am trying to make component based Objects which can be easly created with custom set type of rules for each component value.

How I am doing it: I have created IComponent interface which each component implements. All components need to be structs, example:

public struct Weight : IComponent
{
     int weight;
}

Each Object is defined by just list of components with their values. Then to make it custom set of rules I made ObjectSettings which holds list of generic class ComponentSetup<T> where T : IComponent. ComponentSetup is a class which by reflection gets list of fields in IComponent and pairs them in Dicionary as FieldName and GenerationType for field. For example: for Object "Car" :

Car:
    Weight:
         weight: 
            GenerationType: RandomRange
               Min: 1200
               Max: 1700

For Object "Human":

Human:
    Weight:
         weight: 
            GenerationType: NormalDistribution
               Expected Value: 70
               Variance: 4

For Object "1kg dumbbell":

1kgDumbbell:
    Weight:
         weight: 
            GenerationType: Fixed
               Value: 1

In order to get generated Objects I used reflection to set values of components compose in List and return as Object.

The problem with this approach: When I want to generate 5k-10k of those Objects it takes way too much time.

My solution so far: I generate semi filled objects(on startup) and store them as prefabs in PrefabManager. They are Objects with components' values set only if their GenerationType is "Fixed" and then only fill values with other types of Generation.

My question: How can I make setting values by reflection faster, if it's not possible then how can I get the same result but faster? I also would like to keep prefab generation on startup because they help me instantiating Objects because I don't need to create whole new object, just copy prefab and fill it, which is faster in my case.

EDIT: Adding example code. I didn't test it however it should be easy to understand what I am trying to do:

namespace Example
{
//ProceduralObject Component intreface
public interface IComponent
{
}

//Example component for procedural object
public struct Weight : IComponent
{
    public int weight;
}

//object with procedurally generated components
public class ProceduralObject
{
    public List<IComponent> components = new List<IComponent>();
}


public class ProceduralObjectSettings
{
    public Dictionary<string,ComponentSetup> ComponentSetups = new Dictionary<string,ComponentSetup>();

    public ProceduralObjectSettings()
    {
    }

    public void AddComponent(Type t)
    {
        //check if added component is assignable from correct interface
        if (t.IsAssignableFrom(typeof(IComponent))) ComponentSetups.Add(t.Name,new ComponentSetup(t));
    }
    
    //getting ProceduralObject with generated components
    public ProceduralObject getGeneratedObject()
    {
        ProceduralObject newObject = new ProceduralObject();
        
        
        foreach (var componentSetup in ComponentSetups)
        {
            newObject.components.Add(componentSetup.Value.getGeneratedComponent());
        }

        return newObject;
    }
}

public class ComponentSetup 
{
    // Collection of properties of IComponent it represents
    public Dictionary<string, IGenerationType> propertyGenerationSettings = new Dictionary<string, IGenerationType>();
    // Type of IComponent it represents
    public Type t;
    public ComponentSetup(Type t)
    {
        this.t = t;
        
        //Getting all fields of represented IComponent and adding them to propertyGenerationSettings with default GenerationType
        var fields = t.GetFields();
        for (int i = 0; i < fields.Length; i++)
        {
            propertyGenerationSettings.Add(fields[i].Name,new EmptyGenerationType());
        }
    }
    
    //Generating new component with settings
    public IComponent getGeneratedComponent()
    {
        IComponent toReturn = (IComponent)Activator.CreateInstance(t);

        var fields = toReturn.GetType().GetFields();
        
        foreach (var property in propertyGenerationSettings)
        { 
            var fieldInfo = fields.First(field => field.Name == property.Key);
            toReturn.GetType().SetMemberValue(fieldInfo, property.Value.GetGeneratedValue());
        }

        return toReturn;
    }
}

public interface IGenerationType
{
    System.Object GetGeneratedValue();
}

public class EmptyGenerationType : IGenerationType
{
    public object GetGeneratedValue()
    {
        throw new Exception("You can't use EmptyGenerationType");
    }
}

public class RandomRangeGenerationType : IGenerationType
{
    private double min, max;
    public RandomRangeGenerationType(double min, double max)
    {
        this.min = min;
        this.max = max;
    }
    
    public object GetGeneratedValue()
    {
        return null; /* return */
    }
}

public class NormalDistributionGenerationType : IGenerationType
{
    private double expectedValue, variance;
    public  NormalDistributionGenerationType(double expectedValue, double variance)
    {
        this.expectedValue = expectedValue;
        this.variance = variance;
    }
    
    public object GetGeneratedValue()
    {
        return null; /* return */
    }
}

public class FixedGenerationType : IGenerationType
{
    public double value;

    public FixedGenerationType(double value)
    {
        this.value = value;
    }
    
    public object GetGeneratedValue()
    {
        return null;
    }
}


public class Example
{
    public void Main()
    {
        Dictionary<string,ProceduralObjectSettings> proceduralObjectsCollection = new Dictionary<string,ProceduralObjectSettings>();
        
        proceduralObjectsCollection.Add("Car",new ProceduralObjectSettings());
        proceduralObjectsCollection["Car"].AddComponent(typeof(Weight));
        proceduralObjectsCollection["Car"].ComponentSetups["Weight"].propertyGenerationSettings["weight"] = new RandomRangeGenerationType(1200,1700);
        
        proceduralObjectsCollection.Add("Human",new ProceduralObjectSettings());
        proceduralObjectsCollection["Human"].AddComponent(typeof(Weight));
        proceduralObjectsCollection["Human"].ComponentSetups["Weight"].propertyGenerationSettings["weight"] = new NormalDistributionGenerationType(70,4);
        
        proceduralObjectsCollection.Add("1kgDumbbell",new ProceduralObjectSettings());
        proceduralObjectsCollection["1kgDumbbell"].AddComponent(typeof(Weight));
        proceduralObjectsCollection["1kgDumbbell"].ComponentSetups["Weight"].propertyGenerationSettings["weight"] = new FixedGenerationType(1);
    }
}

}

1 Answers1

3

Reflection is slow, but the execution of delegates is fast. Therefore, if you need to execute things obtained by reflection very often, it is good practise to use reflection to create a delegate and that use that delegate.

Creating the delegate is very simple, as long as you know the type of the property and the type that declares the property. The easiest way to get them is to have these types as generic type parameters in an open generic type. Then, you can close the type at runtime (using MakeGenericType on System.Type) and instantiate the closed generic type using System.Activator.CreateInstance. This is of course costly, but you only need to create objects that describe the properties of your models once and then use it as factories for as many instances as you like without any reflection calls.

Edit: this is how it can look like using properties instead of fields, based on your example code If you really want to go with fields (which I would encourage you not to do), creating the delegate is slightly more complex (use the expression compiler or emit IL code), but the principal approach remains the same.

public class ComponentSetup
{
    // Collection of properties of IComponent it represents
    private Dictionary<string, PropertySetter> propertyGenerationSettings = new Dictionary<string, PropertySetter>();
    // Type of IComponent it represents
    public Type t;
    public ComponentSetup( Type t )
    {
        this.t = t;

        //Getting all fields of represented IComponent and adding them to propertyGenerationSettings with default GenerationType
        var fields = t.GetProperties();
        for(int i = 0; i < fields.Length; i++)
        {
            var propertySetterType = typeof( PropertySetter<,> ).MakeGenericType( t, fields[i].PropertyType );
            var setter = (PropertySetter)Activator.CreateInstance( propertySetterType, fields[i] );
            propertyGenerationSettings.Add( fields[i].Name, setter );
        }
    }

    public void SetGenerator<T>( string property, IGenerationType<T> generator )
    {
        propertyGenerationSettings[property].SetGenerator( generator );
    }

    //Generating new component with settings
    public IComponent getGeneratedComponent()
    {
        IComponent toReturn = (IComponent)Activator.CreateInstance( t );

        foreach(var property in propertyGenerationSettings)
        {
            property.Value.Set( toReturn );
        }

        return toReturn;
    }
}

internal abstract class PropertySetter
{
    public abstract void Set( object target );

    public abstract void SetGenerator( object generator );
}

internal class PropertySetter<T, TField> : PropertySetter
{
    private Action<T, TField> setter;
    private IGenerationType<TField> generator;

    public PropertySetter( PropertyInfo property )
    {
        setter = (Action<T, TField>)property.SetMethod.CreateDelegate( typeof( Action<T, TField> ) );
        generator = new EmptyGenerationType<TField>();
    }

    public override void Set( object target )
    {
        if(target is T targetObj)
        {
            setter( targetObj, generator.GetGeneratedValue() );
        }
    }

    public override void SetGenerator( object generator )
    {
        this.generator = (generator as IGenerationType<TField>) ?? this.generator;
    }
}

public interface IGenerationType<T>
{
    T GetGeneratedValue();
}
Georg
  • 5,626
  • 1
  • 23
  • 44
  • I don't really understand your approach. Can you elaborate? –  Jul 13 '20 at 17:34
  • I have one problem with this implementation. I studied it for some time to deeply understand what's going on and there's one thing that's not working properly `setter = (Action)property.SetMethod.CreateDelegate( typeof( Action ) );` CreateDelegate doesn't return Action, it returns Action and then I cannot use it in Set() method, any suggestion what to do with this? –  Jul 17 '20 at 14:23
  • @ArekŻyłkowski I guess you got the type parameter to CreateDelegate incorrect, don't miss the T type parameter! If you call CreateDelegate on instance methods, the CLR allows you to create two different kinds of delegates: One where you pass the instance and one where you don't. In the latter case, you would have to pass the instance as a parameter to CreateDelegate (you would bind the delegate to an object), but in this case, you wouldn't. – Georg Jul 17 '20 at 15:42
  • I'm sorry but I am missing something. For Weight component example I provided (but with weight as property) I use `Action` and property type is `System.Double`In this case I get `ArgumentException: method arguments are incompatible` –  Jul 17 '20 at 18:31
  • @ArekŻyłkowski You are actually right. Strange, it works if Weight is a class but doesnt if Weight is a struct. In that case, you will have to use an expression compiler or reflection emit. – Georg Jul 17 '20 at 18:48
  • I am not really into reflection and only need this for this implemenation. Can you explain how can I get same result? –  Jul 17 '20 at 19:00
  • I solved it with help of this post: https://stackoverflow.com/questions/17660097/is-it-possible-to-speed-this-method-up/17669142#17669142 –  Jul 17 '20 at 20:41