<style>.lazy{display:none}</style> Skip to main content
Tag

tutorial

Unreal Engine C++ Fundamentals – AHUD, UUserWidget and UWidgetAnimation

By Development, Tutorial No Comments

Hey guys,

We are going to continue our C++ Fundamentals lessons by creating a custom HUD using AHUD class, then adding a custom widget to the HUD using UUserWidget and finally rounding things off by adding some UWidgetAnimations to make the whole thing look a bit more pretty.

As usual you can find the starter project on our GitHub page.

Hold up, lets get some background on all this stuff first !

HUD – heads.up.display. this is a way of projecting various bits of information in the form of text, images, animations, etc to inform the player as to what is happening to him, other things, or his environment.

Alternatively a hud can also represent menu screens, credits, etc.

Essentially they are the large canvas on which various components and widgets will be displayed on.

For example:
– hud that show cases all the player attributes like health, mana and stamina
– options menu for configuring graphics properties
– scrolling credits

User Widget – a single purpose component that you can attach to various locations on the hud as well as the view port. this is normally either a single “ui thing” or a logical grouping of “ui things”.

For example:
– a cross hair in a FPS game
– health and mana bars on top of each other
– speed gauge in a car sim

Widget Animation – are user widget components responsible for handling the animation for all the various components added to the user widget.

This includes manipulation of states like transformation, visibility, color and opacity. Which allows designers to create various fun and exciting ways of presenting data and feedback to the player.

Example:
– health bar is actually red liquid that boils away as the player takes damage
– damage indicators that show up when a player hits something and proceed to fall to the ground while fading away

That’s better, now what ?

Well lets take a look at our HUD class and what we can do with it.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
UCLASS()
class UE4FUNDAMENTALS08_API AInGameHUD : public AHUD
{
GENERATED_BODY()
public:
AInGameHUD();
// Primary draw call for the HUD.
virtual void DrawHUD() override;
virtual void BeginPlay() override;
virtual void Tick(float DeltaSeconds) override;
UFUNCTION()
void UpdateComboCount(int32 Value);
UFUNCTION()
void ResetCombo();
UPROPERTY(EditDefaultsOnly, Category = "Interactive")
TSubclassOf<UUserWidget> HitComboWidgetClass;
private:
UHitComboWidget* HitComboWidget;
};
UCLASS() class UE4FUNDAMENTALS08_API AInGameHUD : public AHUD { GENERATED_BODY() public: AInGameHUD(); // Primary draw call for the HUD. virtual void DrawHUD() override; virtual void BeginPlay() override; virtual void Tick(float DeltaSeconds) override; UFUNCTION() void UpdateComboCount(int32 Value); UFUNCTION() void ResetCombo(); UPROPERTY(EditDefaultsOnly, Category = "Interactive") TSubclassOf<UUserWidget> HitComboWidgetClass; private: UHitComboWidget* HitComboWidget; };
UCLASS()
class UE4FUNDAMENTALS08_API AInGameHUD : public AHUD
{
  GENERATED_BODY()

public:
  AInGameHUD();

  // Primary draw call for the HUD.
  virtual void DrawHUD() override;

  virtual void BeginPlay() override;

  virtual void Tick(float DeltaSeconds) override;

  UFUNCTION()
  void UpdateComboCount(int32 Value);

  UFUNCTION()
  void ResetCombo();

  UPROPERTY(EditDefaultsOnly, Category = "Interactive")
  TSubclassOf<UUserWidget> HitComboWidgetClass;

private:
  UHitComboWidget* HitComboWidget;
};

Similar layout to our player or prop classes but you get a few extra functions like DrawHUD() that allows you to control the various aspects of screen components and widgets.

We also include a TSubclassOf<UUserWidget> that allows us to include various custom widget components as part of this HUD.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
void AInGameHUD::BeginPlay()
{
Super::BeginPlay();
if (HitComboWidgetClass)
{
HitComboWidget = CreateWidget<UHitComboWidget>(GetWorld(), HitComboWidgetClass);
/** Make sure widget was created */
if (HitComboWidget)
{
/** Add it to the viewport */
HitComboWidget->AddToViewport();
}
}
}
void AInGameHUD::BeginPlay() { Super::BeginPlay(); if (HitComboWidgetClass) { HitComboWidget = CreateWidget<UHitComboWidget>(GetWorld(), HitComboWidgetClass); /** Make sure widget was created */ if (HitComboWidget) { /** Add it to the viewport */ HitComboWidget->AddToViewport(); } } }
void AInGameHUD::BeginPlay()
{
  Super::BeginPlay();

  if (HitComboWidgetClass)
  {
    HitComboWidget = CreateWidget<UHitComboWidget>(GetWorld(), HitComboWidgetClass);

    /** Make sure widget was created */
    if (HitComboWidget)
    {
      /** Add it to the viewport */
      HitComboWidget->AddToViewport();
    }
  }
}

The important method here is the one that instantiates the custom widget class and adds it to our view port.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
void AInGameHUD::UpdateComboCount(int32 Value)
{
if (HitComboWidget)
{
HitComboWidget->UpdateComboCount(Value);
}
}
void AInGameHUD::UpdateComboCount(int32 Value) { if (HitComboWidget) { HitComboWidget->UpdateComboCount(Value); } }
void AInGameHUD::UpdateComboCount(int32 Value)
{
  if (HitComboWidget)
  {
    HitComboWidget->UpdateComboCount(Value);
  }
}

This is just an example of how we would call our user widget via our hud.

Great we have a HUD but what else do we need ?!

So let’s examine our user widget to see what makes it tick.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
UCLASS()
class UE4FUNDAMENTALS08_API UHitComboWidget : public UUserWidget
{
GENERATED_BODY()
public:
UHitComboWidget(const FObjectInitializer& ObjectInitializer);
virtual void NativeConstruct() override;
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget))
class UTextBlock* TXTCombo;
void UpdateComboCount(int32 Value);
void ResetCombo();
void StoreWidgetAnimations();
UWidgetAnimation* GetAnimationByName(FName AnimationName) const;
private:
TMap<FName, UWidgetAnimation*> AnimationsMap;
UWidgetAnimation* ComboFadeAnimation;
UWidgetAnimation* ComboShakeAnimation;
};
UCLASS() class UE4FUNDAMENTALS08_API UHitComboWidget : public UUserWidget { GENERATED_BODY() public: UHitComboWidget(const FObjectInitializer& ObjectInitializer); virtual void NativeConstruct() override; UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget)) class UTextBlock* TXTCombo; void UpdateComboCount(int32 Value); void ResetCombo(); void StoreWidgetAnimations(); UWidgetAnimation* GetAnimationByName(FName AnimationName) const; private: TMap<FName, UWidgetAnimation*> AnimationsMap; UWidgetAnimation* ComboFadeAnimation; UWidgetAnimation* ComboShakeAnimation; };
UCLASS()
class UE4FUNDAMENTALS08_API UHitComboWidget : public UUserWidget
{
  GENERATED_BODY()
public:
  UHitComboWidget(const FObjectInitializer& ObjectInitializer);

  virtual void NativeConstruct() override;

  UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget))
  class UTextBlock* TXTCombo;

  void UpdateComboCount(int32 Value);

  void ResetCombo();

  void StoreWidgetAnimations();

  UWidgetAnimation* GetAnimationByName(FName AnimationName) const;

private:
  TMap<FName, UWidgetAnimation*> AnimationsMap;

  UWidgetAnimation* ComboFadeAnimation;
  UWidgetAnimation* ComboShakeAnimation;
};

As we can see in the header the deafult constructor for a UUserWidget is a bit different as it expects an ObjectInitializer to be passed in.

We also don’t have a BeginPlay but rather we expose NativeConstruct which esentially behaves the same way and loads stuff after the constructor is already loaded.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget))
class UTextBlock* TXTCombo;
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget)) class UTextBlock* TXTCombo;
UPROPERTY(EditAnywhere, BlueprintReadWrite, meta = (BindWidget))
class UTextBlock* TXTCombo;

This bit here allows us to auto bind by name to a component on the user widget. In this case we are looking for a UTextBlock of name TXTCombo.

[ insert warning image ]

NOTE: You will get warnings if these attributes are not present.

And lastly we have our declared animations.

Ok, moving on the cpp file.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
void UHitComboWidget::NativeConstruct()
{
Super::NativeConstruct();
StoreWidgetAnimations();
ComboFadeAnimation = GetAnimationByName(TEXT("ComboFade"));
ComboShakeAnimation = GetAnimationByName(TEXT("ComboShake"));
}
void UHitComboWidget::NativeConstruct() { Super::NativeConstruct(); StoreWidgetAnimations(); ComboFadeAnimation = GetAnimationByName(TEXT("ComboFade")); ComboShakeAnimation = GetAnimationByName(TEXT("ComboShake")); }
void UHitComboWidget::NativeConstruct()
{
  Super::NativeConstruct();

  StoreWidgetAnimations();

  ComboFadeAnimation = GetAnimationByName(TEXT("ComboFade"));
  ComboShakeAnimation = GetAnimationByName(TEXT("ComboShake"));
}

In the native construct we do a few things, we first use the StoreWidgetAnimations function to populate a map of animations keyed by name.

We then use that data to populate our animation variables by their names of ComboFade and ComboShake respectively.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
void UHitComboWidget::UpdateComboCount(int32 Value)
{
// only update if more than one hit
if (TXTCombo && Value > 1)
{
if (TXTCombo->Visibility == ESlateVisibility::Hidden)
{
TXTCombo->SetVisibility(ESlateVisibility::Visible);
}
TXTCombo->SetText(FText::FromString((FString::FromInt(Value) + "x Combo")));
if (ComboShakeAnimation)
{
PlayAnimation(ComboShakeAnimation, 0.f, 1, EUMGSequencePlayMode::Forward, 1.f);
}
if (ComboFadeAnimation)
{
PlayAnimation(ComboFadeAnimation, 0.f, 1, EUMGSequencePlayMode::Forward, 1.f);
}
}
}
void UHitComboWidget::UpdateComboCount(int32 Value) { // only update if more than one hit if (TXTCombo && Value > 1) { if (TXTCombo->Visibility == ESlateVisibility::Hidden) { TXTCombo->SetVisibility(ESlateVisibility::Visible); } TXTCombo->SetText(FText::FromString((FString::FromInt(Value) + "x Combo"))); if (ComboShakeAnimation) { PlayAnimation(ComboShakeAnimation, 0.f, 1, EUMGSequencePlayMode::Forward, 1.f); } if (ComboFadeAnimation) { PlayAnimation(ComboFadeAnimation, 0.f, 1, EUMGSequencePlayMode::Forward, 1.f); } } }
void UHitComboWidget::UpdateComboCount(int32 Value)
{
  // only update if more than one hit
  if (TXTCombo && Value > 1)
  {
    if (TXTCombo->Visibility == ESlateVisibility::Hidden)
    {
      TXTCombo->SetVisibility(ESlateVisibility::Visible);
    }
    TXTCombo->SetText(FText::FromString((FString::FromInt(Value) + "x Combo")));

    if (ComboShakeAnimation)
    {
      PlayAnimation(ComboShakeAnimation, 0.f, 1, EUMGSequencePlayMode::Forward, 1.f);
    }

    if (ComboFadeAnimation)
    {
      PlayAnimation(ComboFadeAnimation, 0.f, 1, EUMGSequencePlayMode::Forward, 1.f);
    }
  }
}

In this method, we manipulate the state of the TXTCombo viability based on it being reset and set it back to visible.

We also take in the user provided combo count and add it to the text.

Once that is in place we play back the two animations, one to shake the text and the other to fade it away after a few seconds.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
void UHitComboWidget::ResetCombo()
{
GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Orange, __FUNCTION__);
if (TXTCombo)
{
TXTCombo->SetVisibility(ESlateVisibility::Hidden);
}
}
void UHitComboWidget::ResetCombo() { GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Orange, __FUNCTION__); if (TXTCombo) { TXTCombo->SetVisibility(ESlateVisibility::Hidden); } }
void UHitComboWidget::ResetCombo()
{
  GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Orange, __FUNCTION__);

  if (TXTCombo)
  {
    TXTCombo->SetVisibility(ESlateVisibility::Hidden);
  }
}

In this method we reset the visibility of the TXTCombo back to hidden.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
void UHitComboWidget::StoreWidgetAnimations()
{
AnimationsMap.Empty();
UProperty* Prop = GetClass()->PropertyLink;
// check each property of this class
while (Prop)
{
// only evaluate object properties, skip rest
if (Prop->GetClass() == UObjectProperty::StaticClass())
{
UObjectProperty* ObjProp = Cast<UObjectProperty>(Prop);
// only get back properties that are of type widget animation
if (ObjProp->PropertyClass == UWidgetAnimation::StaticClass())
{
UObject* Obj = ObjProp->GetObjectPropertyValue_InContainer(this);
// only get back properties that are of type widget animation
UWidgetAnimation* WidgetAnimation = Cast<UWidgetAnimation>(Obj);
// if casting worked update map with new animation
if (WidgetAnimation && WidgetAnimation->MovieScene)
{
FName AnimName = WidgetAnimation->MovieScene->GetFName();
GEngine->AddOnScreenDebugMessage(-1, 4.5f, FColor::Magenta, AnimName.ToString());
AnimationsMap.Add(AnimName, WidgetAnimation);
}
}
}
Prop = Prop->PropertyLinkNext;
}
}
void UHitComboWidget::StoreWidgetAnimations() { AnimationsMap.Empty(); UProperty* Prop = GetClass()->PropertyLink; // check each property of this class while (Prop) { // only evaluate object properties, skip rest if (Prop->GetClass() == UObjectProperty::StaticClass()) { UObjectProperty* ObjProp = Cast<UObjectProperty>(Prop); // only get back properties that are of type widget animation if (ObjProp->PropertyClass == UWidgetAnimation::StaticClass()) { UObject* Obj = ObjProp->GetObjectPropertyValue_InContainer(this); // only get back properties that are of type widget animation UWidgetAnimation* WidgetAnimation = Cast<UWidgetAnimation>(Obj); // if casting worked update map with new animation if (WidgetAnimation && WidgetAnimation->MovieScene) { FName AnimName = WidgetAnimation->MovieScene->GetFName(); GEngine->AddOnScreenDebugMessage(-1, 4.5f, FColor::Magenta, AnimName.ToString()); AnimationsMap.Add(AnimName, WidgetAnimation); } } } Prop = Prop->PropertyLinkNext; } }
void UHitComboWidget::StoreWidgetAnimations()
{
  AnimationsMap.Empty();

  UProperty* Prop = GetClass()->PropertyLink;

  // check each property of this class
  while (Prop)
  {
    // only evaluate object properties, skip rest
    if (Prop->GetClass() == UObjectProperty::StaticClass())
    {
      UObjectProperty* ObjProp = Cast<UObjectProperty>(Prop);

      // only get back properties that are of type widget animation
      if (ObjProp->PropertyClass == UWidgetAnimation::StaticClass())
      {
        UObject* Obj = ObjProp->GetObjectPropertyValue_InContainer(this);
        // only get back properties that are of type widget animation
        UWidgetAnimation* WidgetAnimation = Cast<UWidgetAnimation>(Obj);
        // if casting worked update map with new animation
        if (WidgetAnimation && WidgetAnimation->MovieScene)
        {
          FName AnimName = WidgetAnimation->MovieScene->GetFName();
          GEngine->AddOnScreenDebugMessage(-1, 4.5f, FColor::Magenta, AnimName.ToString());
          AnimationsMap.Add(AnimName, WidgetAnimation);
        }
      }
    }

    Prop = Prop->PropertyLinkNext;
  }
}

This is the big method that allows us to process each property of the current user widget and evaluate them for widget animations.

When a widget animation is found we update our private map with those details.

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
UWidgetAnimation* UHitComboWidget::GetAnimationByName(FName AnimationName) const
{
UWidgetAnimation* const* WidgetAnimation = AnimationsMap.Find(AnimationName);
if (WidgetAnimation)
{
return *WidgetAnimation;
}
return nullptr;
}
UWidgetAnimation* UHitComboWidget::GetAnimationByName(FName AnimationName) const { UWidgetAnimation* const* WidgetAnimation = AnimationsMap.Find(AnimationName); if (WidgetAnimation) { return *WidgetAnimation; } return nullptr; }
UWidgetAnimation* UHitComboWidget::GetAnimationByName(FName AnimationName) const
{
  UWidgetAnimation* const* WidgetAnimation = AnimationsMap.Find(AnimationName);
  if (WidgetAnimation)
  {
    return *WidgetAnimation;
  }
  return nullptr;
}

This method allows us to retrieve our animation by the keyed name.

In addition to the video here is a bunch of additional reading material:

Unreal Engine C++ Fundamentals – FTimerHandle & Timers

By Development, Tutorial, Unreal No Comments

Hey guys,

We are bouncing back to C++ Fundamentals this time looking at FTimerHandle and  Timers in general by messing around with our props.

Additionally you can always find the sample project on our GitHub page.

Timers ?! What are they ?

Timers allow us to trigger events based on elapsed time in the form of creating asynchronous callbacks to specific function pointers.

Plain English: we light a fuse, let it burn down, something explodes !

How do we make these magical exploding clocks ?

Well let’s take a look at some code as these things are pretty easy to understand.

.h

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
FTimerHandle TriggerDestroyTimerHandle;
UFUNCTION()
void TriggerDestroy();
FTimerHandle TriggerDestroyTimerHandle; UFUNCTION() void TriggerDestroy();
FTimerHandle TriggerDestroyTimerHandle;

UFUNCTION()
void TriggerDestroy();

.cpp

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
GetWorld()->GetTimerManager().SetTimer(TriggerDestroyTimerHandle, this, &ADestructibleProp::TriggerDestroy, 5.f, true);
void ADestructibleProp::TriggerDestroy()
{
// do stuff
}
GetWorld()->GetTimerManager().SetTimer(TriggerDestroyTimerHandle, this, &ADestructibleProp::TriggerDestroy, 5.f, true); void ADestructibleProp::TriggerDestroy() { // do stuff }
GetWorld()->GetTimerManager().SetTimer(TriggerDestroyTimerHandle, this, &ADestructibleProp::TriggerDestroy, 5.f, true);

void ADestructibleProp::TriggerDestroy()
{
  // do stuff
}

So as you can see the implementation is pretty easy to understand.

  • We are creating a timer from our Time Manager object.
  • We are then registering this call back to use our Timer Handle.
  • Then we assign a callback method to be triggered at the end of our countdown.
  • Then finally we set that the countdown duration is 5 seconds.

Excellent ! Let’s blow some things up.

Timers can also be used to trigger modifications to specific values every so often. In this case we are going to simulate a fuse that will count down every second before it triggers an event.

First we are going to call the timer as we did previously, except this time we are going to call a new method and have this timer run every second.

.cpp

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
GetWorld()->GetTimerManager().SetTimer(TriggerDestroyTimerHandle, this, &ADestructibleProp::TriggerCountdownToDestroy, 1.f, true);
GetWorld()->GetTimerManager().SetTimer(TriggerDestroyTimerHandle, this, &ADestructibleProp::TriggerCountdownToDestroy, 1.f, true);
GetWorld()->GetTimerManager().SetTimer(TriggerDestroyTimerHandle, this, &ADestructibleProp::TriggerCountdownToDestroy, 1.f, true);

Then we add in our count down logic

.h

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
int32 TriggerCountdown;
UFUNCTION()
void TriggerCountdownToDestroy();
int32 TriggerCountdown; UFUNCTION() void TriggerCountdownToDestroy();
int32 TriggerCountdown;

UFUNCTION()
void TriggerCountdownToDestroy();

Finally we add in our implementation where we modify our count down variable by subtracting one from it every second.

Additionally we trigger a second timer call to our original method by randomly generating a delay before it gets triggered.

.cpp

Plain text
Copy to clipboard
Open code in new window
EnlighterJS 3 Syntax Highlighter
void ADestructibleProp::TriggerCountdownToDestroy()
{
// count down to zero
if (--TriggerCountdown <= 0)
{
int32 RandomDelay = FMath::RandRange(1, 5);
GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Orange, "Boom ! with a delay of: " + FString::FromInt(RandomDelay));
GetWorld()->GetTimerManager().SetTimer(TriggerDestroyTimerHandle, this, &ADestructibleProp::TriggerDestroy, RandomDelay, true);
}
else
{
GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Orange, "Counting down to explosion: " + FString::FromInt(TriggerCountdown));
}
}
void ADestructibleProp::TriggerCountdownToDestroy() { // count down to zero if (--TriggerCountdown <= 0) { int32 RandomDelay = FMath::RandRange(1, 5); GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Orange, "Boom ! with a delay of: " + FString::FromInt(RandomDelay)); GetWorld()->GetTimerManager().SetTimer(TriggerDestroyTimerHandle, this, &ADestructibleProp::TriggerDestroy, RandomDelay, true); } else { GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Orange, "Counting down to explosion: " + FString::FromInt(TriggerCountdown)); } }
void ADestructibleProp::TriggerCountdownToDestroy()
{
  // count down to zero
  if (--TriggerCountdown <= 0) 
  {
    int32 RandomDelay = FMath::RandRange(1, 5);

    GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Orange, "Boom ! with a delay of: " + FString::FromInt(RandomDelay));
    GetWorld()->GetTimerManager().SetTimer(TriggerDestroyTimerHandle, this, &ADestructibleProp::TriggerDestroy, RandomDelay, true);
  }
  else 
  {
    GEngine->AddOnScreenDebugMessage(-1, 3.f, FColor::Orange, "Counting down to explosion: " + FString::FromInt(TriggerCountdown));
  }
}

This allows us to have custom implementations in our timers that perform very specific work.

Pretty cool !

 

Below you can find a few more links with some additional reading material:

Unreal C++ Props – Part Two- Physics Constraints BONUS: Attack modifiers in C++

By Development, Props, Tutorial, Unreal No Comments

Hey guys,

Today we are going to continue evolving our props, this time by introducing physical components.

Physical components allow us to create things like punching bags, springs, various pivot points as well as hanging our player from the ceiling in various sadistic ways. How fun !

In addition to that we are going to throw in some addition attack modifiers so we can easily switch between light, medium and strong attacks.

The starter project is available on our GitHub page.

Below is a bit more reading material for those curious about all the various options in physics constraints: