Search Unity

  1. Welcome to the Unity Forums! Please take the time to read our Code of Conduct to familiarize yourself with the forum rules and how to post constructively.
  2. Dismiss Notice

How to point a SerializeField to a scripting file?

Discussion in 'Scripting' started by doogog, Jul 30, 2020.

  1. doogog

    doogog

    Joined:
    Oct 23, 2018
    Posts:
    9
    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?
     
  2. Kurt-Dekker

    Kurt-Dekker

    Joined:
    Mar 16, 2013
    Posts:
    36,762
    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.

    Code (csharp):
    1.     public enum MyKlassType
    2.     {
    3.         LaserGun,
    4.         SquirtGun,
    5.         Banana,
    6.     }
    7.  
    8.     public MyKlassType KlassType;
    gives:

    Screen Shot 2020-07-29 at 8.54.01 PM.png
     
  3. doogog

    doogog

    Joined:
    Oct 23, 2018
    Posts:
    9
    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.
     
  4. TheZombieKiller

    TheZombieKiller

    Joined:
    Feb 8, 2013
    Posts:
    258
    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.

    Code (CSharp):
    1. using System;
    2. using System.Linq;
    3. using System.Reflection;
    4. using UnityEngine;
    5. using JetBrains.Annotations;
    6. #if UNITY_EDITOR
    7. using UnityEditor;
    8. #endif
    9.  
    10. [BaseTypeRequired(typeof(string))]
    11. [AttributeUsage(AttributeTargets.Field, AllowMultiple = false)]
    12. public sealed class ComponentDropdownAttribute : PropertyAttribute
    13. {
    14.     public Type BaseType { get; }
    15.  
    16.     public ComponentDropdownAttribute()
    17.         : this(typeof(Component))
    18.     {
    19.     }
    20.  
    21.     public ComponentDropdownAttribute(Type baseType)
    22.     {
    23.         BaseType = baseType;
    24.     }
    25.  
    26. #if UNITY_EDITOR
    27.     [CustomPropertyDrawer(typeof(ComponentDropdownAttribute))]
    28.     class ComponentDropdownDrawer : PropertyDrawer
    29.     {
    30.         public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    31.         {
    32.             var types            = TypeCache.GetTypesDerivedFrom(((ComponentDropdownAttribute)attribute).BaseType).ToArray();
    33.             var index            = Array.FindIndex(types, type => GetQualifiedTypeName(type) == property.stringValue);
    34.             index                = EditorGUI.Popup(position, label.text, index, types.Select(type => type.FullName).ToArray());
    35.             property.stringValue = index == -1 ? null : GetQualifiedTypeName(types[index]);
    36.         }
    37.  
    38.         static string GetQualifiedTypeName(Type type)
    39.         {
    40.             return Assembly.CreateQualifiedName(type.Assembly.GetName().Name, type.FullName);
    41.         }
    42.     }
    43. #endif
    44. }
    45.  
    46. class TypeReferenceExample : MonoBehaviour
    47. {
    48.     [SerializeField, ComponentDropdown]
    49.     string componentTypeToAdd;
    50.  
    51.     void Awake()
    52.     {
    53.         var type = Type.GetType(componentTypeToAdd);
    54.         Debug.LogFormat("Adding component of type {0}.", type.FullName);
    55.         gameObject.AddComponent(type);
    56.     }
    57. }
    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:
    Code (CSharp):
    1. using System;
    2. using UnityEngine;
    3. #if UNITY_EDITOR
    4. using UnityEditor;
    5. #endif
    6.  
    7. [Serializable]
    8. public class ScriptReference : ISerializationCallbackReceiver
    9. {
    10. #if UNITY_EDITOR
    11.     [SerializeField]
    12.     MonoScript scriptAsset;
    13. #endif
    14.  
    15.     [SerializeField]
    16.     string typeName;
    17.  
    18.     public Type ScriptType { get; private set; }
    19.  
    20.     void ISerializationCallbackReceiver.OnAfterDeserialize()
    21.     {
    22.         if (string.IsNullOrEmpty(typeName))
    23.             ScriptType = null;
    24.         else
    25.             ScriptType = Type.GetType(typeName);
    26.     }
    27.  
    28.     void ISerializationCallbackReceiver.OnBeforeSerialize()
    29.     {
    30.     #if UNITY_EDITOR
    31.         if (scriptAsset)
    32.             typeName = scriptAsset.GetClass()?.AssemblyQualifiedName;
    33.         else
    34.             typeName = null;
    35.     #endif
    36.     }
    37.  
    38. #if UNITY_EDITOR
    39.     [CustomPropertyDrawer(typeof(ScriptReference))]
    40.     class ScriptReferenceDrawer : PropertyDrawer
    41.     {
    42.         public override void OnGUI(Rect position, SerializedProperty property, GUIContent label)
    43.         {
    44.             EditorGUI.ObjectField(position, property.FindPropertyRelative(nameof(scriptAsset)), label);
    45.         }
    46.     }
    47. #endif
    48. }
     
    Last edited: Jul 30, 2020
  5. doogog

    doogog

    Joined:
    Oct 23, 2018
    Posts:
    9
    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?
     
    Last edited: Aug 1, 2020
  6. Karto_

    Karto_

    Joined:
    Oct 14, 2018
    Posts:
    3
    I would also like to know if this is possible
     
  7. Sluggy

    Sluggy

    Joined:
    Nov 27, 2012
    Posts:
    841
    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
     
  8. Sluggy

    Sluggy

    Joined:
    Nov 27, 2012
    Posts:
    841
    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.
     
  9. spiney199

    spiney199

    Joined:
    Feb 11, 2021
    Posts:
    5,895
    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.