How to instantiate a new & unique ScriptableObject during run-time

List<InventoryItem> itemLibrary = new List<InventoryItem>();
After serializing this list, I created two different items and put them into this list: For readability sake → I have one asset that’s just the regular ‘EquipableItem’ and the other ‘StatsEquipableItem’ but the items themselves are named as ‘Base EquipableItem’ & ‘Base StatsEquipableItem’.

These items are not defined at all, all configurable properties are empty besides the generic pick up spawner on each item and their ItemID’s.

Imagine with me for a moment: This gameObject will roll a dice via code when called. When the dice is rolled it will elect one of these items to proceed forward with. With this item my script will select a random name & description (appropriately), whether it’s stackable based on certain data and where it could be slotted if at all for the equipment or action bars and all other relevant things by RNG.

However despite that I got all of this working till this point…

I seem to have run into an issue. What I’d like to do is be able to instantiate a new instance of that ‘Base EquipableItem’ or ‘Base StatsEquipableItem’ and have them be entirely different from the previously created item that occured.

I want to have a ‘Base’ item for each individually different item that could be obtained in my game, and then have the ‘CreateRandomItem()’ method use that Base Item and then fill in all the blanks without associating itself with the Base item at the end… so it can be re-usable to continuously create new items from the base item.

Some code for context.

    public void CreateNewItem()
    {
        int randomIndex = UnityEngine.Random.Range(0, itemLibrary.Count);
        CreateRandomItem(itemLibrary[randomIndex]);
    }

    InventoryItem CreateRandomItem(InventoryItem createItem)
    {
        if (createItem is StatsEquipableItem)
        {
            StatsEquipableItem statItem = createItem as StatsEquipableItem;
            return SetUpEquipableModifierItem(statItem);
        }

        else if (createItem is WeaponConfig)
        {
            WeaponConfig weapon = createItem as WeaponConfig;
            return SetUpWeaponItem(weapon);
        }

        else if (createItem is EquipableItem)
        {
            EquipableItem equipItem = createItem as EquipableItem;
            return SetUpEquipableItem(equipItem);
        }

        return createItem;
    }

     WeaponConfig SetUpWeaponItem(WeaponConfig weapon)
    {
        weapon.SetItemID(Guid.NewGuid().ToString());
        SetUpWeaponLocations(weapon, out var location, out var category);
        weapon.SetDisplayName("Weapon");
        weapon.SetDescription("This weapon is a sword.");
        SpawnGenericPickup(weapon.SpawnPickup(Vector3.zero, 1));

        return weapon;
    }

    EquipableItem SetUpEquipableItem(EquipableItem equipableItem)
    {
        equipableItem.SetItemID(Guid.NewGuid().ToString());
        equipableItem.SetDisplayName("No Stats Equip");
        equipableItem.SetDescription("Here you have an example of no stats equipped.");
        SpawnGenericPickup(equipableItem.SpawnPickup(Vector3.zero, 1));
        return equipableItem;
    }


    StatsEquipableItem SetUpEquipableModifierItem(StatsEquipableItem statItem)
    {
        GameObject player = GameObject.FindGameObjectWithTag("Player");
        statItem.SetItemID(Guid.NewGuid().ToString());

        statItem.AddRandomStat(player);

        statItem.SetDisplayName($"Modifier Item");
        SpawnGenericPickup(statItem.SpawnPickup(Vector3.zero, 1));
        return statItem;
    }

When I learnt that the InventoryItem was an abstract class I removed the abstract and tried doing something like…

ScriptableObject newItem = Instantiate(statItem); // This was inside the SetUpEquipableModifierItem method

I tried doing something like the above here but it still created an instance that was linked with the base item, so any changes to the base also affected the instantiated item. This is also occuring with the set up I have in the code you’re reviewing now…

Okay I think I’ve got the gist of it.

            // TESTING CODE
            ScriptableObject newItem = ScriptableObject.CreateInstance<StatsEquipableItem>();
            StatsEquipableItem item = newItem as StatsEquipableItem;
            item.SetSpawnPickup(prefab);
            item.SetDisplayName("Item: " + index);
            index++;
            CreateRandomItem(item);
            instantiatedItemsList.Add(item);
           // TESTING CODE 

Seems I’m on the right track, but if I do

ScriptableObject newItem = ScriptableObject.CreateInstance<InventoryItem>();

And then

InventoryItem item = newItem as InventoryItem;
CreateRandomItem(item);

Nothing happens, no code error, but also no item creation, even though the CreateRandomItem requires a InventoryItem to be parsed as a parameter before checking whether it belongs to a specific category like EquipableItem or etc.

Because as it stands, it’s only specifically creating an instance of the StatsEquipableItem and none other which isn’t the functionality I obviously want.

You can create a Scriptable Objects during runtime but there’s an issue with that, the item won’t be saved, you’ll have to do that manually using JSON.

The issue I’m having with this approach is that is way too complicated for what you are trying to achieve, I suggest using pure C# classes instead of scriptable objects since they are far easier to use, that implies that you would have to revamp the whole system, but it will come with additional benefits, like being able to create truly random items with truly random properties, for instance, swords with different stats but also with different abilities, like a sword that burns, other that slows, other that slows and burns, and so on.

Yes you’re quite right.

Interestingly enough, if I never collect the item and save. After loading back the item is still there, and is collectible. The issue now is that the Equipment or Inventory isn’t storing that the item was collected or equipped.

I actually do this in my own projects, using a base item and assigning random attributes. You have the right idea in mind, but the problem is in the execution. An InventoryItem instantiated as an InventoryItem cannot and will not ever be a child class. The child class is what needs to be instantiated, in other words, they need to be StatsEquipableItems in the first place.

I make those StatsEquipableItems ISaveables, the Capture and RestoreStates take care of remembering the random attributes assigned to them.

So here’s what I do:
For each general type of StatsEquipableItem, I create a template SO in the assets. So if I want a boot, then I create a StatsEquipableItem, call it a boot, describe a boot, and give it the equip location and a boot icon/pickup. You can be even more generic if you wish as far as that goes.

When I want to drop a new boot, I Instantiate the boot I created in the asset menu

StatsInventoryItem droppedItem = Instantiate(bootTemplate);

This creates a new boot, but does not save it to disk. This droppedItem boot will have the exact same GUID, however, and this is the key to how the whole thing works.

Now that you have your droppedItem from the template, you can change the things you want to change about it, assign random stats, give it a random name or description (or if you’re clever, infer a description from the random stats). This is the item that will go to Inventory or StatsEquipment.

Now, when it’s time to Save, your stores (Inventory, StatsEquipment, ActionStore) will need to have an additional field. They’re going to store the ItemID of the SO (which matches the one we can retrieve by ItemID!), and they’re going to store the result of the item’s CaptureState.
When you restore, you’re going to Instantiate the item like you did when you created it, and then you’re going to call that item’s RestoreState, passing along the object you got from CaptureState.

I know this is short on code, but I’m on the lunch break and just have time to give out the rough outline.

1 Like

Here’s a link to one of my projects (code, no real assets). If you look at the StatsEquipableItem and the Equipment.cs, you’ll get an idea of what I’ve got going on with this. Fair warning, the saving system is Json instead of BinaryFormatter, so there’s some differences there.

1 Like

I’m glad to see that this method is possible and it’s not a deadend. I’d love to see your project code to review for this as well but I think you might’ve forgot to add the link or I’m blind xD

Oh and here’s my Equipment.cs CaptureState / RestoreState() as of now:

        object ISaveable.CaptureState()
        {
            var equippedItemsForSerialization = new Dictionary<EquipLocation, string>();
            foreach (var pair in equippedItems)
            {
                equippedItemsForSerialization[pair.Key] = pair.Value.GetItemID();
                Debug.Log(pair.Value.GetItemID());
            }
            return equippedItemsForSerialization;
        }

        void ISaveable.RestoreState(object state)
        {
            equippedItems = new Dictionary<EquipLocation, EquipableItem>();

            var equippedItemsForSerialization = (Dictionary<EquipLocation, string>)state;

            foreach (var pair in equippedItemsForSerialization)
            {
                var item = (EquipableItem)InventoryItem.GetFromID(pair.Value);
                if (item != null)
                {
                    equippedItems[pair.Key] = item;
                }
            }
            equipmentUpdated?.Invoke();
        }

So should I declare a new Dict for say, a “StatsEquipableItem, string”? Then following through with my presumption that string value will CaptureState() the item’s ItemID and in Restore State() grab the dictionary’s item in question’s itemID and re-instantiate the specific StatsEquipableItem and put it back into the equipped location if it is equipped else go into Inventory. Something like that?

When saving, it’s best to create a struct for each item

[System.Serializable] struct itemStruct()
{
    public string itemID;
    public object state;
}

Then your equippedItemsForSerialization will be a Dictionary<EquipLocation, itemStruct>
You’ll instantiate the result of GetFromID(itemStruct.itemID) and then call RestoreState on that instantiated SO with the itemStruct.state

Here’s the link to my project, don’t know why that didn’t go through:
Spellborn GitLab

1 Like

Alright, I’ll give this a tackle and report back with my findings! :slight_smile:

So after looking around in your Equipment.cs, I did the same following the struct you implemented

        object ISaveable.CaptureState()
        {
            var equippedItemsForSerialization = new Dictionary<EquipLocation, ItemStruct>();
            ItemStruct itemStruct = new ItemStruct();

            foreach (var pair in equippedItems)
            {
                itemStruct.itemID = pair.Value.GetItemID();
                itemStruct.state = pair.Value.CaptureState();
                equippedItemsForSerialization[pair.Key] = itemStruct;
            }
            return equippedItemsForSerialization;
        }

        void ISaveable.RestoreState(object state)
        {
            equippedItems = new Dictionary<EquipLocation, EquipableItem>();

            var equippedItemsForSerialization = (Dictionary<EquipLocation, ItemStruct>)state;

            foreach (var pair in equippedItemsForSerialization)
            {
                var item = (EquipableItem)InventoryItem.GetFromID(pair.Value.itemID);
                if (item != null)
                {
                    equippedItems[pair.Key] = Instantiate(item);
                    equippedItems[pair.Key].RestoreState(pair.Value.state);
                }
            }
            equipmentUpdated?.Invoke();
        }

But I’m getting a serialization stream error because → itemStruct.state = pair.Value.CaptureState(); is a CaptureState() for EquipableItem and I’m not sure what to put into that, as well as the RestoreState().

How should I implement the CaptureState() / RestoreState() inside EquipableItem?

I made InventoryItem an ISaveable, but made the CaptureState and RestoreState in InventoryItem virtual. If a descendant (StatsEquipableItem) has data to save, it overrides that method and captures/restores the relevant unique data.
In my case, this is the procedurally generated stats. Take a look at my StatsEquipableItem for what I’ve done.

Yeah a bit overwhelming to differentiate the JSON from the courses Saving System but it is eye opening to see your beautiful code in full display. I will literally go over each document to get better at understanding c# thanks to this accessibility. Seriously, thank you tons!

Now onto my findings…

First of all I immediately went to InventoryItem and implemented ISaveable → I made members virtual to allow for them to be accessible and overriden from StatsEquipableItem, etc.

InventoryItem.cs

        public virtual object CaptureState()
        {
            return itemID;
        }
        public virtual void RestoreState(object state) {}

As far as I could intrepret from the JSON code you’re just returning the ‘name’ of the object right? So could I just return the ItemID of this particular InventoryItem like shown above?

StatsEquipableItem.cs

I implemented the members from InventoryItem’s ISaveables.

public override object CaptureState() 
{
  // capture the data for this stats item
}

public override void RestoreState(object state) 
{
 // return data to this stats item
}

I will sort out this data later… (might need some guidance on it as well) but for now the instantiated items are equipping, able to be collected all like a normal pick up but will not remain when saved and loaded.

Inside the Equipment ISaveable members is there something I’ve done incorrect (i’d rather take this one step at a time and get the functionality of equipment storing the correct data and returning that before tackling the random attributes part ^.^

Equipment.cs


    [Serializable]
    public struct ItemStruct
    {
        public string itemID;
        public object state;
    }

    object ISaveable.CaptureState()
    {
        var equippedItemsForSerialization = new Dictionary<EquipLocation, ItemStruct>();
        ItemStruct itemStruct = new ItemStruct();

        foreach (var pair in equippedItems)
        {
            itemStruct.itemID = pair.Value.GetItemID();
            itemStruct.state = pair.Value.CaptureState();
            equippedItemsForSerialization[pair.Key] = itemStruct;
        }
        return equippedItemsForSerialization;
    }

    void ISaveable.RestoreState(object state)
    {
        equippedItems = new Dictionary<EquipLocation, EquipableItem>();

        var equippedItemsForSerialization = (Dictionary<EquipLocation, ItemStruct>)state;

        foreach (var pair in equippedItemsForSerialization)
        {
            var item = (EquipableItem)InventoryItem.GetFromID(pair.Value.itemID);
            if (item != null)
            {
                equippedItems[pair.Key] = Instantiate(item);
                equippedItems[pair.Key].RestoreState(pair.Value.state);
            }
        }
        equipmentUpdated?.Invoke();
    }

Oh and atm there is no serialization errors occuring, however, it does come up that “abstract class cannot be instantiated.” when I load my file.

LogWarning after Loading

The class named 'Game.Inventories.EquipableItem' is abstract. The script class can't be abstract!
UnityEngine.Resources:LoadAll<Game.Inventories.InventoryItem> (string)

I noticed your EquipableItem was abstract so I made mine abstract, but I think you’re using abstract for a more specific reason… I’m assuming? (bad to assume)!

Anyways you’re always a wonderful help and I hope this isn’t too much to ask from you, if it is please don’t be afraid to politely say "Sorry, I cannot help you further* at any time!

Thsi is just so that -=something=- is returned. The CaptureState/RestoreState in InventoryItem is useless, but it keeps the system from crashing is something doesn’t override it in a subclass.

My specific reason is that if it isn’t a StatsEquipableItem, it’s really quite useless, and I’m not fond of useless loot.
Go ahead and change that back from abstract, and this should go away.

Thsi is just so that -=something=- is returned. The CaptureState/RestoreState in InventoryItem is useless, but it keeps the system from crashing is something doesn’t override it in a subclass.


Oh thanks for that clarification regarding – more or less they’re just there to make it less prone to erroring and only returning anything if it so happens to go beyond from the other subclasses.

Any ideas on what I could be looking at for the Items to be re-instantiated back to the equipped slots after saving/loading?

Edit: Just to be super clear, I mean for the instantiated scriptable objects, not the items that are equipped when manually placed in the scene or already existing as an asset. Those work normal.


I’m going to go over some of your equipment.cs capture/restore states again to double reference check, but I’m thinking I might’ve missed a minute detail, something small or another part of the puzzle

By overviewing the equipment code from my previous post is there anything that pops out as an immediate red flag for why the scriptable object won’t restore its state after being equipped?

It looks right, Show me the capture/restorestates of the item you’re trying to save/restore

1 Like

Sorry for the late response.

Current States:

At this moment, my capture / restore states aren’t doing too much outside of what I have in the Equipment.cs members.

InventoryItem members:

        public virtual object CaptureState(){ return itemID; } // doesn't matter what we return
        public virtual void RestoreState(object state){ }

StatsEquipableItem members:

        public override object CaptureState()
        {
            return base.CaptureState();
            // return the modifiers of this
        }
    public override void RestoreState(object state)
    {
        // item needs to retrieve its state about modifiers
    }

Reasoning:

I figured because the Equipment.cs members were going to handle capturing the new instantiated instance as an equipped Item and returning it without the need of the subclasses capture/restore states being filled out yet.

Useful Context:

I have a ItemRandomizer.cs MonoBehaviour in my scene. For testing purposes when I hit ‘Q’ it instantiates a new instance of a random base item (stats equipable item). The only option temporarily, but maybe permanently is a stat item as shown below.

inspector

The list you seen in the inspector.

    [SerializeField] List<StatsEquipableItem> itemLibrary = new List<StatsEquipableItem>();

That asset item has no details currently besides a generic pick up spawner and it’s itemID.

ItemRandomizer Methods:

Calling the Method to instantiate a new instance of the list’s stats item.

    void Update()
    {
        if (Input.GetKeyDown(KeyCode.Q))
        {
            CreateNewItem();
        }
    }

    public void CreateNewItem()
    {
        int randomIndex = Random.Range(0, itemLibrary.Count);
        SetUpEquipableModifierItem(itemLibrary[randomIndex]);
    }

Then here’s the process of changing the item.

    StatsEquipableItem SetUpEquipableModifierItem(StatsEquipableItem statItem)
    {
        // TESTING INSTANTIATION
        statItem = ScriptableObject.CreateInstance<StatsEquipableItem>();

        GameObject player = GameObject.FindGameObjectWithTag("Player");
        statItem.SetItemID(Guid.NewGuid().ToString());
        statItem.SetSpriteIcon(icon);
        statItem.SetItemPrice(Random.Range(0f, 300f));
        statItem.SetEquipLocation(EquipLocation.Gloves);
        statItem.SetItemCategory(ItemCategory.Armour);
        statItem.AddRandomStat(player);
        statItem.SetDisplayName($"Modifier Item");
        statItem.SetSpawnPickup(SetUpGenericSpawnPickup(prefab));
        SpawnGenericPickup(statItem.SpawnPickup(Vector3.zero, 1));
        return statItem;

        // TESTING INSTANTIATION
    }

The setters are just simple setters inside InventoryItem that allows me to assign the icon, price, description, displayname, it’s pick up spawner, where it gets equipped, it’s item category and lastly changing the instantiated item’s UUID.

Sorry for the more detailed post but it might shine some light in what I’m doing incorrectly execution wise.

That changing of the UUID means that there is nothing to grab on to for re-instantiating the item… It’s our hook into the Resources.

A better approach is to have a collection of “base” StatsEquipableItems, which are mostly pre-configured, except for the stats…

For example, you might have 5 glove bases, each with a different icon, similarly with each category (I know, there’s a lot of setup in this, but what you’ve just shown me won’t work if you want to save the data).

So you have your collection of droppable items, and you select one at random and Instantiate a copy.

I’d start by adding random stat(s).
Now you change the DisplayName to something more random (I do mine based on the most prominent stat)
Set your price.
EquipLocation and ItemCategory should already be set in the template class, as well as the pickup.

In the end, you’ll have a new SO that exists in memory with the same ItemID I can not emphasize enough how important this is, changing the ItemID immediately removes the SO from being saveable.

Now given the parameters I’ve set up… here’s how CaptureState() looks in the StatsEquipableItem (broad strokes, not specific code)

[System.Serializable] 
public struct StatsItemEntry
{
     public string displayName;
     public float price;
     public Dictionary<Stat, float> additiveModifiers;
     public Dictionary<Stat, float> percentageModifiers;
}

public override object CaptureState()
{
    StatsItemEntry result = new StatsItemEntry();
    result.displayName = displayName;
    result.price = price;
    result.additiveModifiers = additiveModifiers;
    result.percentageModifiers = percentageModifiers;
    return result;
}

public void RestoreState(object state)
{
     if(state is StatsItemEntry entry)
     {
          description = entry.description;
          price = entry.price;
          additiveModifiers = entry.additiveModifiers;
          percentageModifiers = entry.percentageModifiers;
      }
}
1 Like

If I ever visit the US i’m buying you a beer. You single handedly turned into thanos and was like “I’m about to end this dudes career with a broad stroke of code” xD

On a serious note: It appears I’ve successfully been able to instantiate a base stats equipable item that doesn’t share any relevancy in terms of data and is saving both inside the inventory, and the equipment along with the relevant stats and value.

One small edit: Should I be doing return statItem = ScriptableObject.CreateInstance<StatsEquipableItem>(); ? or just returning the ‘statItem’ as is.

Here’s the current code

StatsEquipableItem.cs

        [Serializable]
        public struct SaveStatItems
        {
            public string displayName;
            public string description;
            public float price;
            public List<Modifier> additiveModifiers;
            public List<Modifier> percentageModifiers;
        }

        public override object CaptureState()
        {
            SaveStatItems saveData = new SaveStatItems();

            saveData.additiveModifiers = new List<Modifier>();
            saveData.percentageModifiers = new List<Modifier>();

            saveData.displayName = DisplayName;
            saveData.description = Description;
            saveData.price = Price;

            saveData.additiveModifiers = additiveModifiers;
            saveData.percentageModifiers = percentageModifiers;

            return saveData;
        }

        public override void RestoreState(object state)
        {
            if (state is SaveStatItems save)
            {
                DisplayName = save.displayName;
                Description = save.description;
                Price = save.price;
                additiveModifiers = save.additiveModifiers;
                percentageModifiers = save.percentageModifiers;
            }
        }

ItemRandomizer.cs

    StatsEquipableItem SetUpEquipableModifierItem(StatsEquipableItem statItem)
    {
        GameObject player = GameObject.FindGameObjectWithTag("Player");
        statItem.AddRandomStat(player);
        statItem.SetDisplayName($"Modifier Gloves Item");
        statItem.SetDescription($"Stat Modifier Gloves");
        statItem.SetSpawnPickup(SetUpGenericSpawnPickup(prefab));
        SpawnGenericPickup(statItem.SpawnPickup(Vector3.zero, 1));
        return statItem = ScriptableObject.CreateInstance<StatsEquipableItem>();
        // return statItem;
    }

I still utilize the pick up generic code I made inside ItemRandomizer.cs, because, when instantiating the new item, the ‘SpawnPickup’ from the schools InventoryItem code errors out with this line: var pickup = Instantiate(this.pickup); when attempting to loot the object.

Inventory.cs members

        [Serializable]
        private struct InventorySlotRecord
        {
            public string itemID;
            public int number;
            public object state;
        }
    
        object ISaveable.CaptureState()
        {
            var slotStrings = new InventorySlotRecord[inventorySize];
            for (int i = 0; i < inventorySize; i++)
            {
                if (slots[i].item != null)
                {
                    slotStrings[i].itemID = slots[i].item.GetItemID();
                    slotStrings[i].number = slots[i].number;
                    slotStrings[i].state = slots[i].item.CaptureState();
                }
            }
            return slotStrings;
        }

        void ISaveable.RestoreState(object state)
        {
            var slotStrings = (InventorySlotRecord[])state;
            for (int i = 0; i < inventorySize; i++)
            {
                InventoryItem item = InventoryItem.GetFromID(slotStrings[i].itemID);

                if (!item) continue;
                if (!item.IsStackable())
                {
                    InventoryItem newItem = Instantiate(item);
                    newItem.RestoreState(slotStrings[i].state);
                    item = newItem;
                }

                slots[i].item = item;
                slots[i].number = slotStrings[i].number;
            }
            inventoryUpdated?.Invoke();
        }

I have a duplication bug that is seldom occuring, it seems like sometimes it happens, other times it doesn’t. This is how I managed to reproduce it. Also I edited the post above to include the capture / restore states for my Inventory.cs members. (It appears it might be from that)

Reproduce Bug process:

  1. I Instantiate the first item, this auto equips to my equipment upon collecting it.
  2. I then Instantiate another item of the same stats equipable item but don’t collect it.
  3. I save and then collect the second instantiated item.
  4. After picking up the item, I load and then collect the pick up again. That item occasionally places two items into my Inventory. One is blank, the other has the correct data on it.

Also another rare abnormality is at times if I instantiate a drop, it’s visible I know it’s there, save, then hit reload, sometimes it vanishes. But that’s really rare too.

When you drop the item for the first time, you’re going to ScriptableObject.CreateInstance (don’t worry about casting, it will be inferred by the SO you pass as a parameter to CreateInstance, make mods (on the instance) and return the instance you created.
When you restore the item, you’re going to ScriptableObject.CreateInstance the result of GetFromID, call the instance’s RestoreState with the state you provided.

Make it a Diet Dr. Pepper and you’re on. Oh, and my snap of the finger isn’t to end your career, it’s to help you make your career. :slight_smile:

1 Like

Privacy & Terms