The importance of UI and its Architecture

Large and complex

As someone who creates UI at Epic, I sometimes get asked for tips and tricks for structuring UI when it comes to C++ and Blueprints. As a result, I wanted to create an in-depth example that shows off some UMG best practices.Game UI Systems are:

There are exceptions, of course, but many, if not most, games have relatively deep progression systems, quest systems, stat systems, retention systems, and so forth. The “UI team,” in my experience, not only owns the UI presented to the user, but the underlying systems to track all the data often saved in account systems and cloud-based data stores as well as the hooks in gameplay systems to gather the data.

Volatile

These systems tend to grow and change quite a bit during development as the team tries to find what is right for their game.

Nuanced

The styling, look and feel, layout, and presentation are tweaked and changed a lot while trying to deal with feedback that likely includes varied opinions and results from users with different skill levels and gaming backgrounds.

Separation of business logic and the visuals of your UI

Allows quick iteration of layout and visuals

Effective business logic debugging

Performance

The example

Class Architecture

C++ code

Blueprints

Class Architecture

UMyData is a C++ class that inherits from UObject.

UMyWidget is a C++ class that inherits from UUserWidget.

MyBlueprint is a Widget Blueprint that derives from UMyWidget.

Let’s Code

Our Data Class

Given that information, I strongly suggest spending the extra time up front to establish a strong architecture that provides:I’ll present a very effective pattern I've found at achieving these goals with a real-world example below.Let’s say you’re working on a game. Let’s pick one at random… Fortnite will do. ;)We want an Item Shop that looks something like this:Were going to dive deep with this example to address:For the most part, I jump into the hows pretty quickly and cover the whys throughout the explanations and comments of the code I present.The architecture I generally suggest roughly follows this pattern:The idea is to create data classes to encapsulate all the information about a thing your UI wants to communicate to the user.It being a UObject allows us to control access with public/private, have getters/setters and include useful API.This approach separates the lifetime of the data from the UI, which is very desirable, as store offers, inventory items, player stats, and all the other data sources rarely share the lifetime of UI objects. Furthermore, you can have multiple widgets sourcing data from the same data objects. These classes may well exist already, created by other engineers in other disciplines as part of creating the gameplay or the store backend, however, even in these cases, often times the UI needs its own data class to wrap up the gameplay data alongside more data that is UI specific.These C++ classes are intended to define widget specific API for use in Blueprints as well as Blueprintable events to define the contract that Blueprints must follow to properly interact with the underlying system.In the Widget Blueprint, you create and layout all the visible UI you need, style it, and then utilizing both the API provided by UMyData and UMyWidget, populate all the UI primitives (text boxes, images, …) with the necessary data. You’ll also listen to the events UMyWidget provides to know when to update the corresponding UI. In interactive cases, you may respond to a button click and call into some provided API on the UMyData or UMyWidget.First, let’s put together our data for an offer in our shop.UOfferInfo derives from UObject, because we want to expose it to Blueprints and we want to have UFUNCTIONS to access our data. We’ll keep it relatively simple for the example but imagine price, image, and much more.

UENUM

(BlueprintType)

enum class

EOfferType

uint8

Normal,

Featured

};

UCLASS

(BlueprintType)

class

UOfferInfo

public

UObject

GENERATED_UCLASS_BODY

()

public:

UFUNCTION

(BlueprintCallable, Category =

"OfferInfo"

FText

GetName

()

const

UFUNCTION

(BlueprintCallable, Category =

"OfferInfo"

FText

GetDescription

()

const

UFUNCTION

(BlueprintCallable, Category =

"OfferInfo"

EOfferType

GetOfferType

()

const

private:

// All your data including UProperties!

Our Shop Widget

};Next, let’s look at our shop widget, which will be responsible for providing interface and behavior for the screen that shows all the offers. Showing the details about an individual offer will be relegated to a UOfferWidgetBase UserWidget we’ll look at later.

// Abstract because we want to inherit a Blueprint class from this base

// but don't want users to be able to instance the base class directly

UCLASS

(Abstract, Blueprintable, BlueprintType, ClassGroup = UI)

class

UOfferShopWidgetBase

public

UUserWidget

GENERATED_UCLASS_BODY

()

public:

// This function tells the users that we have started reading offers

// The Blueprint will most likely put up a throbber and text explaining were downloading data

UFUNCTION

(BlueprintImplementableEvent, Category =

"OfferShopWidget"

void

OnStartReadingOffers

();

// We tell the Blueprint we want to generate some UI for a specific Offer

// Most likely the Blueprint will create an Offer widget and hand it the UOfferInfo* for it to do it's thing

// I suggest API like this to allow the Blueprint full control over the layout

// Maybe the visual design calls for a vertical box, and next week changes to scroll horizontally,

// or maybe a tile grid, or some combination.... you get the idea ;)

UFUNCTION

(BlueprintImplementableEvent, Category =

"OfferShopWidget"

void

GenerateOffer

UOfferInfo

* OfferData);

// This function tells the users that we have finished generating offers

// The Blueprint will most likely hide the throbber and complete any setup of the screen

UFUNCTION

(BlueprintImplementableEvent, Category =

"OfferShopWidget"

void

OnOfferGenerationCompleted

();

// Utility functions for the Blueprint to get data it will use

UFUNCTION

(BlueprintCallable, Category =

"OfferShopWidget"

FDateTime

GetStoreRefreshDate

()

const

protected:

// Keep all your internal function at least protected

private:

// More internal functions



// All your data including UProperties!

UPROPERTY

(transient)

TArray

UOfferInfo

Our Offer Widget

*>CurrentOffers;};Lastly, let’s look at our base C++ class for widgets to display an individual offer.To achieve the mock up, we’ll derive two Blueprints with different layouts for the two different tiles.

// Abstract because we want to inherit a Blueprint class from this base

// but don't want users to be able to instance the base class directly

UCLASS

(Abstract, Blueprintable, BlueprintType, ClassGroup = UI)

class

UOfferWidgetBase

public

UUserWidget

GENERATED_UCLASS_BODY

()

public:

// This is the function you have to call after Creating a new instance of one of these widgets

// With this pattern you can call SetupOffer again if you want to show something different

// without creating a whole new widget, generally important for performance especially when working with

// inventories and such where you'll most likely want to pool/reuse widgets to keep the UI fast

UFUNCTION

(BlueprintCallable, Category =

"OfferWidget"

void

SetupOffer

UOfferInfo

* InOfferData)

// Threw in a little implementation here to explain why I don't tend to use BluprintNativeEvents

// The primary reason is that it introduces possible user error where the call the parent function

// in the wrong place or not at all.

// The UX surrounding BlueprintImplementableEvent is also a little simpler.

OfferData = InOfferData;

OnOfferSet

();

// We tell the Blueprint we got data to show

// The Blueprint will most likely call GetOfferInfo() then the GetName, GetDescription,

// and many more to populate it's text fields, textures and everything else

UFUNCTION

(BlueprintImplementableEvent, Category =

"OfferWidget"

void

OnOfferSet

();

// Utility functions for the Blueprint to get data it will use

UFUNCTION

(BlueprintCallable, Category =

"OfferWidget"

UOfferInfo

GetOfferInfo

()

const

private:

UPROPERTY

(transient)

UOfferInfo

Let’s Blueprint

FeaturedOffers_HorBox Takes up the left half of the screen and will be where we put our featured offers.

NormalOffers_WrapBox Takes up the right half of the screen and will be where we put our normal offers.

RefreshTime_TextBlock A text block to put our store refresh timer in.



Change the layout easily

Swap out the widgets used in different situations

Modify the properties of the slot, which is returned from the “Add Child to…” calls

* OfferData;};Time to create a UMG Widget that derives from our UOfferShopWidgetBase and build out a layout to accommodate the data we will populate at runtime.And here’s the graph:Because we let the Blueprint deal with creating the widgets and adding them into the appropriate containers, we gain the ability to:Now let’s create our Offer widgets. We’ll create two UMG Widgets deriving from the UOfferWidgetBase class we made in C++. Let’s call them OfferTileSmallWidget and OfferTileLargeWidget. The GenerateOffer event in the Offer Shop widget Blueprint pictured above creates these two widgets based on the Offer Type stores in our Offer Data.After we setup the Small and Large widgets to look like our tiles from the “mockup” image at the top, we can create the graph, which in the simplest form would look something like this:We created OfferTileSmallWidget and OfferTileLargeWidget to accommodate the shop screen, but our architecture makes going further easy. Imagine now that in some other screen in our game, we wanted to show off a hot offer!This would take two steps really:

1. In C++, we make a function for the hot offer, and maybe it looks like this:

UOfferInfo * MyLibrary :: GetTheHotOffer ()

This just returns an instance of our OfferInfo data class.

2. Put one of our existing two widgets on the screen and call SetupOffer () with the offer GetTheHotOffer () returns.

Or maybe we create a new Blueprint “HotOfferWidget” that has a fire effect and put that in and call SetupOffer ().

Conclusion

This approach keep business logic in C++, making it easier to maintain, debug and modify. It exposes a strong event-based API for Blueprints to allow creatives the flexibility to make the UX and visual experiences they want.Some things to avoid are Blueprint Ticks, Property Binding, and, to a lesser extent, excessive animations. This approach fundamentally is about providing a set of events that expose the important edges of the system and its state. This naturally tends to discourage Blueprint Ticks and Property Binding simply because their utility is lowered given the existence of your events.Animations are relatively expensive, certainly useful, but you should be judicious in their use, especially when performance really matters, like in a HUD where the UI budget always seems to be near zero. Anyone who’s worked on UI knows what I mean!