In the game I'm building, items can be used as inputs to multiple different game systems. For a fun example, let's imagine a day-old baguette. It could be used as food, which would need some implementation for edible behavior. It could be used as an offering to the harvest gods, needing some favor value baked in. And in desperate circumstances, it could even be a weapon!
The prototype I made for basic items is rooted on a "Base Item" BP, with subclassed blueprints that have special functionalities, like bags. It is functional, and allowed me to get started implementing game systems around it, like the Spellbooks that grant player spells, or itemized bags they can fill with other items:
However, this prototype implementation has a couple underlying technical problems:
If we consider our previous baguette example, we'd need a new subclass that grabs all three of these behaviors, and repeat ad exponentum for every combination of systems that I end up putting in this game.
I'd like to have a system where items are instead data assets, and I can graft on behavior in any combination without making a combinatorial explosion of base blueprint classes. This is especially important because the design I am working on relies on being able to use items between many different systems. A player might cook some mushrooms they found in the forest, or eat them raw, or extract their magical essence with the alchemy system, or use them as material components for a fungal grown spell, etc. etc. etc.
Last month, I set about hardening my player input system by separating it into a keypress handling layer, and a user intent layer. The latter I covered in a previous post, but for the keypresses themselves, I opted to use Unreal's Enhanced Input System to do the heavy lifting. While learning the system, I took some lessons from their data-driven approach to configuring user actions. They were able to set up very nuanced composite behaviors from layering a handful of primitive behaviors together. The icing on top is that each of these behaviors were broken out into their own data asset, so it was easy to build resuable actions that could drive multiple behaviors. I realized that I could apply a similar approach to greatly reduce the complexity of my in-game item system.
Instead of generating a heaving mass of blueprints that I don't really want to spend a Saturday fixing if (when) I decide to change a core behavior later, I could take the Enhanced Input System's approach, and give each item an array of Item Behaviors. If I wanted an item to be equippable, I'd give it an "Equippable" attribute, which contains all the data about how the item interacts with the equipment system. If I wanted an item to provide a spell the player can cast, it gets a "Spell Provider" attribute that will generate the runtime spell for us.
The "Magic" powering the Enhanced Input System's usability in the editor is the (woefully underdocumented) "EditInlineNew" Attribute that can be given to UCLASSes. What this does is allow you to create new UObject instances in the property window of the editor, and set their UProperty fields at design time. This will allow me to create items as Data Assets, rather than Data-Only Blueprints as I had been before. With a little bit of clever initialization, I can elide a significant amount of boilerplate code related to setting up custom items at runtime. Before this refactor, if I had wanted there to be a chest in the bottom of a dungeon containing a +1 flaming longsword, coated in Drow poison, and caked in rust, I would have needed to make a Blueprint that created all of that state on Begin Play, which is much more tedious than it deserves to be. Now I can just do this:
Instead of having to remember to do something like this each time I wanted to set up a container with an item:
One rough edge of the Data Asset based approach is that it makes it too easy to create a particularly nasty type of bug, where it's easy to inadvertently start modifying designer data if you forget to deep-copy a pointer somewhere in C++ code. To defend against this, I decided to separate Items into ItemArchetypes and ItemInstances. Archetypes are a collection of Read-Only Data Attributes that would contain all the design-time data needed to set up an item, and would be responsible for spawning an ItemInstance on begin play that would have all the mutable runtime state. If I didn't do this, I'd run the risk of corrupting the Data Assets if I forgot to deep-copy a pointer somewhere in C++ code.
I'll end with some examples of the new Item Definitions:
And one last picture of how items looked in the inspector before this change:
Good riddance!