Equipment slots and multiple rings/trinkets/etc

Hello, I’ve finished the Equipment System part of the RPG Inventory Systems tutorial and noticed that it currently only works if a character has only 1 of each type of equipment slot. A lot of MMOs have multiple slots for jewellery and the ability to wield 1-handed weapons in either the primary or off-hand weapon slots.
I’ve seen a few posts asking about this type of thing before I wasn’t able to figure out what to do from the answers and there didn’t seem to be any follow-up with the OP actually getting it working themselves.

From what I can tell, I’m looking for a way to tell EquipmentSlotUI.cs which equipment slot the item is actually being dropped into. I feel like I must be missing something really obvious because it is checking if the item shares the same EquipLocation as the equipment slot, which means at some point it must know the slot itself that the item is going to, otherwise you would surely be able to equip items by dropping them into any slot and they would automatically equip themselves to the correct locations.

Im like 99% positive that we covered this somewhere in one of the 4 courses, because I have this implemented in my game… I just don’t remember which :sweat_smile:

Currently, the Equipment is set up to take in one and one only item per Equipment location.

So here’s what happens behind the scenes when we equip an item…
Our equipment is currently being stored in a Dictionary<EquipLocation, EquipableItem>. While this makes it very easy to locate and access equipment based on it’s location, Dictionaries use keys which (by nature) must be unique. This is where the problem lies. Any solution invariably has to change the underlying data structure so that in addition to the location, we’re also saving a slot within that location…

Once that is done, we also have to change our Equipment Slot UI so that in addition to taking in a location, it also takes in a slot within that location…

You would think I’d have saved some of the previous ideas, but I don’t currently have a copy of the course project implementing slots within the equipment…

So let’s start on one that I’ll be sure to commit to one of my projects…

We’ll start with Equipment, as that’s the underlying data structure. Currently, we use:

        // STATE
        Dictionary<EquipLocation, EquipableItem> equippedItems = new Dictionary<EquipLocation, EquipableItem>();

So the first thing we need to do is to change the Key that addresses our equipment.

Now while you can have a Dictionary with a compound key (like our Progression dictionary that uses a

Dictionary<CharacterClass, Dictionary<Stat, float[]>>

This simply won’t work for a key. What we need is a way to have a key that combines the EquipLocation and a slot number…
Fortunately, you can use a struct for this, but we need to do a little business to allow this to work correctly and predictably…
We’ll start with the basic struct:

    public struct EquipmentSlot
    {
        public EquipLocation equipLocation;
        public int slot;
    }

Simple enough, we can use this for our Dictionary

        Dictionary<EquipmentSlot, EquipableItem> equippedItems = new Dictionary<EquipmentSlot, EquipableItem>();

Now right away, if you do this, you’ll find that your compiler screams bloody murder with a ton of errors. What we have to do from here is follow the errors… But first, we need to make sure that if I have the same EquipLocation and int in my struct, that the Dictionary will always see that as the same key. We do that by overriding a couple of methods within the struct that are common to all objects (in general, we don’t have to worry about this within MonoBehaviours, but with custom classes, we do if we want to have equality comparison).

We need to override two methods to ensure this, GetHashCode, and Equals.

    [System.Serializable]
    public struct EquipmentSlot
    {
        public EquipLocation equipLocation;
        public int slot;

        public override int GetHashCode()
        {
            return HashCode.Combine(equipLocation, slot);
        }

        public override bool Equals(object obj)
        {
            if (obj is EquipmentSlot other)
            {
                return (other.equipLocation == equipLocation && other.slot == slot);
            }
            return false;
        }
    }

Now we’re ready to begin the task of addressing the large amount of errors that have crept up (Ok, it’s only 9, but that number will likely rise before it shrinks)


Most of these are because all of our methods are trying to access the Dictionary incorrectly. We’ll just follow them along one by one…

Our first error is in GetItemInSlot. We take in an EquipLocation and use it as an index into the equippedItems. This method actually has two such errors.

       public EquipableItem GetItemInSlot(EquipLocation equipLocation)
        {
            if (!equippedItems.ContainsKey(equipLocation))
            {
                return null;
            }

            return equippedItems[equipLocation];
        }

So the decision we need to make, and this is the time to make it, is if we want to take in an EquipmentSlot in our methods, or if we want to construct an EquipmentSlot by taking in an EquipLocation and an integer slot. The first approach may actually be the least amount of refactoring, and our EquipmentSlotUI will only have to keep track of an EquipmentSlot instead of both an EquipLocation and Slot.
With that in mind, we’ll clean up all of our methods one by one by changing EquipLocation to EquipSlot. First up, let’s handle GetItemInSlot(). Along the way, we’ll modernize the code to take care of some better language features in C#
Note: Not all methods in this teardown may be in your Equipment.cs, depending on which courses you have completed, you can ignore these methods or simply include them for the future

        public EquipableItem GetItemInSlot(EquipmentSlot equipmentSlot)
        {
            return equippedItems.TryGetValue(equipmentSlot, out EquipableItem item) ? item : null;
        }

If you’re wondering what I did there, I used a Ternery Operator (the ?)… before the ? is what would be inside of an if statement. In this case, I used a feature of Dictionaries, TryGetValue. If TryGetValue returns true, then the item is valid, and it’s returned (the value after the ?), then a : goes before the result if the TryGetValue failed, it returns null.

The next one is a bit trickier, as we’re going to have to do some sanity checking:

        public void AddItem(EquipLocation slot, EquipableItem item)
        {
            if (!IsValidEquipLocation(slot, item)) return;
            equippedItems[slot] = item;

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

In this case, we’re checking to make sure that we can actually equip the item in that slot. We’ll deal with the EquipLocation for now, and see what we can do about restricting the slot number later (we don’t, for example, want to wear two helmets or two pairs of chain leggings).

        public void AddItem(EquipmentSlot slot, EquipableItem item)
        {
            if (!IsValidEquipLocation(slot.equipLocation, item)) return;
            equippedItems[slot] = item;
            equipmentUpdated?.Invoke();
        }

In this case, we merely need to get the equipLocation from the slot and test that against IsValidEquipLocation. QED.

Next up is RemoveItem

        public void RemoveItem(EquipLocation slot)
        {
            equippedItems.Remove(slot);
            if (equipmentUpdated != null)
            {
                equipmentUpdated();
            }
        }

Once again, fairly easy:

        public void RemoveItem(EquipmentSlot slot)
        {
            equippedItems.Remove(slot);
            equipmentUpdated?.Invoke();
        }

We’re getting closer, but you might notice that as our errors drop in Equipment.cs, more errors are cropping up in other files. We’ll get to those.

        public IEnumerable<EquipLocation> GetAllPopulatedSlots()
        {
            return equippedItems.Keys;
        }

becomes

        public IEnumerable<EquipmentSlot> GetAllPopulatedSlots()
        {
            return equippedItems.Keys;
        }

Capturing and Restoring
Note: I am no longer directly supporting the legacy BinaryFormatter version of the saving system, as it is not safe. Going forward, I am providing Json based saving methods. For more information, see my tutorial at Json 1 Introduction and Installation · Wiki · Brian K. Trotter / RPGMods · GitLab

Here is the Capture/Restore

        public JToken CaptureAsJToken()
        {
            JObject state = new JObject();
            foreach (var pair in equippedItems)
            {
                JObject slotKey = new JObject();
                slotKey["EquipLocation"] = pair.Key.equipLocation.ToString();
                slotKey["Slot"] = pair.Key.slot; 
                state[slotKey.ToString()] = JToken.FromObject(pair.Value.GetItemID());
            }
            return state;
        }

        public void RestoreFromJToken(JToken state)
        {
            if(state is JObject stateObject)
            {
                equippedItems.Clear();
                
                foreach (var pair in stateObject)
                {
                    JToken slotToken = JToken.Parse(pair.Key);
                    if (slotToken is JObject slotKey && slotKey.TryGetValue("EquipLocation", out JToken token) && slotKey.TryGetValue("Slot", out JToken sToken))
                    {
                        if (Enum.TryParse(token.ToString(), true, out EquipLocation key))
                        {
                            if (InventoryItem.GetFromID(pair.Value.ToObject<string>()) is EquipableItem item)
                            {
                                EquipmentSlot slot = new EquipmentSlot(key, sToken.ToObject<int>());
                                equippedItems[slot] = item;
                            }
                        }
                    }
                    
                }
            }
            equipmentUpdated?.Invoke();
        }

One more method that doesn’t flag an error directly, but will matter later is

       public bool IsValidEquipLocation(EquipLocation location, EquipableItem item)
        {
            //Always return true if the location == the equiplocation
            if (item.GetAllowedEquipLocation() == location) return true;
            return false;
        }

This is another fairly easy one

       public bool IsValidEquipLocation(EquipmentSlot location, EquipableItem item)
        {
            //Always return true if the location == the equiplocation
            if (item.GetAllowedEquipLocation() == location.equipLocation) return true;
            return false;
        }

Next up, we’ll get to the new errors we’ve created

1 Like

We did something in a thread that tested for rings, but it was far from a robust solution.

OK I know this is not my questionnaire to ask questions, but is this similar to what I’m trying to do? (I read the title, was just curious… from what I understood, OP is trying here to implement multiple equipment slots). If so, I’d be interested to see where you guys take this as well, as I want to learn along :slight_smile:

This isn’t about modular equipment on the model, but about being able to equip multiple of some items. The previous version we worked on was not terribly robust. This one is a bit more in depth, but not quite finished. I’ll have to complete it over the next few days, as I’m out of time for the evening…

1 Like

Thanks Brian for the fast and super in-depth reply!
I’ve implemented most of the changes you listed, the only thing I left for now is the Json part as I’m not through the inventory series and I’m worried changing something like that might conflict with something later on, but I will be sure to come back, watch your tutorial and change it after I’m sure I’m past all the saving related stuff.
The Terney Operator is really cool, I’ve seen it used a few times before but keep forgetting what it does, it definitely seems like something that would be helpful to remember as it makes the code look a lot cleaner.
The “GetAllPopulatedSlots” method wasn’t in my code at all but you mentioned that might be the case that some are added later, either way I added it in.

The only errors remaining seem to be on “slot.equipLocation” when calling AddItem or a handful of methods called in EquipmentSlotUI, but I believe you said we’d get to that later.
image

I tried taking a crack at it myself, but didn’t have any luck, here was my process:

  • Hovering over the error, it says ‘EquipLocation cannot be converted to EquipmentSlot’, but if I change it to equipmentSlot that ‘doesn’t exist in the current context’, so I tried changing the EquipLocaction in the config data section of EquipSlotUI to EquipmentSlot.
  • Now that it was being referenced, this fixed everything apart from the “equipLocation” compared when GetAllowedEquipLocation is called in MaxAcceptable, so instead I undid my change to config data and added in EquipmentSlot rather than overwrite EquipLocation.
  • This still left the error on AddItem as posted above and I didn’t want to mess with what you suggested, so I ended up reverting my changes and simply waiting.

You know, that’s a very silly one… I missed it the first time because it didn’t generate an error right away (IsValidEquipLocation didn’t at first, either, until I started preparing the EquipmentSlotUI script.
When I fixed IsValidEquipLocation, I didn’t realize I triggered a new error in AddItem.
We simply need to pass the slot, not the slot.equipLocation into IsValidEquipLocation.

        public void AddItem(EquipmentSlot slot, EquipableItem item)
        {
            if (!IsValidEquipLocation(slot, item)) return;
            equippedItems[slot] = item;
            equipmentUpdated?.Invoke();
        }
1 Like

I got sidetracked this evening, but I’ll get the EquipmentSlotUI script up tommorrow, I promise.

1 Like

Ok, at this point, we have lots of errors created by changing EquipLocation to EquipmentSlot in our Equipment.cs script. Believe it or not, we’re going to tackle most of them in one go:

So our principle problem is that in EquipmentSlotUI, we’re using an EquipLocation instead of an EquipSlot. So let’s start by simply changing the definition of equipLocation from an EquipLocatio to an EquipmentSlot

        [SerializeField] private EquipmentSlot equipLocation;

Now just like that, our errors have dissappeared, except that now we have a new error.


If we look we’ll see that our error is here:

        public int MaxAcceptable(InventoryItem item)
        {
            EquipableItem equipableItem = item as EquipableItem;
            if (equipableItem == null) return 0;
            if (!playerEquipment.IsValidEquipLocation(equipLocation, equipableItem)) return 0;
            if (!equipableItem.IsEquippableByClass(playerEquipment.GetComponent<BaseStats>().GetCharacterClass()))
                return 0;
            if (!equipableItem.CanEquip(equipLocation, playerEquipment)) return 0;
            if (GetItem() != null) return 0;

            return 1;
        }

Now it’s entirely possible that your method may not look like this, as but I’m pointing it out just in case you’re using the equipableItem.CanEquip() method. In this case, the fix is quick

        public int MaxAcceptable(InventoryItem item)
        {
            EquipableItem equipableItem = item as EquipableItem;
            if (equipableItem == null) return 0;
            if (!playerEquipment.IsValidEquipLocation(equipLocation, equipableItem)) return 0;
            if (!equipableItem.IsEquippableByClass(playerEquipment.GetComponent<BaseStats>().GetCharacterClass()))
                return 0;
            if (!equipableItem.CanEquip(equipLocation.equipLocation, playerEquipment)) return 0;
            if (GetItem() != null) return 0;

            return 1;
        }

There is one other spot we need to fix before we can move on to the Unity Editor
In Fighter.cs, we try to get the item equipped in EquipSlot.Weapon.

        private void UpdateWeapon()
        {
            var weapon = equipment.GetItemInSlot(EquipLocation.Weapon) as WeaponConfig;
            if (weapon == null)
            {
                EquipWeapon(defaultWeapon);
            }
            else
            {
                EquipWeapon(weapon);
            }
        }

For now, we can simply create a new EquipmentSlot(EquipLocation.Weapon, 0) and pass that in

        private void UpdateWeapon()
        {
            EquipmentSlot slot = new EquipmentSlot(EquipLocation.Weapon, 0);
            var weapon = equipment.GetItemInSlot(slot) as WeaponConfig;
            if (weapon == null)
            {
                EquipWeapon(defaultWeapon);
            }
            else
            {
                EquipWeapon(weapon);
            }
        }

Now that these things are done, we can finally go back in to Unity with compiled code and adjust our Equipment Dialogue
Open up the Equipment window and find each slot. You’ll see they’ve all been reset to a default of Helmet, 0. Simply set each slot to the appropriate slot type and you’ll be good to go.

In terms of functionality, you can add as many EquipmentSlotUI prefabs as you want to the window with different slots… for example, you could add 9 more ring slots (for mortal men, doomed to die), all Ring, each with their own slots. You could, if you wanted, have multiple helms, Body, etc… Mind you, this won’t make a lot of sense to players.
One of the goals here was to have an off-hand weapon. To make this functional, this will require more work, which we’ll tackle tommorow (or this weekend, depending on my time).
It may also be handy to have a mechanism to specify just how many of each Equipment slot is acceptable.

1 Like

Wow! This solution is amazing! One thing I really appreciate is code that is scalable, a lot tutorials I see don’t have that and usually also only work specifically the way they showcase them in the tutorial video.
Having multiple jewellery slots is one thing, but I think this would also allow someone to for example have a game where you control multiple characters and in that case you would need to equip multiple helms and bodies like you mentioned.

It’s reassuring to see too that when I went through my process of attempting to fix the errors on my own, I was on the right track. :smiley:

For MaxAcceptable, I don’t yet have IsEquippableByClass and CanEquip anywhere in my code, so I just commented them in for now as I’m guessing those will come up later in the “Integrating the Inventory System” section of the tutorial, the same goes for the next part with UpdateWeapon as there is no Fighter.cs yet, but I was able to make the other changes and I’m happy to say that equipping multiple of the same item now works! :grin:

Here’s the inventory I’ve been creating with the slots now working:

I tried testing a bunch of things to make sure everything runs correctly, and I did come across one issue, I’m not sure if this is because I’m missing “IsEquippableByClass” and “CanEquip” at the moment, but if I drag an item that doesn’t belong in an equipment slot that currently holds an item, it will boot equipped item out and send it to the first empty slot of the inventory, this also happens even if the item dragged cannot be equipped.
This seems similar to another issue I came across and was unable to fix with the inventory when I was setting a stack limit to my items, where if have full stack of an item and a stack that isn’t full, if I drag the full stack of items to another inventory slot, it will automatically move out as many items from that stack as possible and put them into the stack which wasn’t full. And if I drop any stackable item onto an item of the same type, it will automatically move the one I dragged to the first empty slot of the inventory.

Thanks so much for all the help so far! I’m sure this thread will become the definitive thread for upgrading the equipment system that everyone after me who comes looking and of course myself are immensely grateful for. :smiling_face_with_three_hearts:

In this case, I would actualy have a separate Equipment component on each character. Your UI would then link to the Equipment of the character you’re currently equipping.

Actually, the come into the game through another course, Shops and Ablilities, as well as some side tutorials… I am concerned, though… are you using the versions of the scripts from the Inventory.Zip in the integration section or the versions of the scripts through the instruction on each section. Many of these scripts are incomplete as we put just enough in them for the section at hand, and unintended side effects may occur…

Possibly such as this…

I think at the start of the tutorial I downloaded the main script assets, and then I’ve just been typing them out and following along, there have been a few times I’ve had to go find scripts because they’ve either not been typed up on screen, or only small sections of them were visible for me to write. But by this point in the tutorial series I’ve compared a lot of the scripts line by line because I’ve ran into errors and gone through to make sure that it wasn’t code related.

Oh, but it might be kicking it out of the slot because of the things I changed to try to get items to have a stack limit, I can try to comment that out and change it back to how it was and see if I still have the same issue with the equipment getting kicked.

I went through each of the relevant scripts in the tutorial project and made sure to comment out my item stack code, sadly the issue with equipment getting kicked out when an item is dropped on them is still happening. Is this not the case for your project?

I’ll check on this in the morning. This actually jogs my memory, may need a small patch to the Drag scripts.

I finished the tutorial module, the final parts seem to just be integrating the equipment and inventory into one’s main project, but I’d like to sort out the small issues with equipment getting kicked and the stacks jumping between each other first. I happened to notice during the action bar tutorial, there was a person who asked about the very same issue as equipment that the action bar will kick out an item on the bar if an item is dragged to it, even if the item dragged can’t go onto the bar.

I still need to sort that one out. I have an idea what’s going on there, I just need to dig into the DragItem.cs and tweak the code, and then maybe unit test it for every possible combination I can drum up.

1 Like

As mentioned in my other thread, I was able to finally resolve the last part of the equipment issues which was equipment getting booted from equipment slots when drag and dropping any item into them that doesn’t fit in that slot.
Sadly I found another issue when writing this where if a non-stackable being dragged to a slot is the same as the item that in the slot, it will delete one of the items, but I was able to fix this too.

This was my fix in the end for the booting of equipment:

I added this check to the bulk of the DropItemIntoContainerpart of DragItem.cs to check if the item is even allowed to be placed in the slot, and if it isn’t stackable, to make sure it’s not the same as the one already there.

I changed the return from 0 to 1 for this section of MaxAcceptable in EquipmentSlotUI.cs
image

Writing them out after, these seem like weirdly simple solutions, but it took hours of debugging and trial and error to find this.
It would be amazing to have a future proofed version of this where the item’s ID is compared rather than the item, but as far as I can tell, the system doesn’t differentiate items by an ID. I can see a few issues cropping up in the future and with other people who would want to use this code where items with durability and enchantments and enhancements are only having their “item” compared and therefore would not be directly swappable despite having different properties to the one currently in the slot.

Thanks so much Brian for all the help with the revamped equipment slots, I’m not sure if multiple solutions can be marked on these threads but I will start at your first one. :smiley:

The story of my life. It is often the smallest things that can take forever to nail down.

I tend to agree. In fact, I do a great deal of ID comparisons in my own versions of these scripts, as I use procedurally generated equipment. All Equipment is Instantiated instead of simply linked, but using the ItemID, I can always get back to the base equipment (for saving, etc).

1 Like

Privacy & Terms