A System.Type to Represent "Types" - Which approach?

I am trying to approach representing types in my project. By types I mean types of an object. For example an equipment type might include “weapon”, “armour”, “potion” etc.

Now the “go to” approach that I have seen is to use enums which is okay, but has certain pitfalls especially with scalability and maintaining robust code.

They are what I have used so far but I am honestly reluctant to go much further as I am worried they will become a mess later on.

The question then, is what to use instead. I have done some research and read these pages:

String Enums in C#: Everything You Need to Know - Josip Miskovic, Alternatives to enums in TypeScript and Enumeration classes · Los Techies.

There are several issues I want to solve if I introduce my own type Type:

  • One of the very useful features of enum is when they are serialized in the editor you can select a possible value from a drop down box which makes life easy, especially where scriptable objects are involved.
  • One feature I would like to implement is the ability to have child or sub types. With enums, this isn’t possible, but the concept is clear. If “weapon” is an equipment type, and “long sword” is a weapon type then we can see that all long swords are weapons yet not all weapons are long swords.

I was hoping some people with more experience might be able to point me in the correct or maybe just a direction. I was thinking about implementing type as a tree like structure with nodes but I am not sure and I would still need a type for each node. If I do use my own type, how I will get the editor functionality I desire… Can I write an editor script that provides a similar function as the drop down box when using a type like the enumeration type seen here Enumeration classes · Los Techies. Where the type values are stored as public static members of the class.

As you can see my lack of experience is making it difficult to make a decision so any help or advice would be appreciated. Sorry if the question is a little vague.

You could take a look at SmartEnum for inspiration. Here’s a video showcasing it in action:
How to write “smarter” enums in C#

You couldn’t really use it in Unity like you’d want as it is thought, because it contains readonly fields and Unity can’t serialize it. You’d also need to write a custom PropertyDrawer that would use reflection to generate a popup list by calling SmartEnum.List.

Another replacement for enums, which is used more commonly in the Unity world, is ScriptableObjects:

I did it all the way through, but it’s not really that clean or simple to share it here.

I made the user perspective look simple and it has a couple of neat features, like automatic tooltips, automatic dropdown naming from C# variable names, and the drawer can optionally show an undefined entry aka ‘None’ (and serialize/deserialize it properly) among the other things.

I actually thought about it today as I stumbled on the scripts whether to share it or not. Maybe if I do it better next time. Making a drawer that performs well didn’t quite turn out how I’d like it (it’s O(n) in a couple of critical places), and then the custom derivation itself, even though it’s simple and user-friendly, takes some explaining, otherwise the bugs could be ugly.

SmartEnums are an ugly hack after all, not a general solution. It was pretty good for my use case though.
Still I vote for ScriptableObject as a general solution.

I’m happy to answer questions if someone wants to do it.

1 Like

Scriptable Objects should be the way to go, as they’re the most ready made solution that works in Unity out of the box. Effectively it’s just practising OOP in a way that works with Unity’s serialisation system.

A step up from that is SerializeReference, which lets you serialise polymorphic data right into objects. It does require custom inspector work, but I prefer it as it avoids the need to have tons of potentially very small scriptable objects all over the place.

One more option would actually be to use an enum to define your list of types and then introduce “subtyping” support with a custom attribute and an extension method.

[AttributeUsage(AttributeTargets.Field)]
public sealed class SubtypeOfAttribute : Attribute
{
    public ItemType BaseType { get; }

    public SubtypeOfAttribute(ItemType baseType) => BaseType = baseType;
}

public enum ItemType
{
    None = 0,

    Equipment = 1,

    [SubtypeOf(Equipment)]
    Weapon = 2,

    [SubtypeOf(Weapon)]
    LongSword = 3
}

public static class ItemTypeExtensions
{
    private static readonly Dictionary<ItemType, HashSet<ItemType>> subtypesOf = new Dictionary<ItemType, HashSet<ItemType>>();

    static ItemTypeExtensions()
    {
        foreach(ItemType itemType in typeof(ItemType).GetEnumValues())
        {
            var name = Enum.GetName(typeof(ItemType), itemType);
            if(typeof(ItemType).GetField(name).GetCustomAttribute<SubtypeOfAttribute>() is SubtypeOfAttribute subtypeOf)
            {
                var baseType = subtypeOf.BaseType;
                if(!subtypesOf.TryGetValue(baseType, out HashSet<ItemType> subtypes))
                {
                    subtypes = new HashSet<ItemType>();
                    subtypesOf.Add(baseType, subtypes);
                }

                subtypes.Add(itemType);
            }
        }
    }

    public static bool IsSubtypeOf(this ItemType itemType, ItemType baseType)
    {
        if(!subtypesOf.TryGetValue(baseType, out var subtypes))
        {
            return false;
        }

        if(subtypes.Contains(itemType))
        {
            return true;
        }

        foreach(var subtype in subtypes)
        {
            if(itemType.IsSubtypeOf(subtype))
            {
                return true;
            }
        }

        return false;
    }
}

Usage:

var longsword = ItemType.LongSword;
var weapon = ItemType.Weapon;
var equipment = ItemType.Equipment;

Debug.Log(longsword.IsSubtypeOf(weapon));     // true
Debug.Log(longsword.IsSubtypeOf(equipment)); // true
Debug.Log(weapon.IsSubtypeOf(equipment));    // true
Debug.Log(equipment.IsSubtypeOf(weapon));    // false

Thanks for such great and quick responses. I will have a look and see what I can create with the ideas presented. Thanks!!

This… feels like another dirty hack and is probably hardly scalable.

Lets stay away from enums.

Well yeah, enums inherently don’t scale well, and this extension wouldn’t change that in any way.
You’d still always need to make code modifications to the same file when introducing new types, and probably would need to go edit every switch statement targeting the enum type across the project as well. I would also try and avoid using enums, unless the list of types will remain mostly static, or the number of references to the type can be kept minimal. I usually only use enums when they can be made private or when they only contain a handful of values that are unlikely to change.

Using ScriptableObjects can scale better, but it relies a lot on using serialized fields for hooking things together using the Inspector, so it might not be a good fit for a game that is very code driven. It could also lead to more scene and prefab modifications, potentially leading to more asset conflicts between multiple developers, which are much more difficult to resolve than conflicts in code. It can also become more difficult to figure out which types are still actually being used, and which have become obsolete over time and could be removed from the project, as the number of assets keeps growing. Using ScriptableObjects is definitely a viable option to consider, but I think it can also have it’s own problems when it comes to scaling, depending on the context.

A database can be an interesting option, but you will need some code generator to interface it with Unity. It’s worth considering if you have hundreds of different types.

At the end of the day, there’s no perfect option.

My preferred option these days is the SerializeReference approach. Then it’s just a matter of regular Object Oriented Programming. Though this is from the perspective of a hobbyist.

@SisusCo
Actually I just now remembered why I needed custom enums in the first place.

I was actually completely fine with system enums as they were, because I had a situation where I wanted about a dozen different primitive identities, and I just needed to be able to easily differentiate between them in inspectors and code.

But everything else would also hinge on this design, numerous ScriptableObjects would list and refer to these identities etc. so the major problem was in how enums are serialized in Unity. I am actually thoroughly disappointed in how this is designed, on both ends.

You see, not only enums are a bad and non-scalable solution on their own, but when they serialize, something that was known as “Foo” is 5 all of a sudden. I don’t get this horrible design at all. Enums are practically a ticking time bomb waiting to go off, with a potential to completely demolish the project late into development.

So my immediate priority was to replace the enum entirely, and make something similar which correctly serializes into Foo string. And this is why I said the experiment was successful for my case. My needs regarding the actual enum behavior and scalability were very limited, it was mostly about the human-trackable serialization (you open the YAML and you see Foo, and are free to add more identities in the future without anything breaking) as well as the intuitive inspector behavior.

I’m considering publishing this project in the near future because it definitely has its uses.
I used a couple of tricks with reflection to enable auto-naming (with simple name sanitation similar to what Unity does in auto-inspectors) and the equalities are resolved strictly through string matching. The drawer itself, though suboptimal when there are too many fields (which wasn’t noticeable in my case), has an optional ‘None’ element (which serializes as blank data), a sorting feature, optional custom display names, and optional custom tooltips. (There is also a custom attribute to describe the desired drawer behavior.)

Anyway, if I do write an article I’ll link it here.

@orionsyndrome Yeah enum fields being serialized by their underlying values is definitely unintuitive at first. I’ve gotten quite used to this over time, and now just always make sure that all enum members are explicitly tied to a specific integer value.

Still this can lead to some hilarious end results if an enum is used for defining a living set of values.

public enum PanelIdentifier
{
    None = 0,

    MainMenu = 2,
    PauseMenu = 4,

    Inventory = 12,
    Crafting = 13,

    PlayerMenu = 100,
    StatsMenu = 101,
   
    Dialogue = 300
}

An enum variant that is serialized as a string would definitely be an improvement in such cases.

Yes. I don’t mind this when enums are used in a rigid manner throughout the software, as a system flag or some named setting. These collections are typically out of reach and inflexible by design. But this is the complete opposite of anything that is supposed to belong to high level design of a game. And I had to think very deeply whether ScriptableObjects would fulfill this role. But I’m not sure if anyone really wants their primitive identity identified via GUID, that’s just nonsense. It should be a light, monadic, non-exclusive, non-demanding, unambiguous, human-readable data packet that is both drag-n-droppable inside editor and one that easily passes the UI/code barrier, where its removal does not cascade into development/deployment halt. ScriptableObjects kind of pass this sanity check with okay marks, but aren’t perfect.

@SisusCo
full guide here

1 Like