1. サイトトップ
  2. ブログ
  3. Unreal Engine
  4. UE5
  5. 【UE5】RichTextBlockDecoratorを使ってみた

【UE5】RichTextBlockDecoratorを使ってみた

こんにちは、情熱開発部プログラム課の竹下です。
日ごとに暑さが増し、夏の訪れを間近に感じる頃となってきました。
私もすでにクーラーなしでは過ごせなくなっております。

今回は、Unreal EngineのRichTextBlockDecoratorを利用して、テキスト内に画像や独自のUIを埋め込み表示してみます。

動作環境

UnrealEngine 5.8.0

RichTextBlockDecolatorとは?

RichTextBlockDecoratorとは、RichTextBlock内に入力された特殊なタグを解釈し、対応する処理や装飾を実行するためのクラスです。
通常のRichTextBlockはプレーンなテキストを表示するだけですが、Decoratorを追加・自作することで、テキストの途中に画像を挟んだり、インタラクティブなボタンを埋め込んだりと、タグに応じた独自の表現を実現できるようになります。

GUIでの実装:テキストに画像を埋め込む

概要

Unreal Engineのドキュメントも参考にしつつ、エディタ(GUI)上の操作だけでテキスト内に画像を埋め込む基本的な手順をご紹介します。

1. RichTextStyleRowの設定

まずはRichTextBlock全体のベースとなるフォントスタイルを作成します。
コンテンツブラウザで「データテーブル」を新規作成し、行の構造にRichTextStyleRowを選択します。作成したデータテーブルを開き、行を追加してデフォルトのフォントやサイズを設定しておきます。

2. RichImageRowの設定

次に、テキスト内で使用したい画像を登録します。
先ほどと同様にデータテーブルを新規作成し、今度は行の構造にRichImageRowを選択します。
追加した行に埋め込みたいテクスチャ等の画像データを設定し、タグ呼び出しのキーとなる「Row Name(行の名前)」を設定しておきます。

3. RichTextBlockImageDecoratorの設定

Blueprintの新規作成から、RichTextBlockImageDecoratorを親クラスとして選択し、デコレーターを作成します。
作成したデコレーターを開き、詳細パネルの「Image Set」に、手順2で作成したRichImageRowのデータテーブルを指定します。

4. RichTextBlockの配置

最後に、任意のウィジェットブループリントを開いてRichTextBlockを配置します。
詳細パネルの「Text Style Set」に手順1のデータテーブルを、「Decorators」配列に手順3のデコレーターを登録します。
あとはテキスト入力欄にタグを入力すれば、画像が埋め込まれます。

C++での実装:ホバー時にツールチップを出す

概要

C++でDecoratorクラスを自作すると、単なる画像の埋め込みにとどまらず、テキストの中に「Slate(C++ベースのUI)」を丸ごと作成して埋め込むなど、より複雑で高度な機能を実装することができます。

今回は、「文章中の特定のテキストにマウスカーソルを合わせると、その単語の詳しい説明がツールチップで表示される」 という、RPGのアイテム説明などでよく見かける機能を作ってみます。

1. C++でDecoratorクラスとUIクラスを作成する

Unreal Engineのリッチテキスト拡張には、役割の違う「3つのクラス」を用意する必要があります。まずはこれらの土台(ヘッダー定義)を作成します。
・UMG設定用クラス(URichTextBlockDecorator継承)
・テキスト解析クラス(FRichTextDecorator継承)
・UI描画クラス(SCompoundWidget継承)

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Components/RichTextBlockDecorator.h"
#include "Components/RichTextBlock.h"
#include "MyRichTextBlockDecorator.generated.h"

class SRichTextDetails : public SCompoundWidget
{
public:
    // 初期化
    SLATE_BEGIN_ARGS(SRichTextDetails)
        : _DisplayText()
        , _DisplayTextStyle(&FCoreStyle::Get().GetWidgetStyle<FTextBlockStyle>("Default"))
        , _HoverText()
        , _IsShadowOffset(false)
        {
        }
        SLATE_ATTRIBUTE(FText, DisplayText)
        SLATE_STYLE_ARGUMENT(FTextBlockStyle, DisplayTextStyle)
        SLATE_ATTRIBUTE(FText, HoverText)
        SLATE_ATTRIBUTE(bool, IsShadowOffset)
    SLATE_END_ARGS()

public:
    // 初期化
    void Construct(const FArguments& InArgs);

    FText GetDisplayText() const { return DisplayText.Get(); }

private:
    TAttribute<FText> DisplayText;
    TSharedPtr<STextBlock> TextBlock = nullptr;
};

/**
 * カスタムリッチテキストデコレータ
 */
UCLASS(Blueprintable, BlueprintType)
class DECORATOR_API UMyRichTextBlockDecorator : public URichTextBlockDecorator
{
    GENERATED_BODY()

    friend class FRichTextDetails;

protected:
    virtual TSharedPtr<ITextDecorator> CreateDecorator(URichTextBlock* InOwner) override;

protected:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decorator")
    FString ParseTagName = TEXT("Link"); // 判定したいタグ名
};

class FRichTextDetails : public FRichTextDecorator
{
public:
    FRichTextDetails(
        URichTextBlock* InOwner,
        UMyRichTextBlockDecorator* InDecorator)
        : FRichTextDecorator(InOwner)
        , Decorator(InDecorator)
    {
    }

    virtual bool Supports(const FTextRunParseResults& RunParseResult, const FString& Text) const override;

    virtual TSharedPtr<SWidget> CreateDecoratorWidget(
        const FTextRunInfo& RunInfo,
        const FTextBlockStyle& TextStyle) const override;

private:
    TWeakObjectPtr<UMyRichTextBlockDecorator> Decorator;
};

2. タグを検知してUI生成クラスへ渡す

作成した解析クラスの中身を実装していきます。

#include "MyRichTextBlockDecorator.h"
#include "Widgets/Layout/SScaleBox.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/SToolTip.h"

// --------------------------------------------------------
// UMGクラスからDecoratorを生成する処理
// --------------------------------------------------------
TSharedPtr<ITextDecorator> UMyRichTextBlockDecorator::CreateDecorator(URichTextBlock* InOwner)
{
	return MakeShareable(new FRichTextDetails(InOwner, this));
}

// --------------------------------------------------------
// タグの検知(対象のタグ名と一致しているかチェック)
// --------------------------------------------------------
bool FRichTextDetails::Supports(const FTextRunParseResults& RunParseResult, const FString& Text) const
{
	// 設定したタグ名(Linkなど)と一致したら true を返す
	if (RunParseResult.Name == Decorator->ParseTagName)
	{
		return true;
	}
	return false;
}

// --------------------------------------------------------
// タグ検知後のWidget生成準備(属性の読み取り)
// --------------------------------------------------------
TSharedPtr<SWidget> FRichTextDetails::CreateDecoratorWidget(
	const FTextRunInfo& RunInfo, const FTextBlockStyle& TextStyle) const
{
	FTextBlockStyle DisplayTextStyle = TextStyle;
	FText DescText = FText::GetEmpty();

	// タグの属性に "ID" が含まれているかチェック
	if (RunInfo.MetaData.Contains(TEXT("ID")))
	{
		// idの番号に応じた説明文をセットする
		int32 id = FCString::Atoi(*RunInfo.MetaData[TEXT("ID")]);
		if (id == 0)
		{
			DescText = FText::FromString(TEXT("与えたダメージの一部を回復"));
		}
		else if (id == 1)
		{
			DescText = FText::FromString(TEXT("持続ダメージを与える"));
		}
	}

	// 抽出したテキストと説明文をUI(Slate)クラスに渡して生成
	return SNew(SRichTextDetails)
		.DisplayText(RunInfo.Content)
		.DisplayTextStyle(&DisplayTextStyle)
		.HoverText(DescText);
}

3. 生成されるUIを設定

ホバー時にToolTip(ツールチップ)を使い説明を表示するようにUI生成を行います。

void SRichTextDetails::Construct(const FArguments& InArgs)
{
	DisplayText = InArgs._DisplayText;
	FText HoverText = InArgs._HoverText.Get();
	const FTextBlockStyle* DisplayTextStyle = InArgs._DisplayTextStyle;

	if (!DisplayText.Get().IsEmptyOrWhitespace())
	{
		// 1. テキストブロックを生成
		TextBlock = SNew(STextBlock)
			.TextStyle(DisplayTextStyle)
			.Text(this, &SRichTextDetails::GetDisplayText)
			.ColorAndOpacity(FLinearColor(0.85f, 0.2f, 0.2f, 1.0f));

		// 2. 独自デザインのツールチップを作成する(HoverTextが空でない場合のみ)
		TSharedPtr<SToolTip> CustomToolTip = nullptr;
		if (!HoverText.IsEmpty())
		{
			CustomToolTip = SNew(SToolTip)
				.TextMargin(FMargin(8.0f))
				[
					SNew(STextBlock)
						.Text(HoverText)
						.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
						.ColorAndOpacity(FLinearColor::White)
				];
		}

		// 3. ボタンにツールチップをセットしてUIを構築
		ChildSlot
			[
				SNew(SScaleBox)
					[
						SNew(SButton)
							.ContentPadding(FMargin(0))
							.ButtonStyle(FAppStyle::Get(), "NoBorder")
							.ForegroundColor(FSlateColor::UseForeground())
							.ToolTip(CustomToolTip) // ツールチップの設定
							[
								TextBlock.ToSharedRef()
							]
					]
			];
	}
}

4. RichTextBlockの配置

最後に任意のウィジェットブループリントを開いてRichTextBlockを配置し、「Decorators」に自作デコレーターを登録します。

完成コード

// Fill out your copyright notice in the Description page of Project Settings.

#pragma once

#include "CoreMinimal.h"
#include "Components/RichTextBlockDecorator.h"
#include "Components/RichTextBlock.h"
#include "MyRichTextBlockDecorator.generated.h"

class SRichTextDetails : public SCompoundWidget
{
public:
    // 初期化
    SLATE_BEGIN_ARGS(SRichTextDetails)
        : _DisplayText()
        , _DisplayTextStyle(&FCoreStyle::Get().GetWidgetStyle<FTextBlockStyle>("Default"))
        , _HoverText()
        , _IsShadowOffset(false)
        {
        }
        SLATE_ATTRIBUTE(FText, DisplayText)
        SLATE_STYLE_ARGUMENT(FTextBlockStyle, DisplayTextStyle)
        SLATE_ATTRIBUTE(FText, HoverText)
        SLATE_ATTRIBUTE(bool, IsShadowOffset)
    SLATE_END_ARGS()

public:
    // 初期化
    void Construct(const FArguments& InArgs);

    FText GetDisplayText() const { return DisplayText.Get(); }

private:
    TAttribute<FText> DisplayText;
    TSharedPtr<STextBlock> TextBlock = nullptr;
};

/**
 * カスタムリッチテキストデコレータ
 */
UCLASS(Blueprintable, BlueprintType)
class DECORATOR_API UMyRichTextBlockDecorator : public URichTextBlockDecorator
{
    GENERATED_BODY()

    friend class FRichTextDetails;

protected:
    virtual TSharedPtr<ITextDecorator> CreateDecorator(URichTextBlock* InOwner) override;

protected:
    UPROPERTY(EditAnywhere, BlueprintReadWrite, Category = "Decorator")
    FString ParseTagName = TEXT("Link");
};

class FRichTextDetails : public FRichTextDecorator
{
public:
    FRichTextDetails(
        URichTextBlock* InOwner,
        UMyRichTextBlockDecorator* InDecorator)
        : FRichTextDecorator(InOwner)
        , Decorator(InDecorator)
    {
    }

    virtual bool Supports(const FTextRunParseResults& RunParseResult, const FString& Text) const override;

    virtual TSharedPtr<SWidget> CreateDecoratorWidget(
        const FTextRunInfo& RunInfo,
        const FTextBlockStyle& TextStyle) const override;

private:
    TWeakObjectPtr<UMyRichTextBlockDecorator> Decorator;
};
// Fill out your copyright notice in the Description page of Project Settings.

#include "MyRichTextBlockDecorator.h"
#include "Widgets/Layout/SScaleBox.h"
#include "Widgets/Text/STextBlock.h"
#include "Widgets/Input/SButton.h"
#include "Widgets/SToolTip.h"

void SRichTextDetails::Construct(const FArguments& InArgs)
{
	DisplayText = InArgs._DisplayText;
	FText HoverText = InArgs._HoverText.Get();
	const FTextBlockStyle* DisplayTextStyle = InArgs._DisplayTextStyle;

	if (!DisplayText.Get().IsEmptyOrWhitespace())
	{
		// 1. テキストブロックを生成
		TextBlock = SNew(STextBlock)
			.TextStyle(DisplayTextStyle)
			.Text(this, &SRichTextDetails::GetDisplayText)
			.ColorAndOpacity(FLinearColor(0.85f, 0.2f, 0.2f, 1.0f));

		// 2. 独自デザインのツールチップを作成する(HoverTextが空でない場合のみ)
		TSharedPtr<SToolTip> CustomToolTip = nullptr;
		if (!HoverText.IsEmpty())
		{
			CustomToolTip = SNew(SToolTip)
				.TextMargin(FMargin(8.0f))
				[
					SNew(STextBlock)
						.Text(HoverText)
						.Font(FCoreStyle::GetDefaultFontStyle("Regular", 10))
						.ColorAndOpacity(FLinearColor::White)
				];
		}

		// 3. ボタンにツールチップをセットしてUIを構築
		ChildSlot
			[
				SNew(SScaleBox)
					[
						SNew(SButton)
							.ContentPadding(FMargin(0))
							.ButtonStyle(FAppStyle::Get(), "NoBorder")
							.ForegroundColor(FSlateColor::UseForeground())
							.ToolTip(CustomToolTip)
							[
								TextBlock.ToSharedRef()
							]
					]
			];
	}
}

bool FRichTextDetails::Supports(const FTextRunParseResults& RunParseResult, const FString& Text) const
{
	if (RunParseResult.Name == Decorator->ParseTagName)
	{
		return true;
	}
	return false;
}

TSharedPtr<SWidget> FRichTextDetails::CreateDecoratorWidget(
	const FTextRunInfo& RunInfo, const FTextBlockStyle& TextStyle) const
{
	FTextBlockStyle DisplayTextStyle = TextStyle;

	// タグの中から "ID" 属性を探し、ホバーテキストを決定する
	FText DescText = FText::GetEmpty();
	if (RunInfo.MetaData.Contains(TEXT("ID")))
	{
		int32 id = FCString::Atoi(*RunInfo.MetaData[TEXT("ID")]);

		// idに応じた説明文をセット
		if (id == 0)
		{
			DescText = FText::FromString(TEXT("与えたダメージの一部を回復"));
		}
		else if (id == 1)
		{
			DescText = FText::FromString(TEXT("持続ダメージを与える"));
		}
	}

	// スレートの生成
	return SNew(SRichTextDetails)
		.DisplayText(RunInfo.Content)
		.DisplayTextStyle(&DisplayTextStyle)
		.HoverText(DescText);
}

TSharedPtr<ITextDecorator> UMyRichTextBlockDecorator::CreateDecorator(URichTextBlock* InOwner)
{
	return MakeShareable(new FRichTextDetails(InOwner, this));
}

まとめ

RichTextBlockDecoratorを活用することで、単なる文字列の変更だけでなく、タグ一つで画像やインタラクティブなUIを自由に埋め込み表示できるようになりました。
ゲーム開発において、世界観を伝える文章の見せ方や、ユーザーにとって分かりやすく直感的なテキストUIの構築は非常に重要です。今回紹介した手法を応用すれば、よりリッチで表現力豊かなUIが作れるようになりますので、ぜひご自身のプロジェクトでも試してみてください!


【免責事項】

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