UDN
Search public documentation:

ContentStreamingJP
English Translation
中国翻译
한국어

Interested in the Unreal Engine?
Visit the Unreal Technology site.

Looking for jobs and company info?
Check out the Epic games site.

Questions about support via UDN?
Contact the UDN Staff

コンテンツ ストリーミング

ドキュメントの概要:コンテンツのストリーミングおよびシームレスなワールドの計画と実装について説明

概要

次世代のコンソールプラットフォームは、グラフィックと計算パワーの飛躍的な向上を約束します。しかしそれらのプラットフォームでは、大きな記憶容量が求められます。次世代ゲームが最大限の詳細さを達成するために、ストリーミングとシームレス ワールド サポートを使用してゲーム コンテンツをオンデマンドでロードすることが必要になります。

ストリーミング

ここでは、「ストリーミング」を次のような意味で使用します。視覚予測方式に従ってサイズの大きい生コンテンツをオンデマンドで(通常は数メガバイト)最適化して読み込み、同時に、高速データ変換が必要ないように、プラットフォームのネイティブ形式で直接メモリにロードすることです。

ストリーミングシステムは、UObject 派生クラスのような複合オブジェクト指向データをサポートすることを目指していません。レベルの UTexture オブジェクト (テクスチャを記述している200バイトの UObject 派生クラス) は、レベルのロード元であるメモリ内に常駐していますが、そのテクスチャに関連づけられたバルク ミップマップデータをストリーミングできます。言い換えると、ストリーミング サポートは、バルク コンテンツのみを対象としており、複合データ構造を対象にしていません。

ここでは、テクスチャミップマップのストリーミングと、他のストリーミングが必要なコンテンツ用のパッケージ ストリーミングの使用のみを対象としています。オーディオをストリーム予定はありません。

シームレス ワールド

Unreal Engine 3 はシームレス ワールドをサポートしています。シームレス ワールド システムは、レベルに関連づけられた複合オブジェクト指向データをバックグラウンドで動的にロード/アンロードするために使用されます。このシステムをストリーミングと組み合わせることで、ワールドに関連づけられたすべてのデータを動的にロードすることができます。

Unreal Engine 3 のワールド (UWorld オブジェクト) は複数のレベル (ULevel オブジェクト) で構成できます。; 典型的なゲームには、数百もの個別のレベルが含まれています。これらのレベルは、近接性、明示的なロード/アンロード トリガ、その他の基準に基づいて動的にロードおよびアンロードすることができます。

レベルでは、個別にロードおよびアンロードできるアクタの集まりやその他の複合オブジェクト指向データの組み合わせを定義します。概念的に、あるレベルが参照するすべてのオブジェクトがロードされ、C++ とスクリプトコードが参照することができる場合 (すなわち FindObject?) と、まったくできない場合のいずれかに分かれます。この atomicity (アトミシティ) によって、レベルによって参照されるオブジェクトに、その他の任意のオブジェクトへのポインタを含めることができます。これは、アクタ、コンポネント、マテリアル、ゲームプレー スクリプトの複合階層において非常に頻繁に起きるパターンです。このため、C++ とスクリプト コードは、まだローディングを終えていないオブジェクトへの参照を処理する必要は全くありません。

ローディングやその他の原子的ファイル別操作をサポートする ULevel 抽象化もありますが、一般にゲームプレーコードからは隠されています。アクタとゲームプレー機能は UWorld 抽象化を通して表示され、現在ロードされている静的および動的アクタのすべてを 1 つのリストに集積します。これにより、普通のゲームプレーコードは、レベル「境界」を考慮する必要がなく、アクタはワールド内を移動しても「レベル変更」しません。

非同期パッケージ ローディング

シームレスなワールド ローディング コードは、バックグラウンドでパッケージを完全にロードできる機能に基づいています。このコードを使用することで、他のデータの任意の組み合わせを (プレ) ロードし、もう一度ガーベジコレクションに削除させることができます。

詳細な説明

パッケージ ローディング

Unreal のローディング コードのコアは複数のパッケージで構成されており、その依存関係は DLL をモデルに作られています。パッケージには、以下の内容が、この順序で必ず含まれています。

  • パッケージ ファイル サマリ
  • 名前テーブル
  • インポート テーブル
  • エクスポート テーブル

パッケージ ファイル サマリには、一部の基本的情報と、パッケージに保存されるさまざまな表のオフセット/サイズが含まれています。名前テーブルには、シリアライズされた名前がすべて含まれます。エクスポートテーブルには、パッケージにシリアライズされたすべてのオブジェクトが含まれ、インポートテーブルには、パッケージにシリアライズされたオブジェクトの直接依存が含まれます。

エクスポート テーブルの各エクスポートには、ファイルにおけるそのデータの場所を示すオフセット値が含まれます。エクスポートは、パッケージに直線的に作成/ロードできる順番で、パッケージに保存されます。つまり、オブジェクトのクラスまたはアウターが常にオブジェクト自体の前にソートされます。

パッケージを直接処理している C++ クラスは、ULinker サブクラスの ULinkerLoad と ULinkerSave です。ULinkerLoad は、ロード元の UObject をパッケージに連結しています。関連付けられたリンカーを持つことが決してない UObjects が 2、3 種類あります。たとえばイントリンシック クラスとトップレベルのパッケージなどです。それらの場合、GetLinker() は NULL を返します。しかし、オブジェクトがリンカーから切り離されるのは、このときだけではありません。同じファイル名でパッケージを保存すると、リンカーからそのファイルが切り離されます。これにより、コードの保存時にファイルが安全に上書きされます。また、ファイル名の変更時も同じことがあてはまります。ローダーがリセットされるもう 1 つのケースは、パッケージが異なるアウターへロードされているときです。 コンソールでは、プラットフォーム リンカは、メモリで保存するために保持されません。

一般に、関連づけられたリンカーを使用してオブジェクトをロードすると、既にメモリ内にあるオブジェクトが返されます。一方で、関連付けられたリンカーを持たないオブジェクトをロードすると、ディスクからオブジェクトが置換されることになります。実際にはコードはもっと複雑になります。その理由は、パッケージは保存された後に間接参照されるため、パッケージに関連付けられたリンカーがない場合、変更を元に戻すにはエディタがパッケージの手動リロードをサポートする必要があると同時に、パッケージの変更を上書きしない必要があるためです。スクリプトコンパイルも、この複雑さに輪をかけることになります。 コンソールプラットフォームでは、 StaticLoadObject でローディングしている単独のオブジェクトが、DVD からのロードをさえぎることは実用的でないため、ディスクからロードしない場合に備えて、エンジンは常に最初にメモリにあるオブジェクトを見つけようとします。

単一オブジェクトのロードは、パッケージを通したオブジェクトのロードとは、わずかに異なるコードパスを取ります。これは、本書では取り上げていません。次のリストは、エンジンがパッケージ全体をロードしたときに何が起きているかを説明しています。

  • UObject::LoadPackage が BeginLoad を呼び出す
  • UObject::GetPackageLinker が呼び出され、
    • そのパッケージについて既存のリンカーがあるかどうかをチェック
    • 完全に記述されていないファイル名があればそれを解決
    • 「真の」ファイル名を使用して ULinkerLoad オブジェクトを作成
  • 返された ULinkerLoad は、順番に「すべてのオブジェクトをロード」/「すべてのエクスポートを作成」に使用されます
  • EndLoad が呼び出され、
    • ループ内で実際のシリアライゼーションと PostLoad 呼び出しを宛先指定します
    • ダングリングポインタを避けるために、キャッシュされたインポートオブジェクトをインポートテーブルから切り離します。
    • ダングリングポインタを避けるために、エクスポートテーブルから強制エクスポートを切り離します

オブジェクトが作成時に作業上でもう少し手を加えたい場合は:

  • パッケージファイル概要が読み込まれる
  • 名称テーブルが読み込まれる
  • インポートテーブルが読み込まれる
  • エクスポートテーブルが読み込まれる
  • 既存のオブジェクトは潜在的にエディタ内のエクスポートテーブルに組み込まれる。
  • すべてのインポートが確認される (対応するソースリンクで示されたエクスポートと一致)。

すべてのインポートの確認は、リンカオブジェクトの作成をスピードアップする LOAD_NoVerify により据え置きされますが、これはエラー処理が思ったように行われないということです。

非同期パッケージ ローディング

エンジンは、非同期方法でのパッケージ ローディングをサポートします。より正確には、エンジンは渡されたリンカー内のすべてオブジェクトの、非同期方法によるロードをサポートします。この機能は UObject::PreloadLinkerAsync にあり、下記を行います。

  • 適切なリンカー上で ULinkerLoad:CreateExport にマップする ULinkerLoad:CreateImport を経由して、すべてのインポートを作成します
  • すべてのエクスポートオブジェクトを作成して、それらのデータを (1 つ 1 つ) シリアライズします
  • PreLoad (実際のシリアライゼーション) がすべてのエクスポートに経路指定済みであることを確認します
  • PostLoad をオブジェクトに宛先指定します

これらの操作は、時間制限のあるいくつかのフレームにまたがって行われます。次のステップは、1つ前のステップが完了しなければ実行されません。

PostLoad によって新しいオブジェクトが構築/ロードされる可能性があるため、このプロセスはループで実行されます。残りの作業がなくなれば、非同期方法でロード/作成されたすべてのオブジェクトが、RF_AsyncLoading から削除されます。次に、インポートオブジェクトと強制エクスポートは切り離されます (EndLoad 内のように)。そして、完了コールバックが呼び出されます。

作成されたオブジェクトはローディングが完了するまで他のエンジンから隠される必要があるため、非同期ローディングの間、オブジェクトは RF_AsyncLoading としてマークされます。非同期ローディングコードは、既存オブジェクトを同じ場所にリロードするために使用できません。もし、リロードに使用されればエラーメッセージが表示されます。

シークフリーアスペクト

シークフリー ローディングの場合、すべてのインポートがすでにロード済であることが保証されます。そのため、インポート作成段階にディスク動作が発生しません。パッケージをロードするために必要なすべてのオブジェクトは、たとえそれらがパッケージの一部でなくても、エクスポートテーブルに入っています。これは、RF_ForceTagExp と呼ばれるオブジェクトフラグによって達成されます。このフラグは、インポートテーブル内にしか常駐できないオブジェクトを、保存時に、パッケージにシリアライズすることを強制するため、それ以降はオブジェクトがエクスポートテーブルに属することになります。これらのオブジェクトは特別にフラグされます。これによって、エクスポートが作成されるときには、オリジナル パッケージからロードされたかのように作成されます。つまり、ロードされたパッケージではない「アウターモスト (最外)」を持つことになります。

エンジンは、「強制エクスポート」用のエクスポートを作成する前に、オブジェクトがすでにメモリ内に存在しているかどうかをチェックし、それを reconcile (調和) します。これにより、複数のレベルの数個のマップが、一度ロードするだけでよい同じコンテンツを共有できます。

非同期ローディングについて注意すべきなのは、非同期バックグラウンドローディングの実行中は通常のローディングが発生できないことです。このため、非同期ロード要求がある場合、それが完了するまであらゆるバックグラウンド アクティビティが遮断されます。ガーベージコレクションについてもこのことが当てはまります。ガーベージコレクションは非同期ローディングの間は発生できないため、非同期ローディングが完了するまで、その動作をブロックし、完了後に作業を実行します。自動時限式のガーベジコレクションコードは、ロードをブロックする結果が生じるのを避けるため、未処理の要求がある間はガーベジコレクタの呼び出しを回避します。

シークフリー ローディングは、解決時 (インポート作成時として知られる) に、シークが発生することを避けるため、パッケージに既にロードされていることが保証されていない依存関係を複製する方法に依存しています。大多数のシークは、UMaterialExpression などの小さいヘルパーオブジェクトによって引き起こされます。これは、事実上まったくペイロードがなく、複製する負荷が非常に小さいオブジェクトです。一方静的メッシュデータは、サイズの点から見て、通常複製されるデータのうちでもおそらく最も負荷の高いものでしょう。

通常テクスチャは、そのペイロード/バルクデータを複製しませんが、UObject データと最低ミップレベルだけをシークフリー パッケージに入れ、残りをテクスチャ ストリーミング コードを経由してストリーミングさせます。

シークフリー ローディングをほとんど犠牲にすることなく、複製を削減することもできます。これを行うには、共有コンテンツを持つパッケージを作成して、最初にそれらをロードします。この大部分は、手動プロセスで行われます。そのため、このプロセスを余り活用することは現在のところ想定していません。ほとんどのマップで共有されていないデータを複製することで十分足りると考えているためです。 これはほとんどが手動での処理で、マルチプレイヤーマップ用オーディオを除き、 Gears of War 用には作成していません。

ネットワーキング

ネットワーキング コードは、インデックスを経由してオブジェクト参照を伝えるために使用されるパッケージ マップと呼ばれる構造に依存しています。パッケージ マップ コードは、オブジェクト リンカー インデックスとトップレベル パッケージ名を使用して、オブジェクトをインデックスに、またはその逆に変換します。シークフリー ローディング コードを通してロードされたオブジェクトには、関連づけられたリンカーがありません。そのため、コードは元のリンカー インデックスをシークフリー パッケージに保存し、そのパッケージを使用してパッケージ マップをビルドする必要があります。

初期ローディング

初期ローディングには、少し手間がかかります。クッキング ステップの間には次が行われます:

  • 各スクリプト パッケージがロードされる
  • パッケージよって参照されるすべてのコンテンツが、強制エクスポートによって、パッケージに入れられる

コードがこれを行うことにより、スクリプト/コンテンツの混合パッケージの通常エクスポート後に、強制エクスポートがソートされまする。これによって、解決できない相互参照に取り組む必要がなく、単一のメモリブロックから、通常エクスポートのすべてのデータをロードすることができます。

CreateExport は、すべてのパッケージエクスポートで呼び出されます。呼び出されたエクスポートは、クラスを作成してシリアライズしてコンテンツも作成しますが、コンテンツはシリアライズしません。次に、すべてのクラスが作成されると、スクリプト ブロック用のメモリが解放されます。コンテンツはエクスポートを作成し、通常のシークフリー パッケージのように、スライディング ウィンドウを使用してそれをシリアライズさせることができます。これで、データの非同期プリフェッチが可能になります。

上記はすべて、必須コードによってすべての Native クラスがロードされる前に完了されます。その理由は、 Native クラスによって参照されるコンテンツがある場合、それがシークフリー方法で既にロードされており、ディスクアクセスが行われないことを確実にするためです。ここで特に注意しておくべきなのは、すべての Native クラスはエンジン スタートアップ時に必ずロードされなければならないことです。つまり、デフォルトプロパティまたは直接コード参照などを経由したコンテンツ参照もロードされます。 Native クラスはガーベージ コレクションの対象にならないため、 Native クラスによって参照されるコンテンツが常に存在します。これにより、Native でなければ必ずしもロードされる必要がないコンテンツ参照がNative クラス内に存在しないことが非常に重要になります。このケースの良い回避方法は、そのvをサブクラス化し、いろいろなスクリプト パッケージに属するサブクラス内のコンテンツを参照することです。

初期ロードで、native クラスを含むすべてのスクリプトパッケージは、完全にロードされます。 native クラスを含まないパッケージのクラスとマップファイルに入れられるコンテンツ参照されたもののみをスクリプト化します。ワールドの持続レベル内にコンテンツが移動する、または常にロードされたパッケージを持つことにより既存するワールドにすべてのマップがロードされることをコンテンツが確実にした場合は、複製を減少するさまざまな方法があります。

コンソールのクッキング

コンソールのためのデータの cooking に関しては、バルクデータを強制エクスポートから切り離すために次のことを行う必要があります。一つは、通常パッケージに依存しているマップがクックされる前に、通常パッケージをクックすること、そして 2 つめは、エンジンもオフセット、サイズ、その他のさまざまな情報を追跡することです。これにより、シークフリー レベル パッケージにシリアライズされる lazy (遅延) 配列を正しく修正することができます。また、前記のパッケージ変更の後に、パッケージに依存するすべてのマップが再クックされていることが必要です。クッカはこの用途に適合されており、マップ名リストをコマンドライン上で指定できるようになっています。また、クッカはクックを要するものだけを処理するために必要な、すべての作業を行い、すべての依存関係が自動的に処理されることを確実にします。

詳細は コンテンツのクッキング をご覧ください。

その他の注記

ロードフラグの概念には、プロパゲーションに関連した2、3の問題がありますが、この問題はきれいに解決される予定です。主な問題は、パッケージ内にある各オブジェクトをロードするために使用されたロードフラグは、元々リンカーを作成するときに使用されていることです。このリンカーは、そのパッケージへの参照を持つパッケージをシリアライズするときに暗黙に発生したものである可能性があります。

初期ローディングとフレーム毎の作業量は、UObject::Serialize、派生バージョン、そしてさまざまな << 演算子の性能を改善することで削減できます。エンジンは、バルク シリアライゼーションの概念をサポートします。この概念が依存しているのは、シリアライズされる struct の一つ一つのメンバーが、メモリ内で宣言された順にシリアライズされることと、シリアライズされるデータがメモリ内のレイアウトに直接マッピングされることです。つまり、アラインメントが原因である潜在的ギャップもシリアライズされ、バイト順が異なることは問題になりません。これは、あらゆる可能なバイト順換算を実行するためにコードを保存することに依存しています。また、struct メンバーアラインメントがプラットフォームによって異なる可能性にも対応しています。全般的に見て、バルク シリアライゼーションは、非常にもろい構成概念ですが、カラーまたはベクタの配列のようなシンプルなデータ型においては、非常によく機能します。現在、エンジンは 2 カ所でそれを活用しています。理想としては、TTypeInfo の概念を拡張して、簡単なケースについてはネイティブにシリアライズされた配列が自動的にバルク シリアライゼーションを使用できるようにする予定です。

もう一つの効果の高い最適化として、SerializedTaggedProperties を経由して現在シリアライズされている共通データ型の特例処理を行う予定です。たとえば、FMatrix がスクリプト シリアライゼーションを使用する場合などです。'immutablewhencooked' プロパティフラグをさまざまなケースで使用すれば、データ型に合わせたカスタムのシリアライズコードを使わずにこれを自動化することができます。

テクスチャストリーミング

静的オブジェクトには、テクスチャ ストリーミング コードは、UV スケーリング ファクタを計算に入れて、適用されるオブジェクトの境界ボックスのスクリーン スペース サイズをチェックし、テクスチャ データをビデオ メモリにストリーミングしています。

動的オブジェクトは、ビジビリティに基づきストリームされたテクスチャを持ち、テクスチャが浮き出てしまうのを避けるため、オブジェクトのミップマップ レベルでストリーミングを強制する、さまざまなオーバーライドがあります。現在、多数のユニークにテクスチャされたUnreal Tournament 2007のような、ペースの速いゲームに適応するよう、骨格メッシュのテクスチャストリーミングコードを拡張する計画をしております。

オーディオストリーミング

現時点ではオーディオコンテンツをストリームする方法はございません。しかし、より大きなダイアログの多様性を得るよう cooking ソリューションが実装されています。これは使用者の要求に見合うでしょう。