1. サイトトップ
  2. ブログ
  3. Unreal Engine
  4. UE5
  5. 【UE5】UIをC++と組み合わせて実装してみよう

【UE5】UIをC++と組み合わせて実装してみよう

はじめに

どうもこんにちは、情熱開発部の今井です。
今年も秋が無いんだろうなと思っていたのですが、最近はエアコンがいらない過ごしやすい気候になっていますね。

最近、UE5のUI周りをC++で触る機会が多いので、今回はUIを管理するにあたって知っておくと便利なC++のテクニックを簡単なポップアップUIを作成しつつ紹介していければと思います。

使用バージョン

※UnrealEngine 5.8.3
※ベースプロジェクト ThirdPerson

テクニック紹介に利用するUIのセットアップ

ポップアップUIのレイアウトをWidgetBlueprintで作成

まずは例として表示させるポップアップUI用のWidgetBlueprintを用意していきます(作成場所はContent以下に追加したUIフォルダ)

  1. ContentBrowser右クリック→UserInterface→WidgetBlueprintを押下
  2. 親クラスとしてUserWidgetを選択すると新規作成されるので名前を「WBP_TipsWidget」に設定

では作成されたWBP_TipsWidgetをテキスト表示できる形にしていきます。

CP_TipsWidget[CanvasPanel]、SB_TipsWidget[SizeBox]、TipsText[Text]をそれぞれ配置しつつ、
SizeBoxのAnchorsを中央、Alignmentを両方0.5にしてSizeToContentにチェックを入れるとテキストの大きさにかかわらず中央揃えになります。

ポップアップUIの制御を行うクラスを作成

次は専用のUIクラスを作成し、既存のUIに設定されている親クラスを変更する手順を説明していきます。
まずは新規クラスの作成手順からですね。

  1. ContentBrowserでC++Classesフォルダを選択した状態で右クリック→NewC++Class…
  2. AllClassesに切替→UserWidgetを継承クラスとして選択
  3. Privateに切替→クラス名を「TipsWidget」に設定して作成

Privateフォルダ以下にTipsWidgetクラスが追加されるので、先ほど作成したWBP_TipsWidgetの親クラスをUserWidgetからTipsWidgetに変更していきます。

  1. DesignerからGraphに切替→ClassSettingを押下
  2. DetailsのParentClass項目で親クラスをTipsWidgetに変更

これでWBP_TipsWidgetに専用のクラスを設定できました。

C++でUIを扱う際の基礎知識と実装テクニック紹介

ここからは作成したTipsWidgetクラスをポップアップUI用に編集しつつ、C++でのUI管理周りについてのテクニックを紹介していきます。

WidgetのTick処理を無効化する方法

一つ一つだと影響は小さいですが、複数だと無視できない負荷になるので毎Tick処理を行う必要がないUIでは無効化するのが吉です。

クラス作成直後だとUCLASS()になっている部分をUCLASS(meta = (DisableNativeTick))と記載することでTick処理を無効化できます。

// DisableNativeTickと付けるとTickが無効になる
UCLASS(meta = (DisableNativeTick))
class UTipsWidget : public UUserWidget
{
	GENERATED_BODY()
...

UI要素の初期化イベントの発生順について

C++ではいくつか初期化用の関数がありますが、どれが何か曖昧になりがちなので実行順等をまとめました。

  • 標準コンストラクタ : WidgetTree構築やViewport追加済みか保証されないので、単純なメンバー変数初期化用
  • NativeOnInitialized() : Widgetインスタンス生成直後に一度だけ呼ばれるため、重複実行させたくない処理用
  • NativePreConstruct() : エディタ上の変更時にも実行されるので、World依存処理や動的データ取得はNG
  • NativeConstruct() : Widgetが親WidgetまたはViewportへ追加されるたびに呼ばれる
UCLASS(meta = (DisableNativeTick))
class UTipsWidget : public UUserWidget
{
	GENERATED_BODY()

public:
	// コンストラクタ(バインドしたものにはアクセスできないので注意)
	UTipsWidget(const FObjectInitializer& ObjectInitializer);
	
protected:
	// インスタンス生成時に一度だけ呼ばれる
	virtual void NativeOnInitialized() override;
	// 生成後に呼ばれ、エディタ上で変更を加えた際にも実行される
	virtual void NativePreConstruct() override;
	// Widgetが親WidgetまたはViewportへ追加されるたびに呼ばれる
	virtual void NativeConstruct() override;
...
UTipsWidget::UTipsWidget(const FObjectInitializer& ObjectInitializer):Super(ObjectInitializer)
{
	UE_LOG(LogTemp, Log, TEXT("=== UTipsWidget"));
}

void UTipsWidget::NativeOnInitialized()
{
	Super::NativeOnInitialized();
	UE_LOG(LogTemp, Log, TEXT("=== NativeOnInitialized"));
}

void UTipsWidget::NativePreConstruct()
{
	Super::NativePreConstruct();
	UE_LOG(LogTemp, Log, TEXT("=== NativePreConstruct"));
}

void UTipsWidget::NativeConstruct()
{
	Super::NativeConstruct();
	UE_LOG(LogTemp, Log, TEXT("=== NativeConstruct"));
}

今回はBlueprintでCreateWidgetとAddToViewportを実行する形で、以下のノードをLvl_ThirdPersonに追加しつつログも表示されるようにしました。

ログで表示させている順に上から実行されています(標準コンストラクタが最初)

Widgetのバインド設定について

メンバー変数名と配置したWidget名を同じにすることで、C++から簡単にアクセス可能になるバインド機能があります(今回は試しに透明度をC++側から変更)

C++で使いたいメンバ変数に対して以下を追加するとバインドできます。
UPROPERTY(BlueprintReadWrite, Transient, meta = (BindWidgetOptional))

protected:
	UPROPERTY(BlueprintReadWrite, Transient, meta = (BindWidgetOptional))
	TObjectPtr<class UTextBlock> TipsText;

正常にバインドできているかはIsValid()で確認可能です。

#include "Components/TextBlock.h"

void UTipsWidget::NativeConstruct()
{
	Super::NativeConstruct();
	UE_LOG(LogTemp, Log, TEXT("=== NativeConstruct"));

	// 正常にバインドできているか
	if (IsValid(this->TipsText))
	{
		// 透明度を設定
		this->TipsText->SetOpacity(0.5f);
	}
}

バインドしたWidgetは「BindWidgets」から確認できます。

実行すると正常に透明度(Opacity)が変更されているのが確認できます。

アニメーションの終了タイミングをデリゲートで検知する方法

最後にUIアニメーション終了時に特定の処理をC++で実行させたい場合のコードを紹介していきます。
特にデリゲートは把握しておくと実装の幅が格段に広がります。

必要な関数と変数を用意していきます。

  • DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnFadeInFinished) : フェードイン終了時用デリゲート
  • HandleFadeInFinished() : フェードイン終了時に呼ぶ用の関数(UFUNCTIONを付けること)
  • OnFadeInFinished : 宣言したデリゲートをメンバーとして持たせる
  • Ani_FadeIn(Ani_FadeOut) : アニメーションもバインドさせてメンバーに
// デリゲート宣言用のマクロ
DECLARE_DYNAMIC_MULTICAST_DELEGATE(FOnFadeInFinished);

UCLASS(meta = (DisableNativeTick))
class UTipsWidget : public UUserWidget
{
	GENERATED_BODY()

...

protected:
    // フェードイン終了時に呼ぶ用 ※UFUNCTION()が無いと呼ばれないので注意
    UFUNCTION()
    void HandleFadeInFinished();

public:
	// Blueprintからも購読できるように
	UPROPERTY(BlueprintAssignable)
	FOnFadeInFinished OnFadeInFinished;

protected:
    // バインドしたフェードインアニメ
	UPROPERTY(BlueprintReadWrite, Transient, meta = (BindWidgetAnimOptional))
	TObjectPtr<class UWidgetAnimation> Ani_FadeIn;

    // バインドしたフェードアウトアニメ
	UPROPERTY(BlueprintReadWrite, Transient, meta = (BindWidgetAnimOptional))
	TObjectPtr<class UWidgetAnimation> Ani_FadeOut;

...

NativeConstruct()でフェードイン終了時にHandleFadeInFinished()が実行されるように購読を行います。
BindToAnimationFinished()から指定したアニメーションの終了時にデリゲートを発火させることができます。

#include "Animation/WidgetAnimation.h"
#include "Kismet/KismetSystemLibrary.h"

void UTipsWidget::NativeConstruct()
{
	Super::NativeConstruct();

	// 正常にバインドできているか
	if (IsValid(this->Ani_FadeIn))
	{
		// ディスク/パッケージからロードされた経歴があるか
		if (!HasAnyFlags(RF_WasLoaded))
		{
			// フェードインアニメ終了タイミングで購読
			FWidgetAnimationDynamicEvent FadeInFinishedEvent;
			FadeInFinishedEvent.BindDynamic(this, &UTipsWidget::HandleFadeInFinished);
			this->BindToAnimationFinished(this->Ani_FadeIn, FadeInFinishedEvent);
		}

		// フェードインアニメ再生
		this->PlayAnimation(this->Ani_FadeIn);
	}
}

void UTipsWidget::HandleFadeInFinished()
{
    // 他クラスで使用する用にデリゲートを発火
	this->OnFadeInFinished.Broadcast();

	// 画面にログ表示
	UKismetSystemLibrary::PrintString(
		this,
		"Finished FadeIn",
		true,
		true,
		FColor::Green,
		0.5f);

	if (IsValid(this->Ani_FadeOut))
	{
		this->PlayAnimation(this->Ani_FadeOut);
	}
}

また、他クラスでデリゲートを使用する際は以下のコードで購読することができます。

// 他のクラスでデリゲート発火時に処理を実行する場合
メンバー変数として持たせたUTipsWidget->OnFadeInFinished.AddUniqueUObject(
            this, &このクラス::呼び出したい関数);

メンバーとして持たせたアニメ用変数とエディタで作成したフェードアニメとで名前が同じになっているか注意しましょう。

実行するとフェードインが終了したタイミングでデリゲートが正常に発火し、ログ表示とフェードアウト再生開始処理が走るようになっているのを確認できました。

まとめ

今回紹介したテクニックは基礎的なものですがC++でUIの管理する場合に知っておいて損は無いので活用いただければと思います。
最後までご覧いただきありがとうございました…!


【免責事項】

本サイトでの情報を利用することによる損害等に対し、
株式会社ロジカルビートは一切の責任を負いません。