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