How to point a SerializeField to a scripting file?

I want to make a [SerializeField] point to a scripting file or a custom class. This is so I can set what custom class I want to instantiate on a component on Awake.

For example, the field would point to a LaserGun.cs script file or class, then I would instantiate that LaserGun at in script. Note that the LaserGun.cs is not Monobehavior.

I’ve tried the serialized field a string and just doing a switch case to instantiate the custom class, but that seems clunky. Is there a more elegant way of doing this?

I just tried the most obvious one,

public System.Type tipe;

But alas, was not to be.

Perhaps you could use that, but actually write a custom editor that finagles it around into a string and back and forth for serialization purposes? It would have to use some reflection I imagine… in the end it might be easier to make an enum because those work nicely in the Unity editor, and map those enums to class types with either a switch statement or some data structure that maps the enums to the System.Type.

    public enum MyKlassType
    {
        LaserGun,
        SquirtGun,
        Banana,
    }

    public MyKlassType KlassType;

gives:

6148925--671603--Screen Shot 2020-07-29 at 8.54.01 PM.png

Yes, that’s what my original solution was, but it’s feels a bit like a weird work-around. There are more steps than ideal in adding new types because I’d have to remember where to add it and which nums to map to, so I was hoping for a more elegant solution like SerializeField would provide.

Here’s a quick and dirty solution I just threw together. There are definitely multiple areas it could be improved in, but it does the job.

using System;
using System.Linq;
using System.Reflection;
using UnityEngine;
using JetBrains.Annotations;
#if UNITY_EDITOR
using UnityEditor;
#endif

[BaseTypeRequired(typeof(string))]
[AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
public sealed class ComponentDropdownAttribute : PropertyAttribute
{
    public Type BaseType { get; }

    public ComponentDropdownAttribute()
        : this(typeof(Component))
    {
    }

    public ComponentDropdownAttribute(Type baseType)
    {
        BaseType = baseType;
    }

#if UNITY_EDITOR
    [CustomPropertyDrawer(typeof(ComponentDropdownAttribute))]
    class ComponentDropdownDrawer : PropertyDrawer
    {
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            var types            = TypeCache.GetTypesDerivedFrom(((ComponentDropdownAttribute)attribute).BaseType).ToArray();
            var index            = Array.FindIndex(types, type => GetQualifiedTypeName(type) == property.stringValue);
            index                = EditorGUI.Popup(position, label.text, index, types.Select(type => type.FullName).ToArray());
            property.stringValue = index == -1 ? null : GetQualifiedTypeName(types[index]);
        }

        static string GetQualifiedTypeName(Type type)
        {
            return Assembly.CreateQualifiedName(type.Assembly.GetName().Name, type.FullName);
        }
    }
#endif
}

class TypeReferenceExample : MonoBehaviour
{
    [SerializeField, ComponentDropdown]
    string componentTypeToAdd;

    void Awake()
    {
        var type = Type.GetType(componentTypeToAdd);
        Debug.LogFormat("Adding component of type {0}.", type.FullName);
        gameObject.AddComponent(type);
    }
}

Edit: I just remembered that the MonoScript class exists, so if you’re fine with only being able to reference script assets (and not classes that could be inside a dll) you can do this:

using System;
using UnityEngine;
#if UNITY_EDITOR
using UnityEditor;
#endif

[Serializable]
public class ScriptReference : ISerializationCallbackReceiver
{
#if UNITY_EDITOR
    [SerializeField]
    MonoScript scriptAsset;
#endif

    [SerializeField]
    string typeName;

    public Type ScriptType { get; private set; }

    void ISerializationCallbackReceiver.OnAfterDeserialize()
    {
        if (string.IsNullOrEmpty(typeName))
            ScriptType = null;
        else
            ScriptType = Type.GetType(typeName);
    }

    void ISerializationCallbackReceiver.OnBeforeSerialize()
    {
    #if UNITY_EDITOR
        if (scriptAsset)
            typeName = scriptAsset.GetClass()?.AssemblyQualifiedName;
        else
            typeName = null;
    #endif
    }

#if UNITY_EDITOR
    [CustomPropertyDrawer(typeof(ScriptReference))]
    class ScriptReferenceDrawer : PropertyDrawer
    {
        public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
        {
            EditorGUI.ObjectField(position, property.FindPropertyRelative(nameof(scriptAsset)), label);
        }
    }
#endif
}

The monoscript solution seems to work great so far, thanks!

As an aside, how would I check for a specific Class Type in Unity Editor in order to allow only a certain type to be set in the field?

I would also like to know if this is possible

I don’t think there is much of a need to do that. After all if it is just one specific type and you know what that type is, why not just hardcode it directly in the first place? I’d probably just store a static readonly variable with the full assembly name of the datatype and use that to instantiate it at runtime. No need to get the editor and serialization involved.

Now if you are trying to filter out so that only a particular range of types are usable, then it gets tricky. I have a library on github that provides a special data type that is exposed to the editor and allows for all datatypes to be displayed in a folding drop-down menu. It’s very easy at runtime to call a function on this type and instantiate a new value. It might be possible to use this as a jumping board. You could probably create another attribute that allows listing and collection of names as filters. Then you could modify the property drawer in my code to look for that attribute and only display a type if it matches that filter list. T

The github is below. Take note that it has a couple of dependencies which are listed on the readme but I’ve included their links below as well. They are all custom packages so you’ll need to put them in your project’s packages folder instead of the asset folder. Take note that I have a kinda hard-coded functionality for my needs: In particular, if there are multiple constructors, it ALWAYS chooses the constructor with the greatest number of parameters. And it only supports serializable types for those parameters (for obvious reasons).
https://github.com/Slugronaut/Toolbox-InstantiableType
https://github.com/Slugronaut/Toolbox-TypeHelper
https://github.com/Slugronaut/Toolbox-EditorGUIExtensions

EDIT: I just realized that link actually has a dependecy to Odin as well. So if you don’t have that you will have to replace the serialization functions either with Unity’s built in one or consider looking in to using FullSerializer’s instead.

If you’re just using Odin’s serialiser, that’s open source so you’re good.

Though a lot of the above just seems to be a use case for SerializeReference. You can, technically, serialise any plain C# reference type as it supports System.object, not that you probably should be using object in any significant capacity.