Paul Greveson

Creating Widgets with Unreal & Angelscript

Using Angelscript in Unreal can make creating Widgets way faster and easier to iterate on. Let's take a look at how to build a simple example!

Contents

General Principles

Terms

There are some terms I'm going to use throughout this post, so let's make things clear from the start:

  • Widget - the actual interface that users will interact with, displaying information and handling input
  • Data - any sort of data that is underlying the widget (could be assets, game state, whatever)
  • ViewModel - an intermediary class that converts the Data into something the Widget can use, and also runs operations on the Data
Simple flowchart illustrating the flow between the Widget, the ViewModel and the Data

A ViewModel may not always be necessary - in the simplest case, all you need is a Widget that has simple display and operational logic for working with the underlying Data.

ℹ️ Note

I won't be using a ViewModel in the following basic example, to keep things simple.

Approach

You could consider two main paths to be taken when implementing Widgets in Unreal:

  1. Interface first - Start with a sketch of the user interface and use that to guide how you approach the structure of the Widgets and ViewModel.

    • This implies you know what problem you are trying to solve, but maybe not what data you will work with yet.
  2. Data first - Start with the data you know you need to work with, and figure out a good way to display it (with or without a ViewModel).

    • This implies you have some existing data and need to work out the best way to expose it to the user.

In reality, I find that the best way results in a feedback loop between both of these approaches, refining the data and the interface as you work on it. Build, iterate, build, iterate!

In this example, we're going to build the interface first because it's just a simple tutorial with no particular goal in mind other than to cover the basics.

Prerequisites

If you want to follow everything I'm covering here, you'll need to have downloaded and installed or built Hazelight's UnrealEngine-Angelscript fork. Access to the UnrealEngine-Angelscript Github repository requires access to Epic's Github repositories too, so set that up first by following this guide.

⚠️ Important

Do this first or very little of this will work or make any sense! You can find installation instructions here.

Make sure to install Visual Studio Code (a.k.a. VSCode) for working with Angelscript files. Information about the Unreal Angelscript Extension for VSCode is also mentioned in the guide above, and you should make sure to have it installed too.

ℹ️ Note

The general approaches described below will still apply if you want to write your code in C++ or entirely in Blueprint, but that is not my goal here, so adjust your expectations accordingly if you are not using Angelscript at all.

Create a base class in Angelscript

Let's start by creating a "base class" for our Widget - make a new ExampleWidget.as file somewhere in your Unreal project's /Script/ folder.

  • It should inherit from the UUserWidget class if we are making a general-use Widget that can be used at runtime or in the Editor.
  • It should also be Abstract, because it can't exist without a child implementation to specify the Widget bindings.
/** An example Widget for this tutorial */
UCLASS(Abstract)
class UExampleWidget : UUserWidget
{
};

This is the simplest base class example that does not specify any properties or methods, so it is fundamentally no different to its parent class of UUserWidget at this point.

Add some dependent child Widgets

Now if we want to specify that some Widgets are required to display specific information or handle user inputs, we can specify them here using the BindWidget keyword on a property.

/** An example Widget for this tutorial */
UCLASS(Abstract)
class UExampleWidget : UUserWidget
{
	UPROPERTY(BindWidget)
	protected UTextBlock ExampleText;

	UPROPERTY(BindWidget)
	protected UButton ExampleButton;
};

By using the BindWidget keyword, we tell Unreal that these sub-widgets must exist on any implementation of this Widget class, and this will result in a Widget Blueprint compile error if we fail to specify them.

⚠️ Save & hot reload

Save the Angelscript file so that Unreal will hot-reload this script.

Create a Widget Blueprint subclass

Once you have saved the above file, Angelscript should hot-reload in Unreal and you can now create a Widget Blueprint based on our new Angelscript base class.

  1. In the Content Browser, right-click in empty space (not on an asset) to bring up the context menu.

    • Choose User Interface -> Widget Blueprint near the bottom of the list:
      Right-click in the Content Browser, look in User Interface and select Widget Blueprint
    • Note that if you want to create an Editor Utility Widget that you can launch as a tool in Unreal Editor, instead of just a regular Widget, you must instead choose Editor Utilities -> Editor Utility Widget from the right-click context menu.
  2. In the window that pops up, filter or search for our ExampleWidget class to use as a base:

    Filter the class list for our ExampleWidget, and select it from the list

    • You might need to click the All Classes header to show the list. Unreal strips the U prefix, but you can still paste UExampleWidget here if you want to filter on the exact class name.
    • Also note that our comment over the Angelscript class shows up as a tooltip for the user!
  3. Click Select, and you will go back to the Content Browser with a new asset selected - give your new asset a clear and simple name like WBP_Example:

    Name the new Widget Blueprint clearly

    ✅ Tip: Naming conventions

    I find it's useful to prefix assets in this way (WBP = Widget BluePrint) to tell different types apart at a glance, especially in text lists or logs.

Set up the Widget Blueprint

If you open the asset we just created, you might notice a ⚠️ icon next to the Compile button in the Widget Blueprint toolbar.

If you click Compile, you should see the following errors in the Compiler Results pane:

Compiler errors when first opening and compiling the new Widget Blueprint asset
These compile errors show up because we haven't yet created the child Widgets that our base class expects to be bound. You can toggle the Compiler Results pane from Window -> Compiler Results if you don't see it immediately.

Add the child Widgets

First, make sure that the Palette panel (Window -> Palette) and the Hierarchy panel (Window -> Hierarchy) are visible, so that we can find and drag the Widgets we need into our Widget's hierarchy.

  1. Since we have more than one child Widget to add, we first need a container Widget that can hold more than one Widget - let's use a simple Horizontal Box.

    Search the Palette panel for Horizontal to show the Horizontal Box widget, then click and drag it over to the root Widget in the Hierarchy pane to create an instance of it:

    Search for Horizontal in the Palette pane, and drag it over the base Widget in the Hierarchy pane

  2. Search the Palette panel for Text, then click and drag the Text Widget over the Horizontal Box Widget in the Hierarchy pane to add an instance of it as a child:

    Search for Text in the Palette pane, and drag it over the Horizontal Box Widget in the Hierarchy pane to add it as a child
    Click on the new Text widget or press F2 to rename it to the exact name we specified above - ExampleText.

  3. Search the Palette panel for Button, then click and drag the Button Widget over the Horizontal Box Widget in the Hierarchy pane to add an instance of it as a child:

    Search for Button in the Palette pane, and drag it over the Horizontal Box Widget in the Hierarchy pane to add it as a child
    Click on the new Button widget or press F2 to rename it to the exact name we specified above - ExampleButton.

Now when you click Compile again, the errors should no longer show up, and there should be a ✅ next to the Compile button.

If this isn't the case, double-check that the Widgets you added have the correct names and are the correct type for the BindWidget properties you specified in the Angelscript base class.

Change the preview mode

✅ Tip: Layout preview

At this point I often change the Widget preview mode by using the button in the top right of the Designer view.

I recommend changing it from the default "Fill Screen" (which is rarely what you actually want) to the more clear "Desired" mode, which causes the Widget to take up the actual amount of space it needs at a minimum:

Search for Button in the Palette pane, and drag it over the Horizontal Box Widget in the Hierarchy pane to add it as a child
You can change this at any time to preview how your Widget will look at different screen or window sizes.

Add simple logic in Angelscript

Now we've set up how it will be displayed, that's all we need to do in the Widget Blueprint! Let's go back to VSCode and add some logic into our Angelscript base class.

Implement a PreConstruct method

The PreConstruct method is available to all UserWidget subclasses, and can be overridden in Angelscript.

It will be called on your Widget at edit time and at runtime before it is Constructed, so you can use it to show a more accurate preview of the Widget while you're editing it in the Widget Blueprint editor.

To define this method, just start typing Pre and the Unreal Angelscript extension should prompt you to auto-complete to a full method definition, just hit the Enter key or click on this context menu entry to let VSCode save you some typing!

Typing a function name inside the Angelscript class in VSCode will give you autocompletion prompts to save time!

Then let's just add the following code inside the UExampleWidget class, immediately after the property declarations:

	UFUNCTION(BlueprintOverride)
	void PreConstruct(bool bIsDesignTime)
	{
		const int RandomNumber = Math::Rand();
		const FText RandomNumberText = FText::AsCultureInvariant(f"{RandomNumber}");
		ExampleText.SetText(RandomNumberText);
	}

What this code is doing is split over three lines:

  1. Generating a random number using Math::Rand() and storing it in the RandomNumber variable.
  2. Formatting the random number into a string using the f"" simple string formatting, then converting that to FText using FText::AsCultureInvariant (because we do not want to localize this text, it's just a number).
  3. The resulting FText is then set as the text to display on the ExampleText Widget.

⚠️ Save & hot reload

Save the Angelscript file so that Unreal will hot-reload this script.

Now if you switch back to Unreal, when you hit Compile in the Widget Blueprint editor, you should see that there is now a random number displayed in your ExampleText Widget where it used to say Text Block.

Compiling the WBP_Example widget in Unreal will cause the text block to display a random number

If you hit Compile repeatedly, you will see this number change, because the PreConstruct method is being called each time, generating a new random number and setting it on the Text Block.

Hook up a function callback to the Button click event

Override the Construct method in the same way we overrode PreConstruct before - start typing Con and use VSCode's autocomplete to generate the method.

Inside this method, we're going to bind a method to the OnClicked event of the ExampleButton Widget:

	UFUNCTION(BlueprintOverride)
	void Construct()
	{
		ExampleButton.OnClicked.AddUFunction(this, n"ExampleButtonClicked");
	}

You'll notice that VSCode underlines this line in red, and if you hover over the line, it should say something like Function ExampleButtonClicked does not exist in type UExampleWidget. This is (unsurprisingly) because the function we're asking for doesn't exist yet!

✅ Tip: VSCode method generation

There's an easy way to fix this via the Unreal Angelscript extension - click the little blue bulb icon to the left of the line, and choose Generate Method: ExampleButtonClicked from the pop-up menu, as shown below.

TODO
Clicking this option will generate the exact code we need, inside the class!

	UFUNCTION()
	private void ExampleButtonClicked()
	{
	}

Now whenever you press the button, the event we bound in OnInitialized will cause the ExampleButtonClicked method to be called.

We can put whatever code we want to execute in here - for example, if we wanted to replace the random number in the ExampleText block with a specific string, we can do it like so:

	UFUNCTION()
	private void ExampleButtonClicked()
	{
		const FText OverrideText = NSLOCTEXT("ExampleWidget", "Override", "Override!");
		ExampleText.SetText(OverrideText);
	}

This time, we're using NSLOCTEXT because we want our "Override!" string to be localizable into other languages. Text is not required to be localized for editor tooling, but if you're making something user-facing, then you want to consider this!

The first two arguments are the Namespace and the Key which uniquely identifies this text, and the third argument is the default (in this case English) string to use when localization has not been applied.

⚠️ Save & hot reload

Save the Angelscript file so that Unreal will hot-reload this script.

Testing it all together in an Editor Utility Widget

Unfortunately, since buttons aren't clickable in the Widget Blueprint editor, we need to create a usable instance of our WBP_ExampleWidget somewhere.

We could do that on a game HUD, but a simpler approach is just to put it inside a new Editor Utility Widget that we can just run directly in Unreal Editor.

  1. In the Content Browser, right-click in empty space (not on an asset) to bring up the context menu, and choose Editor Utilities -> Editor Utility Widget in the middle of the list:

    Right-click in the Content Browser, look in Editor Utilities and select Editor Utility Widget
    Once you select this, a popup may ask you what type of Widget you want to use as the root - it doesn't really matter for our purposes here, so you can just pick None.

  2. Open the new asset (likely called NewEditorUtilityWidgetBlueprint, you can rename it if you like) by double-clicking on it or right-clicking on it and choosing Edit from the context menu.

  3. Search the Palette panel for Example, then click and drag the WBP Example Widget over the root of the Editor Utility Widget in the Hierarchy pane to add an instance of it as a child:

    Search for Example in the Palette pane, and drag WBP Example over the root Widget in the Hierarchy pane to add it as a child

  4. Click Compile, make sure it has ✅ compiled successfully, and then click ▶️ Run Utility Widget in the toolbar of the current Widget Blueprint Editor:

    Compile and then run the Editor Utility Widget

This should cause the new Utility Widget to open in a new window. It should be displaying a random number as we expect. Now, if you click the button, the text should change to "Override!".

Clicking the button in the Utility Widget should display the text 'Override!'

Parting thoughts & further work

The key thing about this approach is that we have kept the style of the Widget (defined in the Widget Blueprint) totally separate from the definition and logic of the Widget (defined in the Angelscript base class).

This is nice because the Angelscript code doesn't really have to have any opinion on the visual style of the Widget, it just needs to define the child Widgets it knows it needs complete the functions required of it.

The style can be layered on top by editing the Widget Blueprint - you can use any sort of layout you like, colors, images etc., and none of this requires any changes to the underlying script that drives it all.

Composing & nesting Widgets

Extending this simple example, we could do something a bit more advanced by defining multiple different Widgets which each perform a specific function, and then nesting them inside each other or binding callbacks from one Widget to another, to create more complex setups.

ViewModels

I didn't cover this here, but when things start getting more complicated (e.g. tools that manage large amounts of data), you might find it handy to implement an extra object type as a ViewModel, to act as an intermediary between the Widget and any data you are operating on.

This is basically taking the concepts we already covered here (separating the Widget's design from the logic) and bringing it a step further, so that instead of the Widget dealing with data directly, it will deal with a simpler API via the ViewModel. This allows the same Widget to be able to work with many different types of data, as long as they all share a similar way of working.

ℹ️ Note

I'm using the term ViewModel here because that's how I tend to think about it (like in MVVM), but you could think about it as a Controller if you prefer, such as in the MVC pattern. The main point is that is is a separate entity from both the Data and the Widget.

I'll probably do a deeper dive into this in another blog post later.

Relevant Links