[UE4] RichTextBlock でタイプライター演出

![99991d26e592bd48f0606c329ca8f9ec.gif](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/ce3a3160-f386-4c76-5205-b507d335e593.gif)
アドベンチャーゲームでテキストを一文字ずつ表示させるアレです。
一文字ずつ表示するだけなら TextBlock で十分なのだけれど、文字色変えたり装飾を加えるために RichTextBlock を使いたい。でも構文解析とかどうするんだ？って話になります。

## RichTextBlock シリーズ
[[UE4] RichTextBlock でルビを振る](https://qiita.com/koorinonaka/items/34a47303e9d9c5393105)
[[UE4] RichTextBlock で独自タグを定義する](https://qiita.com/koorinonaka/items/39b01d1652d698e3d73b)

# RichTextBlock について（軽く）
[UMG Rich Text Block - Unreal Engine 4 Documentation](https://docs.unrealengine.com/ja/InteractiveExperiences/UMG/UserGuide/UMGRichTextBlock/index.html)

>吾輩は\<Blue>猫</>である。名前はまだ無い。

よく使う機能としてはテキスト中の部分的な色変えですね。色だけでなくフォントスタイルやその他装飾も設定することができます。フォント設定の部分変更ができなかったり、入れ子構造（\<span>\<span>文字</></>とか）にできなかったりするのはなんとかしたいところですが、、調べてみるとそれはもう柔軟に拡張できるような設計がされています（実装を読むの楽しい）。その辺はまた次回にでも。

# 一文字ずつ表示する
よくある実装だと、FString::Len() で文字数を取得してテキスト置換するなどがあると思います。

>吾輩は\<B

が、例えば 5 文字目までの切り抜きだとこうなってしまいますね。
構文部分も巻き込まれるため、整形後の文字数を取得する必要があります。

## 構文解析クラスを拡張する
構文解析の実装は FDefaultRichTextMarkupParser でされており、URichTextBlock::CreateMarkupParser から拡張ができるようになっています。まずは URichTextBlock 継承の UMyRichTextBlock を作成します。

```C++:MyRichTextBlock.cpp
TSharedPtr<IRichTextMarkupParser> UMyRichTextBlock::CreateMarkupParser()
{
	// 後で MarkupParserInstance にアクセスするために SharedPtr をメンバ変数に保存する
	RichTextMarkupParser = MakeShareable( new FMyDefaultRichTextMarkupParser() );
	return RichTextMarkupParser;
}

void UMyRichTextBlock::SetCharacterCount( int32 NewCount )
{
	if ( RichTextMarkupParser.IsValid() )
	{
		// 後述の MyDefaultRichTextMarkupParser へ表示位置を渡す
		RichTextMarkupParser->SetDisplayCharacterCount( NewCount );
		if ( MyRichTextBlock.IsValid() )
		{
			// 解析処理を再実行
			MyRichTextBlock->Refresh();
		}
	}
}
```

ここでは FDefaultRichTextMarkupParser を複製した FMyDefaultRichTextMarkupParser を作成して拡張します。

```C++:MyRichTextMarkupProcessing.cpp
void FMyDefaultRichTextMarkupParser::Process(
	TArray<FTextLineParseResults>& Results, const FString& Input, FString& Output )
{
	// エンジン側実装の構文解析処理
	TArray<FTextRange> LineRanges;
	FTextRange::CalculateLineRangesFromString( Input, LineRanges );
	ParseLineRanges( Input, LineRanges, Results );
	HandleEscapeSequences( Input, Results, Output );

	// 整形後の文字列をさらに整形する処理を追加
	bHiddenCharacters = HiddenCharacters( Results, DisplayString, Output );
}

void FMyDefaultRichTextMarkupParser::SetDisplayCharacterCount( int32 NewCount )
{
	// 表示位置を指定する
	DisplayCharacterCount = NewCount;
}

bool FMyDefaultRichTextMarkupParser::HiddenCharacters(
	const TArray<FTextLineParseResults>& LineParseResultsArray,
	FString& OutDisplayString, FString& InOutConcatenatedLines ) const
{
	bool bFoundToReplace = false;
	OutDisplayString.Empty();

	if ( !InOutConcatenatedLines.IsEmpty() && DisplayCharacterCount != INDEX_NONE )
	{
		int32 CharIndex = 0;
		for ( const FTextLineParseResults& LineParseResult : LineParseResultsArray )
		{
			for ( const FTextRunParseResults& RunParseResult : LineParseResult.Runs )
			{
				FTextRange TargetRange =
					RunParseResult.ContentRange.IsEmpty() ?
					RunParseResult.OriginalRange : RunParseResult.ContentRange;

				// 整形後の文字列を保存する
				OutDisplayString += InOutConcatenatedLines.Mid(
					TargetRange.BeginIndex, TargetRange.EndIndex - TargetRange.BeginIndex );

				bool bFoundInRunParseResult = false;
				for ( int32 i = 0; i < TargetRange.Len(); ++i )
				{
					int32 TargetIndex = TargetRange.BeginIndex + i;
					if ( ensure( InOutConcatenatedLines.IsValidIndex( TargetIndex ) ) )
					{
						if ( CharIndex++ < DisplayCharacterCount )
						{
							// 表示文字であれば何もしない
							continue;
						}

						// 表示位置まで到達していない文字列は空白に置換する
						InOutConcatenatedLines[TargetIndex] = *TEXT( "\u0020" );
						bFoundInRunParseResult = true;
					}
				}

				bFoundToReplace |= bFoundInRunParseResult;
			}
		}
	}

	return bFoundToReplace;
}
```

タイプライターで隠す部分を空白に置換してます。
テキスト内の空白はレイアウト側でいい感じに処理してくれます。いい感じに。

## ブループリントで適当に処理を書く
![6d232b6699981ffc6382d7c416c83b9d.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/41105/173ba8a4-cf10-1c29-0101-f9b2c611b6bc.png)
Tick で DeltaTime を加算して表示間隔時間で割るだけです。