When implementing a game UI, we often don’t think much about maintainability. We receive a mockup from an artist, break it out into individual elements, place our widgets on the screen with placeholder art and drop in the final assets when they’re ready. This works great for elements such as a HUD or a menu that doesn’t require much iteration, but what do we do when we need to expose a more complex system? Rather than make constant modifications to the UI every time a new item gets added or a new menu option is required, we can set up a data-driven UI to remove ourselves from the pipeline, coupling the underlying data directly to the interface automatically.
A data-driven UI element is one which is constructed procedurally based on some underlying data source instead of being built by hand. The beauty of this pattern is that a designer could make changes to the system being exposed by the UI without having to make any adjustments to the UI itself. The biggest drawback is that since the element only exists at runtime, it can be difficult to preview and finely control how it will appear in the game.
For example, imagine a shop in a game. Our interface needs to show a list of all available items for purchase with their price and an icon. It wouldn’t be too difficult to build a window with all of this information, but what if a designer wants to add or remove items from the shop? What if the prices need adjustment or icon art needs to be updated? All of these would require modifications to the interface, and forgetting to do so could cause the interface to fall out of sync with the data. Nobody wants to purchase an item in a game listed at 500 gold only to find 1000 gold taken out of their inventory!
In this post, I’m going to describe how to set up a data table and couple it to a shop widget which will display an arbitrary number of items in a scrolling list. I’ll also show how to broadcast events when selections are made and talk a bit about how we can expand upon these ideas to fit the needs of your project.
The most important part of any data-driven UI element is the data itself, so let’s set up a data table to contain our shop’s inventory. First, we’ll need to create a struct that represents the columns that each row of our table will contain. Create a struct by clicking Add New, opening the Blueprints category, and clicking Structure.
I’ve set mine up like the image below:
Note that I didn’t create a Name field, as the data table will automatically add this in later. We’ll use the name if we ever need to look up a specific entry in the table.
Next, we’ll create the data table itself by clicking Add New again and expanding the miscellaneous category. Select the struct you created previously, and then start adding in some entries!
Now that we’ve got our data, we need to create two widgets. First, we’ll need our main shop window, which we’ll create whenever the shop is available. Then, we’ll make an ItemRow widget, which will be created at runtime to represent a single row of our data table.
Let’s start with the ItemRow. Our goal is to create a generic widget that can be duplicated and populated automatically, with a layout that can adjust to fit the different text lengths and such that we allow.
I’ve made some assumptions about maximum text size and icon size, but here’s my widget:
Note that I’ve deleted the starting canvas panel from the top of the hierarchy, as this widget will be positioned and sized by its parent.
I also created a SetValues function to simplify feeding the data into my template. I could have used property bindings for a quick and easy solution, but bindings will update the value every tick. In any situation where performance may be a concern, it’s best to avoid bindings and instead, set up events to only update your properties when needed.
Next up, we’ll create the main widget which will hold our entries. This widget will contain the Blueprint for reading in the data table and creating an ItemRow widget for each row. We can temporarily drop some ItemRow widgets in to visualize, but we’ll want an empty scroll box in the final product since the rows will be added by the Blueprint.
Now we just have to add our main widget to the viewport, and it’ll fill up with all of the latest info from the data table! If we want to add, remove, or change any of the entries, all we have to do is update the data table.
MAKING SOME IMPROVEMENTS
The first annoyance you may have noticed is that you have to actually start PIE to view your shop interface, which means making adjustments may come down to trial and error. Luckily, if you’re on 4.15 or later, we’ve added in an Event Pre Construct node for widget Blueprints. This is essentially the same as the Construct node, except it’ll run at edit time so we can see how the widget will look in our preview viewport without hitting play. Simply disconnect the Blueprint we used to build the rows from the Construct node and attach it to the Event Pre Construct node.
Be aware that if you modify the data table, you’ll need to hit Compile to rebuild the preview widget, even if the button is still showing the usual checkmark.
It’s also important to keep in mind that although the nodes connected to Event Pre Construct will still be called at runtime as before, they’re now being called within the Blueprint editor on the preview widget as well. You’ll want to stick to cosmetic code, and avoid referencing anything that isn’t going to exist until runtime.
Another consideration is that even though we’re currently displaying an arbitrary list of items, we’ll need a way of informing other game systems (such as a character controller) when a button was clicked so that we can deduct the gold and add the item to the player’s inventory. To keep things as modular as possible, I like to create an event dispatcher on my top level widget to act as an intermediary between the individual entries and anyone who wants to receive events when a particular button is pressed. Your ItemRow will need a handle back to your root widget so that it can fire the event, which you’ll call by overriding your ItemRow’s OnMouseButtonDown.
In the ItemRow Widget.
Any interested parties can bind a custom event to the root widget’s event dispatcher, receive the name of the clicked item as a parameter, and do their own lookup in the data table for the full details.
In your main, top-level widget.
It’s also important to understand that the data table used in my example is meant to be modified only at edit-time. There may be cases in which you need a more dynamic data structure which can be modified during the course of your game. You may even want to use a data table as the starting state for your data, copying it over to another structure which you can then modify as needed. For maximum versatility, you can create a data structure with arbitrary properties and some metadata that tells the UI how to display those properties. It’s worth taking some time upfront to decide which solution will work best for you, rather than run into unexpected limitations down the line.
While some UI elements don’t change enough to warrant the extra time and effort needed to set up a data-driven widget, many systems can benefit from the flexibility they afford. Rapid iteration is pivotal to good game design, and generating an interface procedurally lets you go from idea to execution in no time at all.