Author: Tucker Sandin
One of the secondary gameplay systems in Grape Juice City is the items, inventory, and crafting systems. Our birds can’t just throw infinite juice bottles for free! No, there needs to be conflict of resources! It’s a competitive game after all. That means we need to have our birds fighting over resources. They must hoard recyclable bottles and cans and have turf wars over the best fruit-producing trees and item crafting stations! To create this conflict, we have items, inventory, and crafting.
Items
The main thing our birds do is throw bottles and cans at pedestrians or spray them with juice boxes. Since these “weapons” do not come out of thin air, we need items.
I found Unity’s ScriptableObjects to be a great way to store item data. These Item ScriptableObjects hold the data needed to represent them in the world or in the interface; they also hold information about how they are used. There is some item data that specifies if it’s a “usable” item (filled bottles that can be thrown), a “resource” item (fruit that can be used to craft soda), or a “powerup” item (items that temporarily unlock abilities like jump boost or large juice explosions). If it’s a usable item, there’s also data to specify if it is thrown (like bottles and cans) or sprayed (juice boxes). Items also hold other information, like a maximum quantity for stacks of the item, a cooldown time if it’s a usable item, and whether it belongs to a specific team (to prevent opponent players from picking up your team’s fruit, for example).
Since items are very often stacked, and since stacking items requires a lot of code (preventing stacks of mixed items or stacks exceeding the item’s stack limit) I created an ItemStack ScriptableObject to hold an item and a quantity, and handle the logic that comes with stacking items.
The ItemStack ScriptableObjects did cause one problem though. Since ScriptableObjects cannot be placed in Unity's Inspector without first being initialized in the Assets folder, we couldn’t put stacks for players’ starting items or into recipes without having to create those stacks in the Assets folder first. And creating these stacks in this way felt kind of dumb, since we’d need to make a ton of stacks for a ton of item-quantity combinations.
My first (shameful) workaround for this was to create new variables to hold “inspector items” and “inspector quantities” which could be set in the Inspector, and then combined into proper item stacks when the game is actually run. Eventually I came to my senses and realized a struct or pure class would be best for these, however at this point the ScriptableObject version of the ItemStacks were too heavily embedded into the codebase, so it would've broken a lot of code to uproot the system and replace it with the better alternative.
Since many items hold data that’s unique to specific subsets of items, I thought about using composition where items have different sets of properties depending on their type. This didn’t work very well with ScriptableObjects, however (due to the way they’re instantiated compared to pure classes), so I had to fall back to just having all items have the same sets of data, and some items just do not use certain data (such as resources never using a cooldown, despite still having it in the code). This isn’t ideal, but the solution would likely be way more code, and scalability is no longer a concern since our design has settled on a small, finite set of items that will not be changed in the future.
Inventory
Since we have items, our birds will need to hold onto them. This means we’ll need an inventory system to handle this.
I found that Unity’s ScriptableObjects were also a good fit for inventories. These Inventory ScriptableObjects hold stacks of items in various lists and slots. Due to the nature of our item types, I found that the inventory was best split into two lists and a slot. One list of item stacks holds the resource items, while the other list holds the usable items. This was because we didn’t want someone to fill up on resources and then have nowhere to fit usable items once they’re crafted (and vice versa). We also wanted to limit players to holding only a single powerup item at a time, so there’s a specific slot for that (which holds an Item and not an ItemStack since quantity will always be one).
Another feature I designed into the inventory is that the functions that add items will return a list of item stacks. This list represents items that could not be added to the inventory, due to insufficient space. For example, let’s say we want to add two grapes to a bird’s inventory, but there’s only enough room for one. We don’t want to cancel adding items all together, because it can still fit 1 more. So, the functions will simply add until the inventory is full, then return the leftover items (in this case 1 grape), so the code that’s adding to the inventory can do something with the leftovers (like drop them on the ground).
There was a lot of code that went into splitting up the inventory like this. The two lists meant caused quite a bit of duplicate code (though only ever duplicated once), but seeing how our inventory’s user interface turned out, scalability was not needed and actually made the interface code harder on my fellow developers. For example, a fixed-size hotbar interface is much easier to code than one of varying size. While the inventory code does allow for a variable stack-count limit, seeing it never get used makes me feel a little better that I didn’t use a more scalable list for the powerup slot as well.
Recipes
Now that we had items and a way to hold them, we need to be able to craft them. Since I originally planned for a scalable design, I created two ScriptableObject classes: Crafting and Recipe.
Recipe objects could be defined in the inspector to essentially house the data for a set of items to create another set of items. Since I designed for scalability, recipes have a list of ingredients and a list of results. Since we may require multiple of the same ingredient or result, these lists hold item stacks, not just items.
For example, the GrapeBottle recipe has two ingredients, a Grape and a Bottle, and one result, GrapeBottle. Since the ingredients are item stacks, we could easily specify that while one bottle is needed, three grapes are required. The list of results would also allow us to (in the future) specify that the results are a grape bottle and some other item (like the excess grape stems or something).
This recipe system was, in my opinion, well designed and scalable. Unfortunately, none of our recipes ended up having more than a single result (making a list of results unnecessary), and none of the recipes currently require multiple of the same item (making the list of item stacks instead of single items unnecessary). I still feel good for designing it this way, however, so the code will stay as it is (though my soul breaks every time I see “ Recipe.Results[0] ” in the code).
Crafting
Finally, with recipes created, we need a way for the players to craft them. Enter the Crafting ScriptableObject.
This crafting object has two purposes: first, it crafts a given recipe using a given inventory, and second, it holds a list of viable recipes the player is allowed to craft. The reason these crafting objects hold a list of recipes is so we can specify that birds can only craft the items surrounding their team’s fruit; we don’t want the player on the grape team to craft lime bottles, for example. To actually craft things, the crafting object takes a desired recipe, checks a given inventory for the required ingredients, and then (if the ingredients are available) deletes the ingredients and adds the results to the inventory.
Crafting with an inventory also handles overflow items (such as crafting 3 items when only 2 can fit in the inventory), so we can do something with the leftovers (like drop them on the ground).
I wasn’t 100% sure about this system, since many developers might’ve put the code into the inventory or recipe systems. However, I do think splitting it up was a good decision. Not only do I expect the whole “one crafting object for each team, with different recipes” thing to be helpful in the future, but it also follows good programming principles that state that “classes, systems, and code should be broken up into single-responsibility segments”, which makes the code more scalable and maintainable.
Conclusion
These were some of the core systems I designed and wrote for this game. Some areas were overly abstracted and prepared for possible scalability that’ll never come. However, some areas did not scale up well enough, so I do believe writing the code in a less scalable way would’ve hurt us much more than making things overly abstract and scalable. I do however wish I could replace inventories and item stacks with structs or classes, instead of being built on Unity's ScriptableObject system. However it's too late in the development timeline to redesign a core system, especially when it works perfectly fine and there are many other systems that still need working on.
Comments