Variable Stat Modifiers

I am trying to work out how to make variable stats on my equipment (like diablo/PoE). For example with the healthy hat having health vary between 1 and 500 (for testing), rolled when the item is created.

I have tried something like this in StatsEquippableItem:

    public void SetModifiers()
    {
        int additiveLength = additiveModifiers.Length;

        Modifier newModifier = new Modifier
        {
            stat = Stat.Health,
            value = Random.Range(1, 500)
        };
        
        additiveModifiers[additiveLength] = newModifier;
    }

My problem is that I can’t work out how to call this code in the first place through the inventory system. If someone could advise whether I am vaguely on the right lines with this, or if I am trying to do something this inventory system isn’t really geared for?

Thanks in advance!

Continuing to research this, I think my premise of trying to do this on the scriptable object is wrong (as far as I can tell Scriptable objects are meant to be fairly static data containers).

This leaves me a little stumped as to how to progress from the static modifiers set manually on the Scriptable Object, to having the same “base” item, but each one with unique stats.

This is quite doable (I do it in my own projects).

  • You’ll need to make your InventoryItems implement the ISaveable interface
  • InventoryItem.CaptureState should be virtual and return null. InventoryItem.RestoreState should be virtual and contain an empty block.
  • Your StatsInventoryItem should override Capture and RestoreState and return/receive a data structure with the random stats… i use a Dictionary<int, float> and for the int, I convert the Stat enum to an int and back again in restore.
  • When you drop an EquipableItem, Instantiate it instead of simply referencing it. Run the randomization on the instantiated SO and pass this object reference around instead of the original SO.
  • Your Inventory and StatsEquipment classes will need to store the slot, the ItemID (which will be the same as the original SO) and the result of the CaptureState for each InventoryItem. When they RestoreState, they’ll once again need to Instantiate the item and call RestoreState() on the item with the cached state from the save.
1 Like

Hi Brian,

Thanks for the pointers, I am working through your points and am now a little stuck. I believe I have done the following (albeit with fairly placeholdery testing logic)

  • You’ll need to make your InventoryItems implement the ISaveable interface
  • InventoryItem.CaptureState should be virtual and return null. InventoryItem.RestoreState should be virtual and contain an empty block.

Added Isaveable interface to InventoryItem and implemented as follows:

  public virtual object CaptureState()
    {
        return null;
    }

    public virtual void RestoreState(object state)
    {
        //deliberately empty
    }
  • Your StatsInventoryItem should override Capture and RestoreState and return/receive a data structure with the random stats… i use a Dictionary<int, float> and for the int, I convert the Stat enum to an int and back again in restore.

Implemented as follows:

using System;
using System.Collections.Generic;
using GameDevTV.Inventories;
using RPG.Stats;
using UnityEngine;
using Random = UnityEngine.Random;


namespace RPG.Inventories
{

[CreateAssetMenu(menuName = ("RPG/Inventory/Equipable Item"))]
    public class StatsEquippableItem : EquipableItem, IModifierProvider
    {
        

        [SerializeField] private Modifier[] additiveModifiers;
        [SerializeField] private Modifier[] percentageModifiers;


        private Dictionary<int, float> storedModifiers;
        
        //[System.Serializable]
        struct Modifier
        {
            public Stat stat;
            public float value;
        }

        private void Awake()
        {
            SetModifiers();
            SetDictionary();
        }

        public IEnumerable<float> GetAdditiveModifiers(Stat stat)
        {
            foreach (var modifier in additiveModifiers)
            {
                if (modifier.stat == stat)
                {
                    yield return modifier.value;
                }
            }
        }

        public IEnumerable<float> GetPercentageModifiers(Stat stat)
        {
            foreach (var modifier in percentageModifiers)
            {
                if (modifier.stat == stat)
                {
                    yield return modifier.value;
                }
            }
        }

        private void SetDictionary()
        {
            storedModifiers = new Dictionary<int, float>();

            //TODO: make work for multiplicative modifiers as well
            foreach (var additivemodifier in additiveModifiers)
            {
                storedModifiers.Add(ConvertStatToInt(additivemodifier.stat), additivemodifier.value);
            }
        }
        
        public void SetModifiers()
        {
            Modifier newModifier = new Modifier
            {
                stat = Stat.Health,
                value = Random.Range(1, 500)
            };
        
            additiveModifiers[0] = newModifier;
        }

        private int ConvertStatToInt(Stat stat)
        {
            return (int)stat;
        }

        private Stat ConvertIntToStat(int stat)
        {
            return (Stat)stat;
        }



        public override object CaptureState()
        {
            return storedModifiers;
        }

        public override void RestoreState(object state)
        {
            foreach (var entry in (Dictionary<int, float>)state)
            {
                additiveModifiers[0].stat = ConvertIntToStat(entry.Key);
                additiveModifiers[0].value = entry.Value;
            }
        }


    }
}

(I have no idea if the above works yet, as this is where I get stuck)

Your next point is:

  • When you drop an EquipableItem, Instantiate it instead of simply referencing it. Run the randomization on the instantiated SO and pass this object reference around instead of the original SO.

Could you possibly expand on how I do this? I am thinking this is done in inventoritem under the “SpawnPickup” but not clear on how I can achieve what you suggest - especially the bit about passing around after.

Many thanks!

Ordinarily within our Inventory system, we simply pass a reference to the InventoryItem in question… so our InventorySlot, for example, looks something like this:

        public struct InventorySlot
        {
            public InventoryItem item;
            public int number;
        }

So generally speaking, the item in our InventorySlot refers to the ScriptableObject directly in our Assets. When using procedural stats, this is a problem because the ScriptableObject is a shared resource… So if you have a Dr’s Fez of Unusual Time Travel and the enemy has a Dr’s Fez of Unusual Time Travel, changing the random stats on the one changes the random stats on the other because they are literally the same InventoryItem.

The solution for this is to create a temporary copy of the ScriptableObject in memory to work on. This is accomplished much like instantiating a prefab. Given any InventoryItem, you can create this copy like this:

InventoryItem instantiatedItem = Instantiate(originalItem);

Now you can use this instantiatedItem as if it were the original item, but any changes you make will only affect that copy of the item.

Of course, you don’t want to instantiate things like stackables, or they won’t stack. This is because once you’ve instantiated the item

if(instantiatedItem == originalItem)

will be false…
So I do a quick check when retrieving an item…

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

Hi Brian,

Thanks for all your help so far and I think I am nearly there!

I have implemented the instantiation as shown in InventoryItem:

 public Pickup SpawnPickup(Vector3 position, int number)
        {
            var pickup = Instantiate(this.pickup);

            if (this is not ActionItem ai)
            {
                InventoryItem instantiatedItem = Instantiate(this);
                
                if (instantiatedItem is StatsEquippableItem sei)
                {
                    sei.SetModifiers();
                }
                
                pickup.Setup(instantiatedItem, number);
            }
            else
            {
                pickup.Setup(this, number);
            }
            
            
            pickup.transform.position = position;
            return pickup;
        }

This successfully creates the variable health hats.

I have taken a stab at the final step of saving/restoring and have got as far as the correct number of hats appear in my inventory at load, but they don’t have the randomised stats and throw object reference exceptions when trying to equip.

In Inventory.cs I have implemented capture and restore as follows:

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;
                    if (slots[i].item is StatsEquippableItem sei)
                    {
                        slotStrings[i].storedModifiers = (Dictionary<int, float>)sei.CaptureState();
                    }
                    else slotStrings[i].storedModifiers = null;
                }
            }
            return slotStrings;
        }

        void ISaveable.RestoreState(object state)
        {
            var slotStrings = (InventorySlotRecord[])state;
            for (int i = 0; i < inventorySize; i++)
            {
                if (slots[i].item != null)
                {
                    InventoryItem newItem = Instantiate(slots[i].item);

                    if (newItem is StatsEquippableItem)
                    {
                        print("Stats Item Created");
                    }
                    else
                    {
                        print("Other type of item created");
                    }
                    
                    newItem.RestoreState(slotStrings[i].storedModifiers);
                    slots[i].item = newItem;
                }
                
                slots[i].item = InventoryItem.GetFromID(slotStrings[i].itemID);
                slots[i].number = slotStrings[i].number;
            }
            if (inventoryUpdated != null)
            {
                inventoryUpdated();
            }
        }

Interestingly the “stats item created” or “other type of item created” messages aren’t firing. Note I haven’t implemented the safety check for stackable items (yet) as for my testing I only have hats dropping till I get this sorted.

One problem I would wager is this case not being right: slotStrings[i].storedModifiers = (Dictionary<int, float>)sei.CaptureState();

Any last help you can provide to help me get this architecture in appreciated, then I promise to leave you alone :slight_smile:

seems I was so close, bit of reordering of my logic, and we are in business:

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

                if (slots[i].item != null)
                {
                    InventoryItem newItem = Instantiate(slots[i].item);

                    if (newItem is StatsEquippableItem)
                    {
                        print("Stats Item Created");
                    }
                    else
                    {
                        print("Other type of item created");
                    }

                    slots[i].item = newItem;
                    newItem.RestoreState(slotStrings[i].storedModifiers);
                }

            }
            if (inventoryUpdated != null)
            {
                inventoryUpdated();
            }
        }

Now I have the basic framework I hopefully will be good from here :smiley:

Remember that InventorySlotRecord.item is a string, not an InventoryItem

InventoryItem newItem = InventoryItem.GetFromID(slotStrings[i].item);
if(newItem is StatsEquippableItem statsEquipableItem)
{
     Debug.Log($"Stats Item {statsEquipableItem.GetDisplayName()} retrieved, instantiating");
     newItem = Instantiate(statsEquipableItem);
} else
{
     Debug.Log($"Item {newItem.GetDisplayName()} is not a StatsEquipableItem, not instantiating clone.");
}
newItem.RestoreState(slotsStrings[i].storedModifiers);
slots[i].item = newItem;
slots[i].number = SlotStrings[i].number;

Remove the last 2 lines in the foreach block, as you’re already setting the correct items here, and

slots[i].item = InventoryItem.GetFromID(slotStrings[i].itemID);
slots[i].number = slotStrings[i].number;

will overwrite what you just did in the previous fragment of code and leave you with the unmodified originals again.

This topic was automatically closed 24 hours after the last reply. New replies are no longer allowed.

Privacy & Terms