A few people in Slack were asking about good practices when using UMG, especially when mixing with C++
EDIT: Nick showed me a much better way of setting up the pointers. See here.
I did the UI for both Satellite Command (Mobile & PC) and my Hovertank / Battlezone Inspired Pet-Project, and I’ve come up with what I think is a decent, reusable and importantly fast approach to designing and updating my widgets, so figured I’d share my workflow.
First of all, some pr0-tips[SUP]TM[/SUP] for getting the most out of UMG - or just rando things that I’ve picked up.
- The less Widget Components you use (that’s the list of items in the Panel on the left in UMG designer) - the less time it takes the widget to draw. Try to be conscious of that when putting a widget together.
- Anything that should NEVER recieve mouse hits, or be interactable, set it to ‘Self Hit Test Invisible’. Adding Widgets to the hit test grid isn’t free.
- Bindings are naughty and have an inherent cost of their own, avoid them if you can.
- Widgets that go off-screen will be set to ‘Hidden’, will no longer draw or call NativeTick. In some cases, it’s best to add Widgets to a CanvasPanel
- Use the Invalidation Panel. Get into the habit of dropping one in as the root widget and testing things regularly as you add functionality. This saves a metric f-tonne of draw time, especially on mobile.
- The console command stat slate is your new best friend. Use it, a lot.
- Treat your HUD class as a ‘Manager’ for your widgets. Store references to them there, set your default classes there. Having a nice central place to access all of your widgets from anywhere in the game makes life very easy.
- Use TWeakObjectPtr if you’re referencing other widgets or objects in the world. Too often I’ve seen people filling up memory with rubbish that either piles up for the garbage collector, or can’t be removed at all because you’re still referencing it.
- If you’re creating / destroying widgets often (such as for Tool Tips etc), cache them. This is good practice for any game scenario really…
Onto the meat of the subject. I use the UMG designer for two things - designing / placing widgets and animations in the designer, and using events to drive the animations in the event graph. That’s pretty much it - if I can do any code to update/set text, colours or say; the percentage of a progress bar, I always do it in C++. It’ll save you heaps of time and performance later.
One lesson I learnt the hard way, is the importance of splitting up your widgets into sensible chunks. While making Satellite Command I thought it would be easier if I put all of our game widgets into a master ‘InGameWidget’, which turned out to be a huge mistake and massively overcomplicated things. Take a look at this mess (this is after splitting off some chunks into their own Widgets elsewhere). Don’t do this:
So moving onto better practices, since I created this recently (today actually) - here’s an example of how I’m doing things now. The widget below is for the upcoming Steam version of Satellite Command, and is designed to function as a tool tip. When a Player hovers over a “Satellite Item Widget” in the main User Interface, this badboy pops up to show some extra information.
And here’s the list of UWidgets in this UMG Widget, to give you some idea of how it’s put together. Notice how NONE of these things are variables (usually indicated by bold text), and each one is uniquely named to something that makes it easy to identify. Naming widgets properly is a good habit to get into, and we’ll need to do that so we can find and access them in C++.
This is (currently) the entire Blueprint Graph for this widget.
So the first thing I recommend, is creating a ‘Base Widget’ class that all of your subsequent interface widgets inherit from. This is useful for declaring static variables that are used all over the User Interface and make it easy to keep things like colour schemes / themes in line accross the whole UI, and also for creating common functionality accross the whole UI too. Since most of the widgets in Satellite Command are animated for example - I created generic events / functions for handling closing & opening of widgets etc.
Here’s a chunk of code from our Base Widget class in Satellite Command. I use a whole bunch of statics to declare variables / colours / text that I’ll resuse all over the place. Materials / textures too. These are all Protected Static vars in the Header, most of the time Const too.
BaseWidget.cpp
USoundBase* UGESGame_BaseWidget::UnfoldAudio = nullptr;
USoundBase* UGESGame_BaseWidget::ConfirmAudio = nullptr;
USoundBase* UGESGame_BaseWidget::DeclineAudio = nullptr;
USoundBase* UGESGame_BaseWidget::SwapAudio = nullptr;
USoundBase* UGESGame_BaseWidget::OnAudio = nullptr;
USoundBase* UGESGame_BaseWidget::OffAudio = nullptr;
USoundBase* UGESGame_BaseWidget::WarnBadAudio = nullptr;
USoundBase* UGESGame_BaseWidget::WarnNeutralAudio = nullptr;
USoundBase* UGESGame_BaseWidget::BadActionAudio = nullptr;
UTexture2D* UGESGame_BaseWidget::Bronze_TextureAsset = nullptr;
UTexture2D* UGESGame_BaseWidget::Silver_TextureAsset = nullptr;
UTexture2D* UGESGame_BaseWidget::Gold_TextureAsset = nullptr;
UTexture2D* UGESGame_BaseWidget::Padlock_TextureAsset = nullptr;
UTexture2D* UGESGame_BaseWidget::Facebook_TextureAsset = nullptr;
UTexture2D* UGESGame_BaseWidget::Tick_TextureAsset = nullptr;
UTexture2D* UGESGame_BaseWidget::Cross_TextureAsset = nullptr;
const FLinearColor UGESGame_BaseWidget::Colour_Green = FLinearColor(0.05f, 1.f, 0.05f, 1.f);
const FLinearColor UGESGame_BaseWidget::Colour_Red = FLinearColor(1.f, 0.05f, 0.05f, 1.f);
const FLinearColor UGESGame_BaseWidget::Colour_LightTurquoise = FLinearColor(0.15f, 1.f, 1.f, 1.f);
const FLinearColor UGESGame_BaseWidget::Colour_DarkTurquoise = FLinearColor(0.07f, 0.5f, 0.5f, 1.f);
const FLinearColor UGESGame_BaseWidget::Colour_PrepColour = FLinearColor(1.f, 0.5f, 0.05f, 1.f);
const FLinearColor UGESGame_BaseWidget::ToolColour_NONE = FLinearColor(0.6f, 0.6f, 0.6f, 1.f);
const FLinearColor UGESGame_BaseWidget::ToolColour_HCAM = FLinearColor(1.f, 0.5f, 1.0f, 1.f);
const FLinearColor UGESGame_BaseWidget::ToolColour_UVIR = FLinearColor(0.75f, 0.25f, 1.f, 1.f);
const FLinearColor UGESGame_BaseWidget::ToolColour_LIDAR = FLinearColor(0.f, 0.5f, 1.f, 1.f);
const FLinearColor UGESGame_BaseWidget::ToolColour_KLAS = FLinearColor(0.5f, 1.f, 1.f, 1.f);
const FLinearColor UGESGame_BaseWidget::ToolColour_WAVE = FLinearColor(1.f, 1.f, 0.55f, 1.f);
const FLinearColor UGESGame_BaseWidget::ToolColour_EMIT = FLinearColor(0.5f, 1.f, 0.5f, 1.f);
const FText UGESGame_BaseWidget::ToolText_NONE = FText::FromString(TEXT("None"));
const FText UGESGame_BaseWidget::ToolText_HCAM = FText::FromString(TEXT("H-CAM"));
const FText UGESGame_BaseWidget::ToolText_UVIR = FText::FromString(TEXT("UVIR"));
const FText UGESGame_BaseWidget::ToolText_LIDAR = FText::FromString(TEXT("LIDAR"));
const FText UGESGame_BaseWidget::ToolText_KLAS = FText::FromString(TEXT("KLAS"));
const FText UGESGame_BaseWidget::ToolText_WAVE = FText::FromString(TEXT("WAVE"));
const FText UGESGame_BaseWidget::ToolText_EMIT = FText::FromString(TEXT("EMIT"));
UMaterialInterface* UGESGame_BaseWidget::ToolIcon_NONE = nullptr;
UMaterialInterface* UGESGame_BaseWidget::ToolIcon_HCAM = nullptr;
UMaterialInterface* UGESGame_BaseWidget::ToolIcon_UVIR = nullptr;
UMaterialInterface* UGESGame_BaseWidget::ToolIcon_LIDAR = nullptr;
UMaterialInterface* UGESGame_BaseWidget::ToolIcon_KLAS = nullptr;
UMaterialInterface* UGESGame_BaseWidget::ToolIcon_WAVE = nullptr;
UMaterialInterface* UGESGame_BaseWidget::ToolIcon_EMIT = nullptr;
UGESGame_BaseWidget::UGESGame_BaseWidget(const FObjectInitializer& ObjectInitializer) : Super(ObjectInitializer)
{
OwningGESPlayer = NULL;
OwningGESHUD = NULL;
bBindAndroidEvents = false;
// Shared Resources
static ConstructorHelpers::FObjectFinder<UTexture2D>BronzeTex(TEXT("Texture2D'/Game/UI/Textures/Medals/T2D_MBronze.T2D_MBronze'"));
Bronze_TextureAsset = BronzeTex.Object;
static ConstructorHelpers::FObjectFinder<UTexture2D>SilverTex(TEXT("Texture2D'/Game/UI/Textures/Medals/T2D_MSilver.T2D_MSilver'"));
Silver_TextureAsset = SilverTex.Object;
static ConstructorHelpers::FObjectFinder<UTexture2D>GoldTex(TEXT("Texture2D'/Game/UI/Textures/Medals/T2D_MGold.T2D_MGold'"));
Gold_TextureAsset = GoldTex.Object;
static ConstructorHelpers::FObjectFinder<UTexture2D>LockTex(TEXT("Texture2D'/Game/UI/Textures/Medals/T2D_UILocked.T2D_UILocked'"));
Padlock_TextureAsset = LockTex.Object;
static ConstructorHelpers::FObjectFinder<UTexture2D>FacebookTex(TEXT("Texture2D'/Game/UI/Textures/Icons/T2D_FBLogo.T2D_FBLogo'"));
Facebook_TextureAsset = FacebookTex.Object;
static ConstructorHelpers::FObjectFinder<UTexture2D>TickTex(TEXT("Texture2D'/Game/UMG_Assets/Textures/icon_CheckTrue.icon_CheckTrue'"));
Tick_TextureAsset = TickTex.Object;
static ConstructorHelpers::FObjectFinder<UTexture2D>CrossTex(TEXT("Texture2D'/Game/UMG_Assets/Textures/icon_CheckFalse.icon_CheckFalse'"));
Cross_TextureAsset = CrossTex.Object;
// Reused Audio
static ConstructorHelpers::FObjectFinder<USoundBase>DeclineAsset(TEXT("SoundCue'/Game/GESAudio/UI/Unfold/SC_Unfold.SC_Unfold'"));
DeclineAudio = DeclineAsset.Object;
static ConstructorHelpers::FObjectFinder<USoundBase>ConfirmAsset(TEXT("SoundCue'/Game/GESAudio/UI/Unfold/SC_Unfold.SC_Unfold'"));
ConfirmAudio = ConfirmAsset.Object;
static ConstructorHelpers::FObjectFinder<USoundBase>UnfoldAsset(TEXT("SoundCue'/Game/GESAudio/UI/Unfold/SC_Unfold.SC_Unfold'"));
UnfoldAudio = UnfoldAsset.Object;
static ConstructorHelpers::FObjectFinder<USoundBase>SwapAsset(TEXT("SoundWave'/Game/GESAudio/UI/Swap/SW_UISwapPageA.SW_UISwapPageA'"));
SwapAudio = SwapAsset.Object;
static ConstructorHelpers::FObjectFinder<USoundBase>OnAudioAsset(TEXT("SoundWave'/Game/GESAudio/UI/Toggles/SW_UIOn.SW_UIOn'"));
OnAudio = OnAudioAsset.Object;
static ConstructorHelpers::FObjectFinder<USoundBase>OffAudioAsset(TEXT("SoundWave'/Game/GESAudio/UI/Toggles/SW_UIOff.SW_UIOff'"));
OffAudio = OffAudioAsset.Object;
static ConstructorHelpers::FObjectFinder<USoundBase>WarnBadAsset(TEXT("SoundWave'/Game/GESAudio/UI/Mission/SW_MissionFailed.SW_MissionFailed'"));
WarnBadAudio = WarnBadAsset.Object;
static ConstructorHelpers::FObjectFinder<USoundBase>WarnNeutralAudioAsset(TEXT("SoundWave'/Game/GESAudio/UI/Warnings/SW_UIWarnNeutral.SW_UIWarnNeutral'"));
WarnNeutralAudio = WarnNeutralAudioAsset.Object;
static ConstructorHelpers::FObjectFinder<USoundBase>BadActionAudioAsset(TEXT("SoundWave'/Game/GESAudio/UI/Warnings/SW_UIBadAction.SW_UIBadAction'"));
BadActionAudio = BadActionAudioAsset.Object;
// Reused Icons
static ConstructorHelpers::FObjectFinder<UMaterialInterface>ToolIcon_NoneAsset(TEXT("MaterialInstanceConstant'/Game/UI/MaterialInstances/Tools/MInst_NONE.MInst_NONE'"));
ToolIcon_NONE = ToolIcon_NoneAsset.Object;
static ConstructorHelpers::FObjectFinder<UMaterialInterface>ToolIcon_HCAMAsset(TEXT("MaterialInstanceConstant'/Game/UI/MaterialInstances/Tools/MInst_HCam.MInst_HCAM'"));
ToolIcon_HCAM = ToolIcon_HCAMAsset.Object;
static ConstructorHelpers::FObjectFinder<UMaterialInterface>ToolIcon_UVIRAsset(TEXT("MaterialInstanceConstant'/Game/UI/MaterialInstances/Tools/MInst_UVIR.MInst_UVIR'"));
ToolIcon_UVIR = ToolIcon_UVIRAsset.Object;
static ConstructorHelpers::FObjectFinder<UMaterialInterface>ToolIcon_LIDARAsset(TEXT("MaterialInstanceConstant'/Game/UI/MaterialInstances/Tools/MInst_MAG.MInst_MAG'"));
ToolIcon_LIDAR = ToolIcon_LIDARAsset.Object;
static ConstructorHelpers::FObjectFinder<UMaterialInterface>ToolIcon_KLASAsset(TEXT("MaterialInstanceConstant'/Game/UI/MaterialInstances/Tools/MInst_KLAS.MInst_KLAS'"));
ToolIcon_KLAS = ToolIcon_KLASAsset.Object;
static ConstructorHelpers::FObjectFinder<UMaterialInterface>ToolIcon_WAVEAsset(TEXT("MaterialInstanceConstant'/Game/UI/MaterialInstances/Tools/MInst_WAVE.MInst_WAVE'"));
ToolIcon_WAVE = ToolIcon_WAVEAsset.Object;
static ConstructorHelpers::FObjectFinder<UMaterialInterface>ToolIcon_EMITAsset(TEXT("MaterialInstanceConstant'/Game/UI/MaterialInstances/Tools/MInst_EMIT.MInst_EMIT'"));
ToolIcon_EMIT = ToolIcon_EMITAsset.Object;
}
The next step is updating the items you’ve placed in the designer. The easiest way to do this IMO, is create a list of raw pointers (they don’t need to be UPROPERTY(), the widget already references them elsewhere) - and set them all when the Widget runs it’s ‘NativeConstruct()’ function. I also create a ‘HasValidData()’ function in the header, so that I can always check to ensure the widget has been properly initialized before doing something.
NativeConstruct() runs when adding a Widget to the Viewport (which will also create all of it’s children) - so always ensure you add newly created widgets to the viewport / to a slot before calling functions on them.
CODE REMOVED - SEE POST 3!
Now updating all of these items can be done easily from C++, here’s a chunk for example:
void USCGame_SatInfo::UpdateDynamicInfo(const float InDeltaTime)
{
ASSERTV(HasValidData() == true, TEXT("Info Widget Not Initialized"));
const AGESGame_Satellite* SatContext = CurrentContextItem->GetAssignedSatellite();
ASSERTV(SatContext != nullptr, TEXT("Invalid Satellite"));
// Interpolate values for nicer transition effect
BattProgBar_Ptr->SetPercent(FMath::FInterpTo(BattProgBar_Ptr->Percent, SatContext->SatData.Battery_CurrentChargeRatio, InDeltaTime, ValueInterpSpeed));
FuelProgBar_Ptr->SetPercent(FMath::FInterpTo(FuelProgBar_Ptr->Percent, SatContext->SatData.Fuel_CurrentLevelRatio, InDeltaTime, ValueInterpSpeed));
HullProgBar_Ptr->SetPercent(FMath::FInterpTo(HullProgBar_Ptr->Percent, SatContext->SatData.Hull_CurrentLevelRatio, InDeltaTime, ValueInterpSpeed));
HeatProgBar_Ptr->SetPercent(FMath::FInterpTo(HeatProgBar_Ptr->Percent, SatContext->SatData.Heat_CurrentLevelRatio, InDeltaTime, ValueInterpSpeed));
// Interpolate Upgrade Values
Current_UpgradeRatios[0] = FMath::FInterpTo(Current_UpgradeRatios[0], Target_UpgradeRatios[0], InDeltaTime, ValueInterpSpeed);
Current_UpgradeRatios[1] = FMath::FInterpTo(Current_UpgradeRatios[1], Target_UpgradeRatios[1], InDeltaTime, ValueInterpSpeed);
Current_UpgradeRatios[2] = FMath::FInterpTo(Current_UpgradeRatios[2], Target_UpgradeRatios[2], InDeltaTime, ValueInterpSpeed);
Current_UpgradeRatios[3] = FMath::FInterpTo(Current_UpgradeRatios[3], Target_UpgradeRatios[3], InDeltaTime, ValueInterpSpeed);
Current_UpgradeRatios[4] = FMath::FInterpTo(Current_UpgradeRatios[4], Target_UpgradeRatios[4], InDeltaTime, ValueInterpSpeed);
static FName ProgName = TEXT("Progress");
ToolHexDMI_Ptr->SetScalarParameterValue(ProgName, Current_UpgradeRatios[0]);
BattHexDMI_Ptr->SetScalarParameterValue(ProgName, Current_UpgradeRatios[1]);
FuelHexDMI_Ptr->SetScalarParameterValue(ProgName, Current_UpgradeRatios[2]);
HullHexDMI_Ptr->SetScalarParameterValue(ProgName, Current_UpgradeRatios[3]);
HeatHexDMI_Ptr->SetScalarParameterValue(ProgName, Current_UpgradeRatios[4]);
...
This should give you most of the info you need to know to get started. I personally feel as though this system is nice and clean, keeps my code in C++ (where I like it) and makes things nice and accesible. As a bonus, here’s how I create and reference all my widgets in C++, using the HUD class as a manager.
MyHud.h
public:
FORCEINLINE USCGame_SatelliteBank* GetActiveBankWidget() const { return ActiveBankWidget; }
FORCEINLINE USCGame_SatInfo* GetActiveInfoWidget() const { return ActiveInfoWidget; }
protected:
//////////////////////////////////
///// Game Widget Management /////
//////////////////////////////////
// Widget Classes
UPROPERTY(EditDefaultsOnly, Category = "Game Widgets")
TAssetSubclassOf<USCGame_SatelliteBank> BankWidget;
UPROPERTY(EditDefaultsOnly, Category = "Game Widgets")
TAssetSubclassOf<USCGame_SatInfo> InfoWidget;
// Active Widgets
USCGame_SatelliteBank* ActiveBankWidget;
USCGame_SatInfo* ActiveInfoWidget;
// Functions
void CreateGameWidgets();
void RemoveGameWidgets();
Notice how I always call ‘AddToViewport()’ before anything else after creating the widget, to ensure the pointers are valid.
MyHud.cpp
void AGESGame_ClientHUD::CreateBankWidget()
{
ASSERTV(ActiveBankWidget == nullptr, TEXT("Bank Widget Already Initialized"));
ActiveBankWidget = CreateWidget<USCGame_SatelliteBank>(GetOwningPlayerController(), BankWidget.LoadSynchronous());
ASSERTV(ActiveBankWidget != nullptr, TEXT("Unable to Create Bank Widget"));
ActiveBankWidget->AddToViewport(0);
ActiveBankWidget->SetPositionInViewport(FVector2D(0.f, 0.f));
ActiveBankWidget->SetDesiredSizeInViewport(FVector2D(0.f, 174.f));
ActiveBankWidget->SetAlignmentInViewport(FVector2D(0.f, 1.f));
ActiveBankWidget->SetAnchorsInViewport(FAnchors(0.f, 1.f, 1.f, 1.f));
// Creates all Children etc.
ActiveBankWidget->OnAddedToViewport();
}
void AGESGame_ClientHUD::CreateInfoWidget()
{
ASSERTV(ActiveInfoWidget == nullptr, TEXT("Info Widget Already Initialized"));
ActiveInfoWidget = CreateWidget<USCGame_SatInfo>(GetOwningPlayerController(), InfoWidget.LoadSynchronous());
ASSERTV(ActiveInfoWidget != nullptr, TEXT("Unable to Create Bank Widget"));
ActiveInfoWidget->AddToViewport(1);
ActiveInfoWidget->SetPositionInViewport(FVector2D(0.f, 0.f));
ActiveInfoWidget->SetVisibility(ESlateVisibility::Hidden);
}
If this thread proves interesting to people, I’ll also provide examples of how I safely manage animations / interaction via state machines etc. Let me know if this is of use to you!