How do I store a component type in a variable?

I want to be able to have a public variable in inspector where I can hold components. I don’t want a reference to a specific component on a specific object, I want a reference to a component type.

For example, If I want to check if an object has a component, I would do as below:

    public Component componentVar;

    private void Start()
    {
        if (gameObject.GetComponent<componentVar>())
        {
            //do stuff
        }
    }

The Component class holds references to specific components on objects, not the type. I would like for componentVar to be able to hold any component type, e.g. BoxCollider or Transform.

I also have a second problem where GetComponent doesn’t recognise componentVar, and doesn’t seem to work with any variables of any kind afaik. I’m unsure what to do about that as well. I would like to use ComponentVar as a variable for GetComponent specifically, so I need that to be working as well… So this is kinda two questions.

Apologies for any misuse of terminology, any help is appreciated

I don’t believe there’s a built-in way to serialize a type, but the Addressables package has the SerializedType struct which you can use as a field in your component along with SerializedTypeRestrictionAttribute which you can use to restrict the field’s accepted types to subclasses of Component. Once you have a type, you just need to use the non-generic overload of GetComponent or TryGetComponent.

[SerializedTypeRestriction(type = typeof(Component))]
public SerializedType componentType;

private void Start()
{
  Type componentTypeValue = componentType.Value;
  if (componentTypeValue != null
      && TryGetComponent(componentTypeValue, out Component component))
  {

  }
}

Edit: looking around the internet a bit, I also see this code by Bunny83 right here on the Unity forum. That should handle the whole serialization bit without needing a big package. Not sure about the editor UI aspect of this, Addressables itself uses a bunch of IMGUI stuff to get it done

2 Likes

Thanks! I’ve got it working, unfortunately there’s new problems related to it. I wanted to use this variable inside an array of a nested class, but this throws errors and doesn’t show in the inspector. It breaks some of the other UI as well, any component below is missing the label, but can still be opened and edited. The “add component” button also goes missing. And of course, the variable itself is not visible. The image below should show what I mean.
9835980--1414983--Screenshot 2024-05-15 171929.png
The gap between “Element 0” and “parent” is where it has been declared, but it doesn’t show, and “Script GV” is the start of a new component.

I tried to get around this by using an array instead, but it still wouldn’t work. Image below.
9835980--1414986--Screenshot 2024-05-15 172056.png
This is set in the main class, not nested.

I then tried to see if it would work if the nested class was not in an array, but it did not work.
9835980--1414989--Screenshot 2024-05-15 171704.png
Again, the gap is where it should appear.

It works fine when the variable is set not nested, and not an array. That is exactly what I asked for, however I didn’t anticipate that it wouldn’t work in some situations. I need it to work in the first case, In a nested class within an array. The other two were for testing, to try and solve the issue, but I don’t know how to fix it, so I’m hoping someone here can help.

Here is my current code to declare this array of nested classes, if it helps:

public class Manager : MonoBehaviour
{
    [System.Serializable]
    public class CollectOptions
    {
        [SerializedTypeRestriction(type = typeof(Component))]
        public SerializedType componentType;

        public GameObject parent = null;
        public string tag;
    }
}

There is more code within the class Manager, but that’s removed here for convenience.

And of course, the error in question, is:
NullReferenceException: Object reference not set to an instance of an object
UnityEditor.AddressableAssets.GUI.SerializedTypeDrawer.OnGUIMultiple (UnityEngine.Rect position, UnityEditor.SerializedProperty property, UnityEngine.GUIContent label, System.Boolean showMixed)
It throws twice.

When clicked, it takes me to this:

foreach (var type in m_Types)
                typeContent.Add(new GUIContent(AddressableAssetUtility.GetCachedTypeDisplayName(type), ""));

The first line has the error, this is in SerializedTypeDrawer, part of the Addressables package. Worth noting I know nothing about the Addressables package so if that’s the solution, please keep that in mind.

It also caused a stack overflow when I kept interacting with the UI, maybe wasn’t the smartest move.

I’m unsure what to do and am giving as much info as i can about the issue in the hopes that someone has a solution to this. I’m hoping its something silly because I’m not experienced with this type of thing.

That seems quite buggy, not what I expected from a first-party package… Which version of Unity are you using, just so we’re on the same page?

Also, I tried my hand at implementing a UI for Bunny’s SerializableType. It’s tweaked to use strings for UI Toolkit bindability and generics for filtering which types can be assigned. It seems to work well enough on my end when using it in an array (and a list) and multi-selecting, on Unity 6000.0.1f1. Maybe this works a little better?

Usage example:

    public SerializableType<Component> serializableType;
    void Start()
    {
        Debug.Log(serializableType.type);
    }

Code:

using System;
using System.Buffers;
using System.Collections;
using System.Collections.Generic;
using System.IO;
using System.Linq;
#if UNITY_EDITOR
using UnityEditor;
using UnityEditor.UIElements;
#endif
using UnityEngine;
using UnityEngine.UIElements;

[Serializable]
public struct SerializableType<T>
{
    public Type type
    {
        get
        {
            return SerializableTypeUtility.ReadFromString(data);
        }
        set
        {
            data = SerializableTypeUtility.WriteToString(value);
        }

    }

    public string data;
}

public static class SerializableTypeUtility
{
    // Type serialization from https://discussions.unity.com/t/508053/10
    public static Type Read(BinaryReader aReader)
    {
        var paramCount = aReader.ReadByte();
        if (paramCount == 0xFF)
            return null;
        var typeName = aReader.ReadString();
        var type = System.Type.GetType(typeName);
        if (type == null)
            throw new Exception("Can't find type; '" + typeName + "'");
        if (type.IsGenericTypeDefinition && paramCount > 0)
        {
            var p = new Type[paramCount];
            for (int i = 0; i < paramCount; i++)
            {
                p[i] = Read(aReader);
            }
            type = type.MakeGenericType(p);
        }
        return type;
    }

    public static Type ReadFromString(string text)
    {
        int n = (int)((long)text.Length * 3 / 4);
        if (string.IsNullOrWhiteSpace(text))
        {
            return null;
        }
        byte[] tmp = ArrayPool<byte>.Shared.Rent(n);
        try
        {

            if (!Convert.TryFromBase64String(text, tmp, out int nActual))
            {
                return null;
            }
            using (var stream = new MemoryStream(tmp, 0, nActual))
            using (var r = new BinaryReader(stream))
            {
                return Read(r);
            }
        }
        finally
        {
            ArrayPool<byte>.Shared.Return(tmp);
        }
    }

    public static string WriteToString(Type type)
    {
        using (var stream = new MemoryStream())
        using (var w = new BinaryWriter(stream))
        {
            Write(w, type);
            return Convert.ToBase64String(stream.ToArray());
        }
    }

    public static void Write(BinaryWriter aWriter, Type aType)
    {
        if (aType == null)
        {
            aWriter.Write((byte)0xFF);
            return;
        }
        if (aType.IsGenericType)
        {
            var t = aType.GetGenericTypeDefinition();
            var p = aType.GetGenericArguments();
            aWriter.Write((byte)p.Length);
            aWriter.Write(t.AssemblyQualifiedName);
            for (int i = 0; i < p.Length; i++)
            {
                Write(aWriter, p[i]);
            }
            return;
        }
        aWriter.Write((byte)0);
        aWriter.Write(aType.AssemblyQualifiedName);
    }
}

#if UNITY_INCLUDE_TESTS
internal class Tests
{
    private static readonly Type[] s_types =
        {
            typeof(int),
            typeof(List<UnityEngine.Object>),
            typeof(Dictionary<UnityEngine.Object, HashSet<Renderer>>),
            typeof(Component[])
        };

    [NUnit.Framework.Test]
    public void Conversion_Success([NUnit.Framework.ValueSource(nameof(s_types))] Type type)
    {
        var converted = SerializableTypeUtility.WriteToString(type);
        NUnit.Framework.Assert.That(SerializableTypeUtility.ReadFromString(converted), NUnit.Framework.Is.EqualTo(type));
    }
}
#endif

#if UNITY_EDITOR
[CustomPropertyDrawer(typeof(SerializableType<>), true)]
internal class SerializableTypePropertyDrawer : PropertyDrawer
{
    public override VisualElement CreatePropertyGUI(SerializedProperty property)
    {
        var targetObjectType = DetermineTargetType(fieldInfo.FieldType);
        var field = new SerializableTypeField(targetObjectType, property.displayName);
        var targetProperty = property.FindPropertyRelative("data");
        field.TrackPropertyValue(targetProperty, field.UpdateDisplay);
        field.BindProperty(targetProperty);
        field.UpdateDisplay(targetProperty);
        return field;
    }

    private static Type GetElementType(Type t)
    {
        if (t.IsGenericType)
        {
            return t.GetGenericArguments()[0];
        }
        return t;
    }

    private static Type DetermineTargetType(Type t)
    {
        if (typeof(IEnumerable).IsAssignableFrom(t) && t.IsGenericType)
        {
            return GetElementType(t.GetGenericArguments()[0]);
        }
        if (t.IsArray)
        {
            return GetElementType(t.GetElementType());
        }
        return GetElementType(t);
    }
}

internal partial class SerializableTypeField : BaseField<string>
{
    private readonly VisualElement _content;
    private readonly TextField _textField;
    private readonly Button _selectButton;
    private readonly TypeSelectorPopupWindowContent _typeSelectorPopupWindowContent;
    private readonly List<Type> _filteredTypes;

    public SerializableTypeField(Type filterType) : this(filterType, null)
    {
    }

    public SerializableTypeField(Type filterType, string label) : this(filterType, label, new VisualElement())
    {
    }

    public SerializableTypeField(Type filterType, string label, VisualElement visualInput) : base(label, visualInput)
    {
        AddToClassList(alignedFieldUssClassName);
        _content = visualInput;
        _content.style.flexDirection = FlexDirection.Row;
        _content.Add(_textField = new TextField() { style = { marginBottom = 0, marginLeft = 0, marginRight = 0, marginTop = 0, flexShrink = 1, flexGrow = 1 }, enabledSelf = false });
        _content.Add(_selectButton = new Button(OpenSelector) { style = { marginBottom = 0, marginLeft = 0, marginRight = 0, marginTop = 0 }, text = ">" });
        _typeSelectorPopupWindowContent = new TypeSelectorPopupWindowContent(this);
        _filteredTypes = TypeCache.GetTypesDerivedFrom(filterType).OrderBy(static s => s.FullName, StringComparer.InvariantCulture).ToList();
    }

    protected override void UpdateMixedValueContent()
    {
        UpdateDisplay("\u2014");
        //_selectButton.SetEnabled(false);
    }

    internal void UpdateDisplay(SerializedProperty serializedProperty)
    {
        UpdateDisplay(SerializableTypeUtility.ReadFromString(serializedProperty.stringValue));
        //_selectButton.SetEnabled(true);
    }

    private void UpdateDisplay(Type type)
    {
        if (type != null)
        {
            UpdateDisplay(GetFormattedType(type));
        }
        else
        {
            UpdateDisplay("Not Set");
        }

    }
    private void UpdateDisplay(string text)
    {
        _textField.value = text;
    }

    internal static string GetFormattedType(Type t)
    {
        return $"{t.Name} ({t.FullName})";
    }

    public List<Type> GetFilteredTypes()
    {
        return _filteredTypes;
    }

    private void OpenSelector()
    {
        UnityEditor.PopupWindow.Show(worldBound, _typeSelectorPopupWindowContent);
    }

    private class TypeSelectorPopupWindowContent : PopupWindowContent
    {
        private const int itemHeight = 18;
        private readonly SerializableTypeField _field;
        private readonly Toolbar _toolbar;
        private readonly ToolbarSearchField _search;
        private readonly ListView _listView;
        private List<Type> _types;
        private bool _pauseSelectionCheck;

        internal TypeSelectorPopupWindowContent(SerializableTypeField field)
        {
            _field = field;
            _listView = new ListView
            {
                reorderable = false,
                selectionType = SelectionType.Single,
                showAlternatingRowBackgrounds = AlternatingRowBackground.All,
                virtualizationMethod = CollectionVirtualizationMethod.FixedHeight,
                fixedItemHeight = itemHeight
            };
            _listView.makeItem = () => new Label();
            _listView.bindItem = (v, i) =>
            {
                Type t = ((IReadOnlyList<Type>)_listView.itemsSource)[i];
                ((Label)v).text = GetFormattedType(t);
            };
            _listView.selectionChanged += (s) =>
            {
                SetFromSelection(s);
            };
            _listView.itemsChosen += (s) =>
            {
                SetFromSelection(s);
                editorWindow.Close();
            };
            _toolbar = new Toolbar();
            _search = new ToolbarSearchField();
            var searchState = new DelayedSearchController(_search);
            searchState.SearchTextChanged += s => { SetFilter(s); };
            _toolbar.Add(_search);
        }

        private void SetFromSelection(IEnumerable<object> selection)
        {
            if (_pauseSelectionCheck)
            {
                return;
            }
            Type type = selection.FirstOrDefault() as Type;
            _field.value = SerializableTypeUtility.WriteToString(type);
        }

        private void SetList(List<Type> types)
        {
            _listView.itemsSource = types;
            int index = types.IndexOf(SerializableTypeUtility.ReadFromString(_field.value));
            if (index >= 0)
            {
                _listView.SetSelection(index);
            }
            else
            {
                _listView.ClearSelection();
            }
        }

        private void SetFilter(string filter)
        {
            _pauseSelectionCheck = true;
            if (string.IsNullOrEmpty(filter))
            {
                SetList(_types);
            }
            else
            {
                List<Type> filtered = new();
                foreach (var type in _types)
                {
                    if (type.FullName.Contains(filter, StringComparison.InvariantCultureIgnoreCase))
                    {
                        filtered.Add(type);
                    }
                }
                SetList(filtered);
            }
            _pauseSelectionCheck = false;
        }

        public override Vector2 GetWindowSize()
        {
            float width = Math.Max(200, _field.resolvedStyle.width);
            return new Vector2(width, 500);
        }

        public override void OnOpen()
        {
            editorWindow.rootVisualElement.Clear();
            editorWindow.rootVisualElement.Add(_toolbar);
            editorWindow.rootVisualElement.Add(_listView);
            _types = _field.GetFilteredTypes();
            SetList(_types);
            editorWindow.rootVisualElement.RegisterCallback<NavigationCancelEvent>(HandleNavigationCancelEvent);
            editorWindow.rootVisualElement.schedule.Execute(() =>
            {
                _search.Focus();
            });
        }

        public override void OnClose()
        {
            editorWindow.rootVisualElement.UnregisterCallback<NavigationCancelEvent>(HandleNavigationCancelEvent);
        }

        private void HandleNavigationCancelEvent(NavigationCancelEvent evt)
        {
            editorWindow.Close();
        }

        private class DelayedSearchController : IDisposable
        {
            private const int DelayMs = 300;

            public event Action<string> SearchTextChanged;

            private readonly VisualElement _search;
            private readonly int _delayMs;
            private string _activeText;
            private readonly IVisualElementScheduledItem _scheduledItem;
            private bool _disposed;

            public DelayedSearchController(VisualElement search, int delayMs = DelayMs)
            {
                _search = search;
                _delayMs = delayMs;
                _activeText = "";
                _scheduledItem = null;
                search.RegisterCallback<ChangeEvent<string>>(OnSearchChanged);
                search.RegisterCallback<NavigationSubmitEvent>(OnSearchEnter);
                _scheduledItem = search.schedule.Execute(UpdateItem);
                _scheduledItem.Pause();
            }

            public void Dispose()
            {
                if (_disposed)
                {
                    return;
                }
                _disposed = true;
                _search.UnregisterCallback<ChangeEvent<string>>(OnSearchChanged);
                _search.UnregisterCallback<NavigationSubmitEvent>(OnSearchEnter);
                _scheduledItem.Pause();
            }

            private void OnSearchEnter(NavigationSubmitEvent evt)
            {
                if (_disposed)
                {
                    return;
                }
                _scheduledItem?.ExecuteLater(0);
            }

            private void OnSearchChanged(ChangeEvent<string> evt)
            {
                if (_disposed)
                {
                    return;
                }
                _activeText = evt.newValue;
                if (string.IsNullOrEmpty(_activeText))
                {
                    _scheduledItem?.ExecuteLater(0);
                }
                else
                {
                    _scheduledItem?.ExecuteLater(_delayMs);
                }
            }

            private void UpdateItem()
            {
                SearchTextChanged?.Invoke(_activeText);
            }
        }
    }
}
#endif

Why?

There’s likely a better way to satisfy the requirement. Like a factory that creates components based on an enum.

This would also make the GetComponent lookup easier or unnecessary.

I don’t really think ‘using an enum’ is ever a good option for something that potentially needs to scale by a large amount.

FWIW’s for OP, Odin Inspector + Serialiser readily supports serialising and drawing System.Type with a very good drawer for it. Alternatively, using some good old OOP and making your behaviour more pluggable with scriptable objects or SerializeReference might be get you the flexibility you need.

I’m using the same unity version.

I am trying to implement your code, however I don’t know where it should go. I’ve put both the “usage” and “code” parts into separate csharp scripts but there are a few errors in the code. maybe because I copy-pasted it directly, not really knowing what to do with it… I’m not versed in coding for the editor, what do I do with the code to make it work?

The Tests class in the second file (L118 to L136) is not necessary, so you can remove that. If errors remain, copying the compiler errors from the Console would help.

I’ll just attach the modified file here:

The UIElements module (another name for UI Toolkit) may need to be enabled, you can do so in the Package Manager:

9836394–1415055–SerializableType.cs (12.6 KB)

I have the updated version now and also the module enabled.

The errors do remain, but they don’t show up in the console. I have taken screenshots of them in visual studio instead.
9836439--1415058--Screenshot 2024-05-15 213207.png
This is on line 303. I’m entirely unsure what any of this means, to be honest. I tweaked it a little but couldn’t figure it out.

There is also this:
9836439--1415061--Screenshot 2024-05-15 213309.png
Line 180. There are 8 different errors in this line. I’m not sure what the issue is. I can show the other errors too if needed.

And finally, my last problem, is that I still have zero idea how either of these files are supposed to be used in the project…

Unity is on an old release of .NET and C#. It’s confused and misidentifying the feature because of that. You want one of these instead.

List<Type> filtered = new List<Type>();

or

var filtered = new List<Type>();

Same problem. Static lambdas are a C# 9 feature. Unity doesn’t fully support C# 9.

1 Like

SerializableType.cs contains the SerializableType struct and supporting editor UI. The “usage example” snippet I posted earlier is just an example of what might go in a MonoBehaviour.

Visual Studio should not be reporting errors for that syntax, it’s C# 9 and is supported on Unity 6 Preview. Failing to identify LINQ is also a bad sign. Which version of the Visual Studio editor package do you have in Unity’s Package Manager / do you have it installed at all? Install the latest version (2.0.22). Also, install the “Game Development with Unity” workload in Visual Studio Installer.

1 Like

It’s really not. Well, yeah, it’s old, but there isn’t a problem on Unity’s side. That’s C# 9 syntax that works on Unity since 2021.2(?). The problem is Visual Studio. To clarify, the exact code works perfectly on my Unity 6 Preview (6000.0.1f1) install, with similar code having been used throughout 2022 releases. If errors are not showing up in the Console, then 99 times out of 100 it’s compiled correctly, and you just have a misconfigured IDE like Visual Studio.

1 Like

Yep. Just verified on my end that it’s working. I should have just done that before posting. :stuck_out_tongue:

1 Like

This fixed it, thanks ^^"
I was using vs 2017, switching to the latest version fixed the problem and everything is working now. Thanks for the help!