🤔 Reflection and ScriptableObject, runtime null ref Exception

I am trying to make a class helper to create dynamically objects using a ScriptableObject as config file for parameters needed for the constructor of each object I want to create. At runtime the GetType() returns a valid type/object ref if inspected then generates errors internally see the pictures. I’ll be grateful if anyone can point me to where or how to diagnose this issue.

public static T ParseProperties<T>(ScriptableObject configSO, string objectName, params string[] propertyNames) where T : class
		{
			if (configSO == null) return null;


			Type configType = configSO.GetType();
			Type targetType = typeof(T);

			if (string.IsNullOrEmpty(objectName)) return null;

			// Create a dictionary to hold parsed property values
			Dictionary<string, object> parsedValues = new();

			foreach (var propName in propertyNames)
			{
				//var propInfo = configSO.GetType().GetField(propName);
				var propInfo = configType.GetField(propName);
				if (propInfo != null)
				{
					parsedValues.Add(propName, propInfo.GetValue(configSO));
				}
			}

			// Create an object of the target type dynamically
			if (targetType.Name.Equals(objectName))
			{

				return CreateObject(targetType, parsedValues) as T;

			}
			return null;
		}


		private static object CreateObject(Type targetType, Dictionary<string, object> parsedValues)
		{
			// Check for a constructor that matches all the parsed values
			ConstructorInfo constructor = null;

			var constructors = targetType.GetConstructors();
			foreach (var ctor in constructors)
			{
				var parameters = ctor.GetParameters();
				if (parameters.Length == parsedValues.Count)
				{
					bool match = true;
					foreach (var parameter in parameters)
					{
						if (!parsedValues.ContainsKey(parameter.Name))
						{
							match = false;
							break;
						}
					}
					if (match)
					{
						constructor = ctor;
						break;
					}

				}
			}

			if (constructor == null)
			{
				//If no constructor match the property, use an object creation without params
				return Activator.CreateInstance(targetType);
			}
			else
			{
				List<object> paramValues = new();
				foreach (var parameter in constructor.GetParameters())
				{
					paramValues.Add(parsedValues[parameter.Name]);
				}

				return constructor.Invoke(paramValues.ToArray());

			}

		}
	}

AFAIK this just bubbles down ultimately to new on the type you selected.

Using new to create MonoBehaviours or ScriptableObjects is not supported.

Doing so will create a dead half-object that has no corresponding engine side backing, giving you null references.

Instead you must create the MonoBehaviour using AddComponent() on a GameObject instance, or use ScriptableObject.CreateInstance() to make your ScriptableObject.

3 Likes

Yes, like Kurt said, if the object you try to create is a MonoBehaviour or ScriptableObject, you can not use the Activator. So assuming that your MonoBehaviours and ScriptableObject do not have explicit constructors (which they shouldn’t) you need to change this part:

if (constructor == null)
{
    //If no constructor match the property, use an object creation without params
    return Activator.CreateInstance(targetType);
}

to something like this:

if (constructor == null)
{
    //If no constructor match the property, use an object creation without params
    if (typeof(ScriptableObject).IsAssignableFrom(targetType))
        return ScriptableObject.Create(targetType);
    if (typeof(Component).IsAssignableFrom(targetType))
        return (new GameObject("whatever")).AddComponent(targetType);
    return Activator.CreateInstance(targetType);
}

Of course if the intention is to create components this way, you would most likely run into some context issues. In this case I simply create a new gameobject for the component. Maybe it’s possible to pass in a context object for such cases, that’s up to your needs.

ps: Haven’t syntax checked my snippet.

1 Like

Sorry for the confusion, this part is just a safety net if I forget something :stuck_out_tongue: what the class creates are pure C# classes the ScriptableObject just holds the default values for the ctor of these classes.

just posted this for an overview of the class

return Activator.CreateInstance(targetType);

this is the issue whatever I try Field or Property even runtime versions propInfo is always null
using binding Attributes no change.

Regarding the Internal error on IDE it is related to the fact that the SO has no generic method declared.
I implemented a generic GetValue later and somehow the error in the picture stopped and I could at least inspect the SO while debugging.
It feels like ScriptableObject doesn’t support reflection or Type.GetField / Type.GetProperty.

Well, you haven’t really shown any example of the classes we talk about. Are you sure the fields are actually public fields? When you use GetField without any binding flags, it can only access public fields. Can you show an example class that you try to read the data from and what string(s) you actually pass into “propertyNames”?

1 Like

Sure,
Here is a call example

moveIt = ParseProperties<MovementManager>(configSO, "MovementManager", "playerGObject", "animator", "characterController", "MoveSpeed", "isSprinting", "SprintSpeed", "RotationSmoothTime", "SpeedChangeRate", "JumpHeight", "Gravity", "JumpTimeout", "FallTimeout", "Grounded", "GroundedOffset", "GroundedRadius", "GroundLayers", "_cinemachineTargetYaw", "_cinemachineTargetPitch", "analogMovement");

The C# class constructor

public MovementManager(GameObject playerGobject, Animator animator, CharacterController characterController, float moveSpeed, bool issprinting, float sprintSpeed, float rotationSmoothTime,
             float speedChangeRate, float jumpHeight, float gravity, float jumpTimeout, float fallTimeout, bool grounded, float groundedOffset, float groundedRadius, LayerMask groundLayers,
             float cinemachineTargetYaw, float cinemachineTargetPitch,
            bool isAnalog = false)

Ok :slight_smile: but you try to read the fields of your configSO. So that’s the thing we need to see. Are those fields actual fields (not properties) and are they all public? If they are not fields (auto properties with a serializedfield attached) or are private, you need to use proper BindingFlags. Note that you should never use auto properties when you need to access the fields as the automatically generated backing field has a horribly mangled name. So we need to see how those fields look like. How are they declared inside your configSO.

1 Like

Why, oh why, aren’t you just using one of the established methods to instantiate classes from a config file? :wink:

For instance, you can use Json to serialize and deserialize regular C# classes with ease and reliability. You don’t need to come up with your own system to do so.

If need be, you can edit all these values in a ScriptableObject and then serialize to json and at runtime deserialize from the same json.

To make the values editable in the Inspector you’d use the same class:

public class Data : ScriptableObject
{
    public MovementClass Movement;
}

[Serializable]
public class MovementClass
{
    // the movement fields here
}

Now on top you also seem to be injecting references like Animator, CharacterController to these instances. You can’t serialize those and you also can’t reference them in the ScriptableObject (assuming they are in-scene references).

To inject these you’d either use a Dependency Injection framework or simply create your own Dependency Lookup class the instantiated class gets its references from.

Either way, the system you are building here seems like reinventing the wheel except your wheels are ellipsoid. :slight_smile:

1 Like

why o why would you make another GameEngine there was CryEngine, UnrealEngine…?
Well I was more interested as why with ScriptableObject I have an issue with GetType and GetField.
as per the wheel, I have many C# classes/ScriptableObjects that I need to instantiate and I don’t want some inconveniences

  1. public or serialized private field for each in my MonoBehaviour
  2. setting up fields in the main MonoBehaviour can get up to 30 variables cause whith your approach a long trip to get them back at runtime and no way to add them at design time.
  3. I use the ScriptableObject to setup the default state after that code handle itself / change whatever it needs
  4. I am not injecting dependencies for this part it’ll be overkill / not efficient
  5. all the references get to where they need to be just the retrieval of the SO fields causes me the headache now
  6. all field are similar to this for them to be edited in the inspector before I was using [SerializeField] and private to make it simple.
[Tooltip("Move speed of the character in m/s")]
        public float MoveSpeed = 2.0f;
        [Tooltip("Sprint action triggered")]
        public bool isSprinting = false;

Yeah not sure why any of this is required. Scriptable object you can just Instantiate them. Regular serializable classes you can make a serialisation copy with. Using reflection is way overkill.

I still think you have your head wrapped around a solution that you designed but you haven’t really explored all the available options / alternatives.

On top we don’t have enough info to understand the issue at hand, let alone why you are trying to do so. May be a language barrier.

Could you explain what you mean by that? I think this might reveal the cornerstone misconception.

If I need a ScriptableObject to hold design-time values that a MonoBehaviour needs to use, then first of all, you’d split these up into readonly, design-time only values and runtime values. Thats two classes or structs, the first goes into the ScriptableObject. The MonoBehaviour references that SO and also instantiates - with default values - the runtime values class/struct. If the default runtime values are based on the design-time values, you pass in those design time values in the ctor or an Init method.

A workable alternative if you never need to “reset” the values at runtime is to not make the split. Instead, the SO only holds an instance of the values class, and the MB references the SO and simply clones that class instance the SO holds in order to make sure the instance can be used independently by several MBs.

Cloning (or copying) a regular C# class is a common concept, implemented by the ICloneable interface.

What I mean is this main MonoBehaviour → uses internally 10 classes + components like Animator → Factory Class relies on → ParseProperty to parse the Fields/Values pairs from 10 SriptableObject to instantiate all theobject needed by the main MonoBehavior.

SOs hold many fields and many types trying to automate that in case I have to expand, modify or delete. no need to update the calls or refactor anything

that in itself isn’t relevant to the topic, I just posted the hole code regarding issue in case I may foresaw something a different perspective is always welcome. My only issue is this line of code involving a ScriptableObject

Fear not I am well aware of the established ways, been excited same as everybody after Ryan Hipple’s 2017 Unite talk. Actually, after that I started exploring ways the ScriptableObject Architecture I am curious and can’t wrap my mind around the fact that this line fails where it shouldn’t that’s the only answer I am seek at this point.

thank you in advance, I think in terms of language skills my fault may be to skip some letters while typing otherwise I am using very clear simple sentences that shows my intent. I am not trying to prove a point it is out of topic anyway.

This call was refactored

The new version needs only the configSO and the class to instantiate

 oveIt = ParseProperties<BSN.LLV.CC.MovementManager>(configSO, "MovementManager"); 

Try specifying binding flags.

Almost certainly unrelated. You can try your code with a simple class.

I ask in my second post if the fields are public fields. You gave two examples in that followup which are public fields. However in the sentence I quoted it seems you made them or some private and used SerializeField on them? If that is the case, yes, of course GetField would fail. GetField by default obeys the usual visibility rules. So you can only access public fields as I mentioned. If you want to also access private fields you have to supply BindingFlags to the GetField call. In your case you probably want to use

(BindingFlags.Instance | BindingFlags.Public | BindingFlags.NonPublic)

This would get any instance fields (so no static fields) that are either public or private. The BindingFlags are one of the most central concepts when it comes to reflection as they allow to bypass the usual visibility rules.

It would really help when you could actually share the part of your code you have issues with. And I don’t mean the reflection part, but the part it tries to reflect on. So the actual class and how the fields are declared and which field specifically you have issues with. This is debugging 101. You know which field causes troubles, so check how that field is declared.

I can assure you that reflection does work as it should, no matter if it’s an ordinary class, a ScriptableObject or a MonoBehaviour. I actually have my debug tools as a DLL which I can inject into any Unity game through a mono injector and I can inspect all the instances with reflection just fine :slight_smile:

I did just for fun and it works as intended retrieves the fields creates the object all good see picture

you can see the classes declaration, visibility of the fields, the inspection through expression watch
it is the same manner as per the real classes and config ScriptableObject :slight_smile:

note:updated printSreen quality

here look I working with basic stuff this is the movementConfig SO all public, editable on the inspector no issue here


[CreateAssetMenu(fileName = "MovementManagerConfig", menuName = "Configs/MovementManagerConfig", order =3)]
    public class MovementManagerConfig : ScriptableObject
    {
        [Header("Player")]

        [Tooltip("Move speed of the character in m/s")]
        [SerializeField]
        public float MoveSpeed = 2.0f;

        [Tooltip("Sprint action triggered")]
        [SerializeField]
        public bool isSprinting = false;
        
        [Tooltip("Sprint speed of the character in m/s")]
        [SerializeField]
        public float SprintSpeed = 5.335f;

        [Tooltip("How fast the character turns to face movement direction")]
        [Range(0.0f, 0.3f)]
        public float RotationSmoothTime = 0.12f;

        [Tooltip("Acceleration and deceleration")]
        public float SpeedChangeRate = 10.0f;

        public AudioClip LandingAudioClip;
        public AudioClip[] FootstepAudioClips;
        [Range(0, 1)] public float FootstepAudioVolume = 0.5f;

        [Space(10)]
        [Tooltip("The height the player can jump")]
        public float JumpHeight = 1.2f;

        [Tooltip("The character uses its own gravity value. The engine default is -9.81f")]
        public float Gravity = -15.0f;

        [Space(10)]
        [Tooltip("Time required to pass before being able to jump again. Set to 0f to instantly jump again")]
        public float JumpTimeout = 0.50f;

        [Tooltip("Time required to pass before entering the fall state. Useful for walking down stairs")]
        public float FallTimeout = 0.15f;

        [Header("Player Grounded")]
        [Tooltip("If the character is grounded or not. Not part of the CharacterController built in grounded check")]
        public bool Grounded = true;

        [Tooltip("Useful for rough ground")]
        public float GroundedOffset = -0.14f;

        [Tooltip("The radius of the grounded check. Should match the radius of the CharacterController")]
        public float GroundedRadius = 0.28f;

        [Tooltip("What layers the character uses as ground")]
        public LayerMask GroundLayers;

    }
 

Here’s the culprit :exploding_head: :weary:

I guess while frantically copy, paste do undo pramas/fields, I ended Up loosing sync so for example in the constructor version of the params moveSpeed is used where as I updated all fields to use CamelCase style.

Now, I can extract the fields create the object and more over got some new insights from your feedback Thank you all for your patience with me :heart_eyes: