Enums for ID for growing lists, like ItemID? Better alternatives?

Enums are a love and hate relationship. I love attaching a name to a number but the organization of a growing list is a complete mess, trying to keep open space to add more IDs as the list grows in both width and depth.

One alternative (which isn’t as efficient) could be to just have a class of string fields, but the editor doesn’t let you easily do a drop down for something like that which seems messy. (Perhaps Odin or something could handle that?)

What’s a good way to handle this? Or should I continue as I have with enums?

Why do you care exactly about the integer value?
The idea of enum is to use them as identifiable objects with the convenience of compiler-checks. The integers exist solely for internal reasons so that it does not actually need to store and compare a string.

Because of organization. Like let’s say I have a list of iron weapons, but later on I add a new weapon class like a halberd, I could add Iron_Halberd at the bottom of the enum list or I could add it in a gap I left for Iron_x. If I add it at the bottom, it’ll be messy trying to find where things are or if I’m missing something in the Mithril section for instance.

I could admittedly leave no gap and assign the number then reorganize it how I wanted to, but then I might forget that I used that enum value somewhere in that list.

You could forego enums completely and use scriptable objects with weapon configs. And there you go, infinite number of of possible values.

In your scenario, however, I’d recommend to stop and make sure the problem you’re trying to solve actually exists. Enums allow for compile checks, if you, say, move weapon classes to some sort of json config, you’ll lose that.

Regarding integer IDs, it might be a good idea not to use integer ids while serializing. You could use string representation of the enum, or (probably) associate some descriptive value with it via an attribute.

Ah, does that mean you are currently defining your weapons in code?
For usecases like those where you effectively just want to comfortably and identifiably define things, ScriptableObjects are gaining in popularity.
Make a “Weapon” class that inherits from ScriptableObject and you can instantiate as many weapons as you want in the editor. Then use folders to structure them for yourself as a developer. For within the game, serializable List() can be reordered by dragging the elements left side (think since Unity 2021).

As previously mentioned, the weapon can be a Scriptable Object but WeaponID can also itself be a Scriptable Object. Unity can check for Scriptable Object equality and as long as your reference the same SO asset for the check, it’ll work similarly to an enum but without the management issues of growing project requirements.

[23:39] Extendable Enums

1 Like

Serialization with the usecase that something serialized in an old version, should be de-serializable in a newer version brings a lot of issues with (personally I have resorted to just using strings for that too), but the TE was only worried about the integers for order reasons.

I am using ScriptableObjects, but I want the ability to look them up from the code as well, check if they have the quest item requirements or any of the other various checks so I have an enum ID associated to them.

Using a scriptable object as the ID itself is interesting, are we talking about the GUID from the script itself? If I’m using a SO how would I, for instance, check if they have 10 iron ore if the ID is a GUID? With an enum I’d just check if they had Iron_Ore. I suppose I could use a string ID but then it isn’t type-safe because I’d have to type it in the SO and in the code, but maybe that’s the way to go?

Actually now that I think about it, I suppose the quest SOs could look for the SO ID and never need to know more details in the code, but I can’t help but think there would come a point where looking it up in the code would still be valuable.

Thanks, appreciating the help.

That is something you usually want to strictly avoid because, as you already noticed, this limits extensibility. It means you have to change code for way more game content changes than necessary.

“What” is checking whether iron ore is available? Are you talking about e.g. a crafting recipe?
Do NOT define that in code either, you’ll run into all sorts of issues like this one. Instead make a crafting recipe ScriptableObject (SO) which has a list of your item enums “required_ingridients”. Then you can pull the ingredients into that list via editor and again have completely avoided handling numbers by comparing the contents of that list with the list of the players inventory etc. :slight_smile:
EDIT 2: Probably you’ll wanna wrap the SO into a struct ItemSlot which has a field for a quantity besides the field for the SO.

That’s how you separate the game content from code.

EDIT 2: Avoid GUIDs! Sometimes Unity itself surprisingly changes those. Never write GUIDs down somewhere.
In case there is a misunderstanding: Instead of comparing the GUID of two things, you can ALWAYS just compare the things. Two references to the same SO will compare to equal ( == true).

On a base level, the ID Scriptable Object is empty because the asset itself serves as the ID, it doesn’t have to contain anything. It could, but for ID purposes it’s not necessary.

using UnityEngine;

[CreateAssetMenu(fileName = "New WeaponID", menuName = "New/WeaponID")]
public class WeaponID : ScriptableObject
{
}

Then in some manager class check for WeaponID equality. If manager class and weapon both reference the same asset, it’ll return true.

[SerializeField] private WeaponID OreID;

public void DoWeaponThings(Weapon weapon)
{
     if (weapon.WeaponID == OreID)
     {
          //
     }
}

This assumes you can manually asign the WeaponID in both the weapon and manager class Inspector or by direct SO asset reference in code.

With Odin you can customize Scriptable Object selector to work exactly like an enum dropdown.

Holly fricking molly… In thoughts of this thread I’ve thrown this at ChatGPT.
And it gives an answer which should clear up about everything. And the best part is, you can ask it further questions!

Now just wish there was a way to copy answers easier over here including the pretty formatting, lol. … We people in forums will really be out of business soon due to this :smile:

Q: Please demonstrate how to sue ScriptableObject in Unity to create a quest system that demands Items from the player.
[[ Just realized it even correctly ignored that I misstyped “use” x3 ]]

A:
Sure! Here’s an example of how you can use ScriptableObjects to create a quest system that demands items from the player in Unity:

First, you’ll need to create a Quest ScriptableObject class that represents a quest. This class should have fields for the name of the quest, a description of the quest, the reward for completing the quest, and a list of items that are required to complete the quest.

Here’s an example of what the Quest class might look like:

using UnityEngine;

[CreateAssetMenu(menuName = "Quest")]
public class Quest : ScriptableObject
{
   public string questName;
   public string questDescription;
   public int reward;
   public Item[] requiredItems;
}

Next, you’ll need to create an Item ScriptableObject class that represents an item. This class should have fields for the name of the item, a description of the item, and an sprite representing the item.

Here’s an example of what the Item class might look like:

using UnityEngine;

[CreateAssetMenu(menuName = "Item")]
public class Item : ScriptableObject
{
   public string itemName;
   public string itemDescription;
   public Sprite itemSprite;
}

Once you have the Quest and Item classes set up, you can create instances of these classes in the Unity editor and assign values to their fields. For example, you could create a quest called “Gather Firewood” that requires the player to collect 3 logs and rewards them with 100 gold.

To track the player’s progress on a quest, you’ll need to create a QuestTracker script that keeps track of which quests the player has accepted, which quests they have completed, and which items they have collected.

Here’s an example of what the QuestTracker script might look like:

using UnityEngine;
using System.Collections.Generic;

public class QuestTracker : MonoBehaviour
{
   public List<Quest> acceptedQuests;
   public List<Quest> completedQuests;
   public List<Item> collectedItems;

   public void AcceptQuest(Quest quest)
   {
       acceptedQuests.Add(quest);
   }

   public void CompleteQuest(Quest quest)
   {
       acceptedQuests.Remove(quest);
       completedQuests.Add(quest);
   }

   public void CollectItem(Item item)
   {
       collectedItems.Add(item);
   }

   public bool HasCollectedAllItems(Quest quest)
   {
       foreach (Item requiredItem in quest.requiredItems)
       {
           if (!collectedItems.Contains(requiredItem))
           {
               return false;
           }
       }
       return true;
   }
}

With the QuestTracker script in place, you can use it to track the player’s progress on quests and check whether they have collected all of the required items for a particular quest.

For example, you could use the HasCollectedAllItems method to check whether the player has collected all of the required items for a quest, and if they have, you could call the CompleteQuest method to mark the quest as completed and reward the player with the quest’s reward.

I hope this helps! Let me know if you have any questions.

The obvious thing would be to put that “quest item requirements” into scriptable object, no?

No, we’re talking about scriptable object itself.

You’d have a scriptable object named something like “GameItem” or “GameItemType”. And “Iron Ore” would be a scriptable object asset of “GameItem” type.

In this scenario, your “quest information” would have a field “required iteM”, it would be of type “GameItem”, and you would drag it in inspector with mouse into it. You wouldn’t touch any IDs or GUIDs yourself anywhere.

Basically…

public class GameItemType: ScriptableObject{
    public string name;
    public string description;
/// stats, etc
}

[System.Serializable]
public class QuestRequirments{
    public GameItemType requiredItem;
    public int requiredAmount = 1;
// etc
}


[System.Serializable]
public class GameQuestConfig{
    public QuestRequirmements[] requirments;   
}

No ids anywhere. You configure this within the project.

Actually the majority of it does work without it being hardcoded to check for like Iron_Ore but I might check the quest requirement item enum id to the inventory slot item enum id, but I definitely see the value in the scriptable object IDs now.

Where it does come up somewhat hard-coded though is in stuff like Quests where the unlocks might be too unique outside of typical item rewards. Also when it comes to loot, monsters can drop stuff like various currencies such as gold along with items. I treat them all the same, so I do have a little bit of hard-coded stuff to handle special items that normally wouldn’t go in your inventory, but perhaps that would be better off as being a separate as a secondary loot list.

Also yeah, ChatGPT is absolutely incredible!

Also for @PanthenEye that is very neat, I know when you duplicate a scriptableobject, it also duplicates the meta data, would it then match against multiple files? Just got to make sure you never duplicate?

Thanks all!

Duplicated SO IDs are unique, you can’t match across multiple asset files.

1 Like

When you duplicate SOs. they are independent from each other. But you normally shouldn’t have the risk of accidentally duplicating SOs and assuming they are the same or so.

1 Like

Excellent, thanks everyone!

After pondering and testing this out, how does saving/loading work reliably? It seems like it uses the InstanceId for comparing but it doesn’t appear to be very reliable and can change when reloading the game. It kind of seems like you still want some ID that you set up after looking deeper into it which brings me back to where I started?

In the past I’ve gone the Dictionary<string, WeaponID> route where the string is a GUID on the SO in the form of public string ID = System.Guid.NewGuid().ToString(); The GUID is assigned as readonly at the time of SO asset creation. You serialize the GUID to save file, then retrieve the SO by its ID from the Dictionary.

Not sure if this works without Odin serialized dictionaries, though. I populated the dictionary at edit time whenever entering play mode, so it’s always up to date, but Unity can’t serialize dictionaries without Odin or similar tools and methods for retrieving assets of type were editor only if I recall correctly.

1 Like

Cool thanks, yeah, I use Serialized Dictionary (there’s a free version), although these days I now have Odin as well which I used Dictionary<Enum,SO> when I was doing it.

And you can place that Dictionary inside a ScriptableObject as well so you don’t have to hard reference something in Hierarchy to get access to it.

1 Like