Your user interface styling might be the last thing on your mind when you’re trying to make a game, but like all of the other thousands of little things – it contributes to how players will perceive your game. Good styling can sometimes save an otherwise clunky layout, and bad styling will let down all but the most intuitive layouts. Among your concerns should be standing out from the “default” look that UE4/Slate/UMG will provide.
Granted, your game isn’t going to succeed purely on your super awesome font choice and button gradients; but that may well be the last straw the player needs to walk away. Huh? What’s that? You’re well aware of all of this because your UI designer has been explaining this better than I ever could? You’re only here for the nitty gritty and you’ve probably closed the tab by now? Huh. Alright!
The naive approach is to just start with UMG and use the widget elements that come with the engine. You slap a button in when it makes sense to do so, your text is handled with TextBox, and when you want a list of options you use a ComboBox. WRONG. Do not do this. The only benefit to this is that you can start from nothing and have a fully implemented interface with minimal effort. All of your benefit is upfront.
Let me ask you this: What happens 3 months from now when its time to polish the UI? I’m not going to spend 3 months of time implementing a UI, so you should fill the gaps in yourself.
You’re going to go around to every button in your UI, and you’re going to have to select its image manually. You’re going to have to change its color. If the button uses text, you’re going to have to set that as well. You’ll have to do this for each state, don’t forget. Then, you’ll need to do the sounds. Regardless of how you do this, you’ll – wait a minute. This sounds an awful lot like I’m explaining why you’d use objects to wrap repeated functionality.
There are tons of ways to tackle this problem, however many of them will do one of two things: Either you’re going to be wrapping the basic elements so that you change one class rather than multiple, or you’re going to be scanning through your widgets and applying pre-defined styles. If you’re doing the latter, you’ll want to do this in C++ and you’ll likely want to have certain C++ widget parents. More on this later – it doesn’t imply you’ll have to use Slate.
The first method involves subclassing any of the basic elements you use – yes, we’re going to have our own Button widget. In fact, I often see projects with many button widgets. There are a few things to note when subclassing widgets – UMG won’t allow you to edit widgets introduced in the parent with the designer tab. You also cannot have two widget hierarchies. This means either the parent or the child widget gets to use the widget designer. Note the path to create a subclass of your widget blueprint is NOT the same path you’d use to create a widget blueprint – Instead you’ll be using the Blueprint Class generic handler.
So why subclass at all? Well, for functionality, you handle it the same as you would any other inheritance tree. No special rules for that, it works like you’d expect. For the actual widgets, however, there’s an argument for where you introduce the widgets. Any child whose parent has already defined their widgets can only effectively override style and functionality. Any parent who does not define their widgets can only define functionality which cannot directly bind to widgets – at least in BP (more on this later).
You might go with a scheme somewhere in the middle. For instance, all your buttons would share a GenericProjectNameButton parent which defines a neater interface for what your buttons need to support. Then, you might have a subclass for any buttons which have an icon and text. This widget class, IconAndTextButton, will define a hierarchy with a standard button and two things inside it. Finally, you could subclass IconAndTextButton for both your SettingsIconAndTextButton and GameplayIconAndTextButton. These would override the style of the IconAndTextButton, but could not introduce a new visual element.
Are you still with me? I know I’ve had fun with that last paragraph trying not to accidentally mix terms. That approach has its own set of problems, but you can cut some of them off at the head. There’s a new-ish event you can use called PreConstruct. Do not do anything other than update some element of your widget class with data residing inside your widget class. You should update as much as is possible from this event, as anything you update will be visible to people using your widget in their designer. If you don’t set up PreConstruct, the widget will only look right while playing the game.
Another thing is being able to host children with your custom widget classes. This would be maddening to maintain if you had to subclass for each variation on your widgets. For example, what if you want a IconButton or a TextButton? That’d be insanity. Well, how about a Named Slot? This little guy is going to become your friend, though there will be some annoying limitations. Named slots let you directly work with that slot from outside of the widget. Take ExpandableArea as an example; that widget has two named slots: Header and Body. If you place one of these in your widget hierarchy, you can then put whatever you like in those slots. You can also use named slots from C++ (though not directly in BP), it isn’t limited to just the designer tab.
You can only really use named slots in the designer tab if the widget class in question was the class which introduced the slot. If a parent widget introduced the slot, then you cannot use the slot if you’re not directly working with a widget of that type. In example, if IconAndTextButton introduced two named slots – Icon and Text – and you place a SettingsIconAndTextButton in a widget hierarchy, you can’t directly use either. For that matter, as SettingsIconAndTextButton can’t create any new widgets in designer, that class cannot work directly with the slots. As you can tell, named slots are powerful but limited.
Subclassing widgets in Blueprint to achieve styling or simply unified functionality has its drawbacks. You’ll have noticed all the little drawbacks I’ve been mentioning as we go. Another is that it can be hard to keep track of which elements you’ve already defined, and it can be difficult to resolve near duplicate widgets. You can probably list a few detrimental items yourself, so I’ll just move on to the next method.
You can create style assets for some of the common widgets you’ll use. You can define all of the style details for those widgets in the style assets. You can then get the style structure from these assets as a class default, and apply them to the widget via BP. The main drawback to setting the style like this, is having to get a reference to each widget and manually apply the style. You could walk the widget hierarchy and apply styles, or you could try to listen for new widgets being created and then apply the style then.
You can combine these quite easily, so that you’re subclassing your widgets and then applying a style as suggested here. This is probably closer to ideal, as it removes the need for hyper-specific subclassing.
If you want to have more control over this sort of thing, you can create a UUserWidget subclass in C++. Before you close the tab, we won’t be using Slate directly, don’t worry. See, UUserWidget is what you inherit from to actually create widget blueprints. So by having a layer in between, we can insert any global functionality we want, such as automatic styling. Another thing you can do with this is write functionality as if you were introducing widgets with this class – but you don’t have to introduce them in reality.
This is accomplished via the BindWidget meta tag – there’s an BindWidgetOptional/ OptionalWidget tag as well, but you have to do validity checks in case the widget isn’t created. Since you’re writing C++, you can make full use of StyleSet if you’d like, as well as having access to any style – not a subset.
class UWrapperWidget : public UUserWidget
// do not create in the constructor, use only after initialization
UPROPERTY(BlueprintReadOnly, EditAnywhere, meta = (BindWidget))
class UTextBlock* RequiredTextBlock;
// this can be nullptr
UPROPERTY(BlueprintReadOnly, EditAnywhere, meta = (BindWidgetOptional))
class USpinBox* OptionalSpinBox;
// widgets are selected as they're added
virtual void OnDescendantSelectedByDesigner(UWidget* DescendantWidget) override;
void UWrapperWidget::OnDescendantSelectedByDesigner(UWidget* DescendantWidget)
// do whatever you want
if (DescendantWidget == RequiredTextBlock)
// they selected your text block