I've made a couple passes now at implementing the inventory system in my game, and I've collected a few thoughts on what went well, what was definitely a mistake, and what I'd do differently next time.
When I was implementing bags and equipment for the first time, I had the realization that equippable inventories and containers are very similar objects. They both accept items, they are both ordered (have slot numbers), and they both have constraints on the items that they would accept. I thought it would be a good idea to embrace this, and make the equipment component implement the interface that I had made for bags, since they would both need to accept items from the other. I.e. you can take an item from a bag and put it into an equipment slot, and vice versa. This turned out to massively complicate things further down the line when I went to implement swapping behavior for two-handed equipment that was technically only one item. When a two-handed hammer is in the player's bags, it only occupies one slot. However, when it is equipped to the player, it occupies both the Main Hand and Off Hand slots. When the player dragged the hammer from one slot to another, I'd have to do some runtime discovery of what each container was, and then modify the behavior of the swap operation depending on what I found. If one was a bag and another was an equipment slot, I'd do something different than a bag to bag transfer. When I find myself in this kind of scenario, it almost always means that I've melded two disparate objects together, and I should go back and rethink how I'm using types. This time turned out to be no execption, and I went back and made an Equipment Wearer interface that's extremely similar to an Item Container, but is allowed to diverge for small yet critical functionality differences. If in the future I encountered this scenario, where I had two outwardly similar objects, I'd think really hard before tying them to the same interface, since that forces one system to start adopting assumptions that were made for a completely different system. In other words, it works great until it goes horribly wrong!
I'm particulary happy with a choice to do some additional indirection when it came to implementing the Item Attribute that allowed an item to have an inventory of its own. For an item to hold other items, it must have an ItemizedContainer attribute. I could have put a simple array of items in that attribute, and implemented the necessary Bag interface functions and called it a day, but instead I decided to do this in a separate ItemContainer object, and defer all of the Bag interface functions and data to that object instead. When it came time to set up the Item Attributes I needed for spellbooks, I was able to reuse the ItemizedContainer Attribute, but this time giving it a subclass of the original ItemContainer class that accounted for the additional restrictions on the type of items that can placed inside of spellbooks. I was also able to reuse the ItemContainer class to make an ActorComponent that could be placed on Blueprint-derived Actors, that retained all of the fancy "EditInlineNew" behavior that is only accessibly for C++ derived classes. This component is what will allow me to create lootable chests in blueprints, while retaining the ability to instantiate the entire contents list in the property editor.
A pattern I find myself returning to is:
I'm wondering if making the component implement the interface is the right call here. I end up writing a ton of glue code in the parent Actor that is essentially:
void InterfaceFunction(param) {
AppropriateComponent->InterfaceFunction(Param);
}
Maybe the tool I'm looking for here is code generation? It seems like something I could definitely automate, but I'd rather just write the handful of lines of boring code and move on than dwell too much here. I'd feel differently if I started doing this at scale, but this seems to be a problem only for the bigger base classes (like Items and the base Pawn class).
Something I tried to be very intentional about was using Blueprint Interfaces wherever I could, so that I could easily change out implementations as need arose. I did exactly this several times, making multiple passes at both items and spells, each using BP interfaces. It was really easy to drop in better implementations each time once I had constricted all communication with the object to use the interface. When this didn't serve me well, however, was when I went to re-implement items one more time in C++, to take advantage of language features that couldn't be used with Blueprints. C++ classes don't have any knowledge of BP types, so I had to make a similar interface in C++, and then make a ton of small fixes to the blueprint side of the code using the new interface. In a larger project, this would have been a nightmare change. If I were working on prototyping another project like this, I'd much prefer to just use C++ interfaces in the first place, since they're just header delcarations that change infrequently.
Over the past month, I've been working almost entirely in C++ instead of Blueprints. I've definitely missed the iteration speed that working in Blueprints buys, and would probably stick to using them to prototype functionality, especially if I'm still exploring what the requirements are. I'd avoid BP interfaces next time and put C++ interfaces in the prototype, so I can start building a "real" implementation when the time came without having to go back and fix a ton of broken BP references. I'd almost say they were a trap, but there's definitely value to using them as you're getting a new system stood up, so long as you don't let the references to them proliferate too widely.