【UE5】FNameの3つの罠

プログラマーの尾関です。

今回は文字列クラスである FNameを扱う上で注意すべき「3つの罠」について書いてみます。

  • 検証バージョン:UE5.5.4

FNameとは

Ureal Engineには3つの文字列クラスがあり、大まかに以下の特徴があります

  • FName: 識別子として扱う文字列で、ハッシュ化による高速な文字列比較が可能
  • FString: 複雑な文字列処理が可能で、std::stringのような一般的な文字列クラス
  • FText: ローカライズに対応した文字列クラス

FNameが「高速に扱える」という点のみに着目すると、これを文字列として通常扱うのがパフォーマンス的に良さそうに思えますが、これには「3つの罠」があります。

FNameの3つの罠

ということでFNameの3つの罠についてです。

  1. 格納できるデータが “1023byte” まで
  2. FNamePool問題
  3. ハッシュ化に時間がかかる

これらの罠をそれぞれ見ていきます。

罠1: 1023byte制限

FNameはFStringやFTextと異なり、格納可能な文字データが1023byteまでという制限があります。例えばデバッグのログの用に FName を使っていると、通常は問題なく動作している処理が、特定の文字列を FName に代入したタイミングで停止する問題が発生します。

以下のコードを実行すると、FString から FName に代入したタイミングでクラッシュします。

void TestFNameLengthLimit()
{
  FString LongString;
  for (int32 i = 0; i < 1024; ++i)
  {
    LongString += TEXT("A"); // ASCIIなので1バイトずつ増加
  }

  // これはクラッシュする可能性がある
  FName TooLongName(*LongString);
  UE_LOG(LogTemp, Warning, TEXT("Created FName: %s"), *TooLongName.ToString());
}

罠2: FNamePool問題 (登録順による表記汚染)

FNamePool問題とは、FNameは高速化のため、英字の大文字・小文字が先に登録された文字で優先されてしまう仕様があります。

void TestFNamePoolPollution()
{
  FName First(TEXT("MyFunction"));
  FName Second(TEXT("myfunction")); // 見た目は違うが比較では一致

  UE_LOG(LogTemp, Warning, TEXT("First.ToString(): %s"), *First.ToString());
  UE_LOG(LogTemp, Warning, TEXT("Second.ToString(): %s"), *Second.ToString());

  UE_LOG(LogTemp, Warning, TEXT("Comparison equal: %s"), (First == Second) ? TEXT("true") : TEXT("false")); // trueを返してしまう
}

例えば上記の検証コードを実行すると、一般的な文字列クラスであれば比較の結果は「false」で一致しないはずです。ですが FName は「true」を返します。そのため文字列の完全一致を期待するとそれによって不具合が発生する可能性があります。

さらに問題として、FName::ToString() で文字列を取得した場合、登録した文字と同一の文字が返却されることがあります。

  1. FNameで登録した文字は登録順によって内部的な文字が別の文字に置き換えられることがある
  2. FName::ToString() では正常に文字列が取得できる(ことがある)

2の問題について、Debug/Developmentビルド or PIEでは正常に登録した文字が取得でき、Shippingビルドや Cookの場合には異なる文字が返却される場合があります。

Information

つまり開発中は問題が発生せずに、スタンドアロン用にビルドしたり、製品版でビルドしたときに発生することがある…ということです。

具体的には、UFUNCTIONで定義した関数を文字列で取得する場合には FName となります。これを何らかの文字列処理(例えば Lua に登録する関数名など) としたときに特定のプラットフォームでは作られる文字列に差異が生まれ、それによってクラッシュが発生することがあります。

これを避けるには事前に決まった文字を決まった順番で登録する処理をしておくことで、動作が安定することがあります。ただ本質的な問題の解決方法としては、FName::ToString() を避けることが重要です。

罠3: ハッシュ化に時間がかかる問題

3つ目は「ハッシュ化のコスト」です。FNameはハッシュ化することで文字列比較を高速に行うことができますが、ハッシュ化は時間がかかることあります。

例えば、毎Tickで1000以上のFNameをハッシュ化すると ms単位での負荷となることがあります。

これを避けるには事前に FName を構築しておき、それを使い回す(キャッシュする)ことで負荷を初回処理のみに抑えることができます。

なぜFNameではこのような問題が起きるのか

これらの罠が起きる理由としては、FName用途を理解しておくことが重要です。FNameの設計意図としてはアセット名の識別子として扱い、文字列比較を高速で行うためのものです。

それにより、FNameはユーザーの目に見える UIに表示するテキストデータとしてはいけません。

ユーザーの目に見えるテキストは FText を使うべきです。

Information

「FNameとはそもそも“高速なアセット識別子”であって、“見せる文字列”ではない。FName::ToString()は開発者のデバッグ用と考えるべきで、製品ユーザーに見せてはいけない」

おしまい

PGミーティングで、たまたま「FNameの1023byte制限」の話をしたところ、他のプロジェクトでも、FNameに苦しめられたとのことで「FNamePool問題」「ハッシュ化のコスト問題」の話が聞けたので、今回の記事にしました。

以上、FNameの罠についてでした。

おまけ:各文字列クラスの簡単な説明

おまけとして3つのそれぞれの文字列クラスの概要をまとめておきましたので、これらの違いがよくわかっていない…という方は参考にしていただければと思います。

  • FString: 一般的な文字列クラス。自由度の高い文字列操作が可能
  • FName: 変更不可な高速な文字列クラス。文字列比較などを高速に行えためアセット名やシェーダーパラメータなどの識別子に使う。ただし1023byteまでの制限あり
  • FText: 変更不可な文字列クラス。ローカライズためのメタ情報を持ち文字の長さに制限はない
文字列クラス概要主な用途メモリ効率文字列比較ローカライズ
FString一般的な文字列文字列処理。デバッグ用表示文字低い低速
FName軽量な識別用文字識別子、パラメータ名高い高速
FTextローカライズ文字翻訳対象の文字低い低速

FStringは文字列操作(結合、分割、検索など)を行ったり、ログ出力を行う場合など、プログラムで複雑な処理をしたいときに使います。FNameは高速で扱えることから、文字列比較を大量に処理したいときに、例えばFStringからFNameに変換して処理するといった使い方も考えられます。FTextはローカライズ用で、Widgetなどユーザーに表示する必要にあるテキストは、通常この文字列クラスを使用します。

参考

\ 最新情報をチェック /