このドキュメントでは Unreal Engine (UE) が使用する文字エンコードの概要を説明します。
「The Absolute Minimum Every Software Developer Absolutely, Positively Must Know About Unicode and Character Sets (No Excuses!)」に必要とされる知識が説明されています。
テキスト フォーマット
テキストや文字列を表現するために使用可能な形式がいくつかあります。こうした形式とその良い点と悪い点を理解することで、プロジェクトでどの形式を使うかを決めるのに役立ちます。
以下はフォーマットの技術的定義ではありませんが、このドキュメント用の簡易バージョンとなっています。
$ ASCII : 32と126 (32 と 126 を含む) の間の文字、および 0、9、10、13 です。(P4 タイプのテキスト) (チェックイン時に P4 のトリガーで検証済みです) $ ANSI :ASCII と現行のコードページです (例えば Western European high ASCII) (P4 サーバーにバイナリとして格納しなくてはいけません)。 $ UTF-8 : 8 ビットで構成される文字列です。非 ANSI 文字の生成に特別な文字のシーケンスを使用できます (ASCII のスーパーセット) (P4 タイプの Unicode)。 $ UTF-16 : [BOM] (http://en.wikipedia.org/wiki/Byte-order_mark) 付きで 1 文字を 16 ビットで構成するする文字列です (アストラル文字は 32 ビットまで可能) (P4 タイプの UTF-16) (チェックインの際に P4 トリガーで検証されます)。
バイナリの例
メリット | デメリット |
---|---|
内部フォーマットが定義されていません。フォーマットに関係なく各ファイルを読み込むことができます。 | マージできません。このタイプの全てのファイルは排他的チェックアウトが必要です。 |
内部フォーマットが定義されていません。それぞれのファイルが異なるフォーマットになる場合もあります。 | |
P4 は各バージョンを全て格納します。デポのサイズが必要以上に大きくなる要因となります。 |
テキストの例
メリット | デメリット |
---|---|
マージ可能です。排他的なチェックアウトは必要ありません。 | とても限定的で、ASCII 文字のみを許容します。 |
UTF-8 の例
メリット | デメリット |
---|---|
必要に応じて全ての文字に簡単にアクセスできます。 | アジア系言語に対し別のメモリプロファイルがあります。 |
メモリ消費が少ないです。 | P4 タイプの Unicode は Perforce サーバーでは有効ではありません。 |
ASCII のスーパーセットです。単純な ASCII 文字列は、完全に有効な UTF-8 文字列です。 | 文字列操作がより複雑です。 長さの計算のような簡単な操作さえも文字列をパースしなくてはいけません。 |
ゲームが文字列を ASCII と認識しても機能し、そのように出力をします。 | アジア地域では、 MSDev は ASCII 以外は上手く処理することができないため、チェックイン時にテキストを ASCII として検証します。 |
Unicode が有効になっているサーバーの場合、ファイルのマージが可能で排他的なチェックアウトは必要ありません。 | |
パースして文字列が UTF-8 かどうかを検知することができます (BOM の有無に関係なく) |
UTF-16 の例
メリット | デメリット |
---|---|
必要に応じて全ての文字に簡単にアクセスできます。 | より多くのメモリを使用します。 |
簡単です。メモリの使用量は文字数の 2 倍になります (弊社が使用する文字は全て Basic Multilingual Plane) にあります。 | BOM が無い場合はこのフォーマットの検知は困難です。 |
簡単です。文字列操作は文字列をパースせずに分割/結合することができます。 | ゲームが文字列を ASCII と検知した時は機能せず、その旨、出力します (UTF-16 検証ソフトでチェックイン時に検証が可能になりました)。 |
ゲームで使用しているフォーマットと同じです。変換、パース、メモリ操作は必要ありません。 | MSDev はアジア地域では、ASCII 以外は何も処理しません。 これがチェックイン時にテキストを ASCII として検証する理由です。 |
マージ可能です。排他的なチェックアウトは必要ありません。 | |
C# 内部で UTF-16 を使用します。 |
UE4 の内部文字列の表現
Unreal Engine 4 (UE4) の全文字列は、FStrings や TCHAR 配列などの UTF-16 フォーマットでメモリに格納しています。多くのコードが 2 バイトを 1 コードポイントと想定しているため、基本多言語プレーン (Basic Multilingual Plane:BMP) のみをサポートしています。アンリアルの内部エンコードは UCS-2 として記述するのがより正確です。文字列は現行プラットフォームのエンディアンネス (メモリ上でのバイトの並び) に適した方法で格納されます。
パッケージにシリアル化する場合や、ディスクにまたはディスクからシリアル化する場合、またはネットワークの送受信でシリアル化する場合は、0xff より小さい TCHAR 文字は全て (8 ビット) バイト列として格納されます。それ以外は 2 バイトの UTF-16 文字列として格納されます。シリアライズコードは、必要に応じていかなるエンディアン変換も処理することができます。
UE4 でロードするテキスト ファイル
Unreal が外部のテキストファイルをロードする時は (例えばランタイム時の .INT
ファイルの読み込み)、ほとんどの場合、「UnMisc.cpp
」にある appLoadFileToString()
関数で処理します。主な処理は、appBufferToString()
関数で行います。
この関数は、UTF-16 ファイルにある Unicode のバイトオーダーマーク (BOM) を読み取り、もし BOM があれば、そのファイルを UTF-16 ファイルとしてビッグエンディアン順もしくはリトルエンディアン順で読み込みます。
BOM が存在しない場合、挙動はプラットフォームによって異なります。
Windows では、デフォルトの Windows MBCS エンコードを使用してテキストを UTF-16 に変換して (米国英語および西ヨーロッパは Windows-1252、韓国語は CP949、日本語は CP932)、MultiByteToWideChar(CP_ACP, MB_ERR_INVALID_CHARS...) を使用します。これは 2009 年 7 月頃の QA ビルドで追加されました。
Windows 以外のプラットフォームで変換に失敗した場合、関数は単にそれぞれのバイトを読み込み、読み込んだものを 16 ビットにパッドして TCHAR
の配列を作成します。
なお、appLoadFileToString()
関数でロードした、UTF-8 エンコード テキストファイルを検出またはデコードするためのコードはありません。
UE で保存したテキスト ファイル
エンジンによって生成されるテキストファイルの多くは、appSaveStringToFile()
関数を利用して保存します。
TCHAR 型の文字がすべてシングルバイトで表されている文字列は、8-bit バイト列として格納されます。もしくは bAlwaysSaveAsAnsi フラグが true で渡されない限り、UTF-16 として格納されます。その場合、まずデフォルトの Windows エンコード形式に変換されます。現時点ではシェーダーファイルのみで実行され、シェーダーコンパイラが抱える UTF-16 ファイルに関する問題を回避します。
UE で使用するテキストファイルに推奨されるエンコード
INT ファイルと INI ファイル
どちらかのバイトオーダー順の UTF-16 です。デフォルトのアジア言語用の MBCS 文字 (例えば CP932) が Windows 上で機能する一方で、これらのファイルを PS3 と Xbox360 プラットフォームへロードする必要があり、変換コードは Windows のみで実行されます。
ソースコード
一般的に、C++ ソースコード内部への文字列リテラルの格納は推奨しておらず、このデータを INT ファイルに格納することを推奨します。
C++ ソースコード
UTF-8 またはデフォルトの Windows のエンコードです。MSVC、Xbox360 コンパイラ、gcc はすべて、UTF-8 でエンコードされたソースファイルで問題ないはずです。例えば著作権、商標、「度」のシンボルのような高いビット セットの文字を持つ Latin-1 でエンコードされたファイルは、ソースコードでは可能な限り避けるべきです。これは、異なるロケールを持つシステム上で符号化が壊れるためです。サードパーティのソフトウェアでのいくつかの事例は回避不可能 (例:著作権表示) なので、MSVC に関しては、警告 4819 を無効化します。これは、アジアの Windows でコンパイルを行う際に起こる警告です。
UTF-16 テキストファイルを Perforce に格納する
-
'Text' を使用しないでください。
UTF-x ファイルがチェックインされている状態でテキストとして格納すると、同期後にファイルは破損します。
-
「バイナリ」を使用する場合、ファイルに排他的チェックアウトのマークをしてください。
- ASCII、UTF-8、UTF-16 文字コードとしてチェックインが可能で、これらはエンジンで機能します。
- しかし、バイナリファイルはマージすることができないので、ファイルが排他的チェックアウトとマークされていない場合は変更は無視されます。
- 'UTF-16' を使用する場合、UTF-16 以外のファイルがチェックインされない様に注意してください。
- 'Unicode' 型は UTF-8 を用いて変換します。ここでは役に立ちません。
変換ルーチン
さまざまなコードから、またさまざまなコードへ文字列を変換する多くのマクロがあります。これらのマクロは、ローカル スコープで宣言されたクラス インスタンスを使用し、スタック上でスペースを割り当てるため、これらへのポインタを保持しないことが非常に重要です。関数呼び出しへ文字列を渡すためだけに使用します。
- TCHAR_TO_ANSI(str)
- TCHAR_TO_OEM(str)
- ANSI_TO_TCHAR(str)
- TCHAR_TO_UTF8(str)
- UTF8_TO_TCHAR(str)
「UnStringConv.h」ファイルから以下のヘルパクラスを使用します。
typedef TStringConversion<TCHAR,ANSICHAR,FANSIToTCHAR_Convert> FANSIToTCHAR;
typedef TStringConversion<ANSICHAR,TCHAR,FTCHARToANSI_Convert> FTCHARToANSI;
typedef TStringConversion<ANSICHAR,TCHAR,FTCHARToOEM_Convert> FTCHARToOEM;
typedef TStringConversion<ANSICHAR,TCHAR,FTCHARToUTF8_Convert> FTCHARToUTF8;
typedef TStringConversion<TCHAR,ANSICHAR,FUTF8ToTCHAR_Convert> FUTF8ToTCHAR;
TCHAR_TO_ANSI の使用時は、バイト数が TCHAR 文字列の長さと同じになると仮定しないことが重要です。複数バイトの文字セットは、TCHAR 文字ごとに複数バイトを必要とすることがあります。最終的な文字列の長さをバイトで知りたい場合は、マクロの代わりにヘルパ クラスを使用することができます。例:
FString String;
...
FTCHARToANSI Convert(*String);
Ar->Serialize((ANSICHAR*)Convert.Get(), Convert.Length()); // FTCHARToANSI::Length() は null ターミネータを除いて、エンコードされた文字列のバイト数を返します。
これらのマクロが宣言するオブジェクトのライフタイムは非常に短いです。意図されたユースケースは関数パラメータとしてであり、この状況に適しています。オブジェクトは対象外となり文字列が解放されるため、変換された文字列のコンテンツに変数を割り当てないでください。使用しているコードが解放されたメモリへのポインタにアクセスを続けるとクラッシュの原因となる可能性があります。
Unicode で ToUpper() と ToLower() が難しい問題
UE4 は、現時点で ANSI のみを処理します (ASCII | コードページ 1252 | | 西ヨーロッパ)
全言語において、不本意ながらも他よりはましな方法は こちら を参照してください。
- 英語、フランス語、ドイツ語、イタリア語、ポルトガル語、スペインとメキシコのスペイン語両方はISO/IEC 8859-1 です。
- ポーランド語、チェコ語、ハンガリー語は ISO/IEC 8859-2 です。
- ロシア語は ISO/IEC 8859-5 です。
こちら のマッピングには、上記の言語に対応する変換ルールが含まれています。「大文字」や「小文字」情報は、期待通りの結果を得るために、適切な Unicode 文字をクロスリファレンスします。
東アジア系言語のエンコードに特有な C++ ソースコードに関する注意事項
UTF-8 およびデフォルトの Windows のエンコードでは、C++ コンパイラに以下のような問題が生じる可能性があります。
デフォルトの Windows によるエンコード
CP932 (日本語)、CP936 (簡体字中国語)、CP950 (繁体字中国語) などの東アジア系言語のダブルバイト文字エンコード形式がソースコードに含まれている場合は、シングルバイト文字のコードページ (米国の CP437 など) を使用して動作する Windows 上で C++ によるソースコードをコンパイルする際に注意が必要です。
東アジア系文字のエンコードシステムは、最初のバイトには 0x81 から 0xFE までが使用され、2 番目のバイトには 0x40 から 0xFE までが使用されます。2 番目のバイトの値 0x5C は、ASCII/latin-1 ではバックスラッシュとして処理され、C++ 言語では特別な意味を持ちます。(文字列リテラル内ではエスケープシークエンスの意味。また、行末での使用は、行の継続を意味します)。 そのようなソースコードを、シングルバイトコードページをもつ Windows でコンパイルする場合、コンパイラは、東アジア系言語のダブルバイト文字のエンコードを無視します。その結果、コンパイルエラーが起きるか、最悪の場合は EXE ファイルでバグが発生します。
シングルライン コメント: 東アジア系言語のコメントに 0x5c が入っている場合は、行の欠落が生じるために、発見が難しいバグやエラーが生じる原因となります。
// EastAsianCharacterCommentThatContains0x5cInTheEndOfComment0x5c'\'
important_function(); /* this line would be connected to above line as part of comment */
文字列リテラル内: 0x5c エスケープシーケンスとして認識するために、文字列の破損またはエラーが生じる原因となります。
printf("EastAsianCharacterThatContains0x5c'\'AndIfContains0x5cInTheEndOfString0x5c'\'");
function();
printf("Compiler recognizes left double quotation mark in this line as the end of string literal that continued from first line, and expected this message is C++ code.");
0x5c に続く文字が実際にエスケープシーケンスを指定する場合、コンパイラは、このエスケープ シーケンス文字のセットを指定された単一文字に変換します。 (エスケープ シーケンスの指定がない場合は、動作結果は実装時の定義に依存することになります。ただし、MSVC では、0x5c が取り除かれ、"unrecognized character escape sequence" (エスケープ シーケンスとして正しく認識できません) という警告が表示されます。) 上記の例は、文字列の最後に 0x5c バックスラッシュがあり、次の文字がダブルクオーテーションマークです。そのため、このエスケープシーケンス「"」は、文字列データの中で 1 つのダブルクォーテーションマークに変換され、コンパイラは次のダブルクオーテーションマークが出てくるか、ファイルの終わりに達するまで、文字列データが生成され続け、エラーが発生します。
危険な文字の例: CP932 (日本語 Shift-JIS) の「表」という文字のコードは、0x955C です。CP932 では、多くの文字に 0x5C が入っています。 CP936 (簡体字中国語 GBK) において、「乗」という文字は 0x815C です。CP936 では、多くの文字に 0x5C が入っています。 CP950 (繁体字中国語 Big5) において、「功」という文字は 0xA55C です。CP950 では、多くの文字に 0x5C が入っています。 CP949 (韓国語 EUC-KR) は問題ありません。EUC-KR では、2 番目のバイトに 0x5C が使用されないためです。
BOM が付いていない UTF-8 (一部のテキストエディタは BOM をシグネチャと呼びます)
東アジア系言語を UTF-8 として格納しているソースコードは、Windows CP949 (韓国語)、CP932 (日本語)、CP936 (簡体字中国語)、CP950 (繁体字中国語) 上で C++ ソースコードのコンパイルをする際は注意が必要です。
UTF-8 文字エンコードは東アジア系文字に 3 バイト使用します。0xE0 から 0xEF までが第 1 バイトに、0x80 から 0xBF までが第 2 バイトに、0x80 から 0xBF までが第 3 バイトに割り当てられています。BOM が付いていない場合、東アジア言語系 Windows のデフォルトのエンコードでは、UTF-8 でエンコードされた 3 バイトとその次に続く 1 バイトを、2 バイトの東アジア系エンコード文字が 2 つあるものとして認識してしまいます。具体的には、第 1 バイトと第 2 バイトを合わせて第 1 の東アジア系文字として認識し、第 3 バイトとその後に続く 1 バイト分を 2 つ目の東アジア系文字として認識するのです。 UTF-8 でエンコードされた 3 バイトに続く文字が、文字列リテラルもしくはコメントにおいて特別な意味がある場合に問題が発生する可能性があります。
インライン コメントの例: コメントを構成するテキストに東アジア系文字が奇数個あり、次に続く文字がコメント終了の記号である場合、コードが欠落してしまうため、発見しづらいバグやエラーが生じます。
/*OddNumberOfEastAsianCharacterComment*/
important_function();
/*normal comment*/
東アジア系言語のコードページを使用した Windows 上のコンパイラは、UTF-8 でデコードされた東アジア系文字からなるコメントの最後に置かれた 1 バイトとアスタリスク (*) を、1 つの東アジア系文字として認識し、その次の文字もコメントの一部として扱ってしまいます。上記の例では、コンパイラは important_function() 関数をコメントの一部として除去してしまうのです。 この動作はたいへん危険なものでありながら、同時に、この欠落したコードを発見することは難しいのです。
シングルライン コメント: バックラッシュ '' が東アジア系言語によるコメントの最後に置かれた場合、行が欠落しないため発見が難しいバグやエラーが発生します。
// OddNumberOfEastAsianCharacterComment\
description(); /* coder intended this line as comment, by using backslash at the end of above line */
プログラマは、コメントの最後に意図的なバックラッシュ '' を置く必要がないため、これは大変珍しいケースです。
文字列リテラル内: 文字列リテラル内に奇数個の東アジア系文字があり、次に続く文字が特別な意味をもつ記号である場合は、文字列が破損してエラーや警告が発生します。
printf("OddNumberOfEastAsiaCharacterString");
printf("OddNumberOfEastAsiaCharacterString%d",0);
printf("OddNumberOfEastAsiaCharacterString\n");
東アジア系言語のコードページを使う Windows では、C++ コンパイラが、UTF-8 でデコードされた東アジア系文字からなる文字列の最後に置かれた 1 バイトとその次に置かれた 1 バイトを、1 つの東アジア系文字として認識してしまいます。運良くコンパイラ警告 C4819 (無効にしていない場合) やエラーによって問題に気付くこともあります。そうでない場合は、文字列が破損してしまいます。
結論
UTF-8 またはデフォルトの Windows によるエンコードを C++ ソース コードに使用することができますが、上記の問題について注意する必要があります。繰り返しになりますが、C++ ソース内部で文字列リテラルの使用は推奨しません。C++ ソースコード内部で東アジア系文字のエンコードを使用する場合、デフォルトのコードページに必ず東アジア系のコードページを使用してください。 その他の適切な方法として、BOM 付きの UTF-8 の使用があげられます (一部のテキストエディタは BOM を Unicode シグネチャと呼びます)。
2010 年 2 月 18 日に、UTF-8 および UTF-16 に関していくつかのコンパイラでテストを行いました。
PC および Xbox 360 用の MSVC や、PS3 用の gcc または slc では、UTF-8 でエンコードされたソースコード (BOM ありと BOM なしの両方) をコンパイルすることができました。 しかし UTF-16 (リトルエンディアンとビッグエンディアン) は、MSVC のみがサポートしています。
Perforce は、UTF-16 と UTF-8 の両方で機能しました。ただし p4 diff コマンドは、UTF-8 ファイルに含まれている BOM の文字を可視化してしまいます。