Community Made Tools

Have you made any useful utilities with Odin?

Login and submit your creations here

Collections with no derived types

Authored by Fulvio
Shared 02-07-2024

Use this attribute with collections serialized with the Odin Serializer. If used, a collection hide the object picker of its elements and, when an object is added, automatically creates it with the type contained in the collection (without any user choice among subtypes.)

Usage

using System;

[Serializable]
class SubType { }

[Serializable]
class DerivedSubType : SubType { }

[Serializable]
class MyClass
{
    // The following collection uses automatically Subtype instances.
    // No interface to select between SubType and DerivedSubType is shown when the user add an element
    [FixedTypeCollection] SubType[] myCollection;

    // The following collection instead uses automatically DerivedSubType as subtype
    [FixedTypeCollection(typeof(DerivedSubType))] SubType[] mySecondCollection;
}

// MyClass is serialized with the Odin Serializer
public class MyScriptableObject : SerializedScriptableObject
{
    [NonSerialized, OdinSerialize] MyClass class;
}

If the object to create in the collection is a UnityEngine.Object or a type derived from it, it's automatically created as null. If instead there is a default constructor, that's called (it's not necessary for the constructor to be public.) As last resort, if no constructor exists, the object is created by unity deserializer (with UnitySerializationUtility.CreateDefaultUnityInitializedObject.)

FixedTypeCollectionAttribute.cs

using System;
using System.Collections.Generic;
using System.Linq;
using System.Reflection;
using System.Text;
using JetBrains.Annotations;
using OdinExtensions.Attributes;
using Sirenix.OdinInspector;
using Sirenix.OdinInspector.Editor;
using Sirenix.Serialization;

namespace OdinExtensions {
    [UsedImplicitly]
    public class FixedTypeCollectionAttributeProcessor : OdinAttributeProcessor {
        public override void ProcessSelfAttributes(InspectorProperty property, List<Attribute> attributes) {
            FixedTypeCollectionAttribute? attribute = GetAttribute(attributes);
            if (attribute is null)
                return;
            attributes.Add(new HideReferenceObjectPickerAttribute());
            Type? type = attribute.SubType ?? GetCollectionSubType(property);
            if (type is not null)
                AddCreateFunction(attributes, type);
        }

        static FixedTypeCollectionAttribute? GetAttribute(List<Attribute> attributes) =>
            attributes.OfType<FixedTypeCollectionAttribute>().FirstOrDefault();

        static void AddCreateFunction(List<Attribute> attributes, Type subType) {
            string typeString = TransformTypeToCode(subType);
            attributes.Add(new ListDrawerSettingsAttribute {
                CustomAddFunction = $"@{nameof(FixedTypeCollectionAttributeProcessor)}.{nameof(Create)}<{typeString}>()"
            });
        }

        static Type? GetCollectionSubType(InspectorProperty property) {
            MemberInfo? memberInfo     = property.Info.GetMemberInfo();
            Type?       fieldType      = (memberInfo as FieldInfo)?.FieldType;
            Type?       propertyType   = (memberInfo as PropertyInfo)?.PropertyType;
            Type?       collectionType = fieldType ?? propertyType;
            return GetSubTypeFromCollectionType(collectionType);

            static Type? GetSubTypeFromCollectionType(Type? type) =>
                type is null
                    ? null
                    : type.IsArray
                        ? type.GetElementType()
                        : type.IsGenericType
                            ? type.GetGenericArguments().FirstOrDefault()
                            : null;
        }

        static string TransformTypeToCode(Type type) {
            string? typeName = type.IsGenericType ? type.GetGenericTypeDefinition().FullName : type.FullName;
            if (typeName is null)
                throw new ArgumentException("Type must have a name.", nameof(type));
            typeName = typeName.Replace("+", ".");
            if (!type.IsGenericType)
                return typeName;
            StringBuilder stringBuilder = new();
            stringBuilder.Append(typeName[..typeName.LastIndexOf('`')]);
            stringBuilder.Append("<");
            stringBuilder.AppendJoin(", ", type.GetGenericArguments().Select(TransformTypeToCode));
            stringBuilder.Append(">");
            return stringBuilder.ToString();
        }

        public static T? Create<T>() {
            if (typeof(T).IsSubclassOf(typeof(UnityEngine.Object)) || typeof(T) == typeof(UnityEngine.Object))
                return default;
            ConstructorInfo? constructor = typeof(T).GetConstructor(
                BindingFlags.Public | BindingFlags.NonPublic | BindingFlags.Instance,
                null,
                CallingConventions.Any,
                Array.Empty<Type>(),
                null);
            object? instance = constructor?.Invoke(Array.Empty<object>());
            if (instance is not null)
                return (T)instance;
            return (T)UnitySerializationUtility.CreateDefaultUnityInitializedObject(typeof(T));
        }
    }
}