Unreal Engine 4での非同期(そうではない)データの読み込み



内容:



みなさんこんにちは!

今日は、 Unreal Engine 4でアセットを処理する方法について説明します。これにより、ゲームのロード中に、目的のないメモリとうめき声でプレイヤーを痛烈に傷つけないようにします。

エンジンの明白でない機能の1つは、リンクシステムを介して影響を受けるすべてのオブジェクトについて、いわゆるクラスデフォルトオブジェクト(CDO)がメモリに格納されることです。 さらに、オブジェクトを完全に機能させるために、オブジェクトに記載されているすべてのリソース(メッシュ、テクスチャ、シェーダーなど)がメモリにロードされます。

その結果、このようなシステムでは、メモリ内のゲームオブジェクトの接続ツリーがどのように「拡張」されるかを厳密に監視する必要があります。 カテゴリから最も単純な条件を導入する場合、例を挙げるのは簡単です。プレーヤーが現在リンゴを制御している場合、「今すぐリンゴを追加購入!」ボタンが表示されます。ユーザーが梨のキャラクターだけをプレイしても、 。

なんで? スキームは非常に簡単です。

  1. HUDはプレーヤーがどのクラスであるかをチェックし、 Appleクラス(およびAppleで言及されているすべてのもの)をメモリにロードします。
  2. チェックが成功した場合、 Buy-Applesウィジェットが作成されます(直接言及されている->すぐにロードされます)。
  3. 押してリンゴ購入すると、 プレミアムストアウィンドウが開きます。
  4. プレミアムストアは 、特定の条件に応じて、146の服のアイコンと、クラスごとに異なる種と果物の樽の20モデルを使用する「キャラクター服」画面を表示できます。

ツリーは引き続きすべての葉まで拡張され、このように、完全に無害なチェックおよび他のクラスへの参照(キャストレベルでも)が表示されます-プレイヤーがこれに必要としないオブジェクトのグループ全体をメモリに保持しますゲームプレイの瞬間。



開発中のある時点で、これはゲームにとって重要になりますが、すぐには重要ではありません(最近のモバイルデバイスでもメモリのしきい値は非常に高くなっています)。 さらに、この種の設計エラーは修正が非常に困難で不快です。

私は常に自分自身で使用し、そのような状況を解決する例として役立つことができ、プロジェクトのニーズに合わせて簡単に拡張できる実用的なソリューションを提供したいと思います。

ステップ1.特別なアセットポインターを使用する


依存関係ツリー全体をメモリにロードするという悪質な慣行を中断するために、Epic Gamesの紳士は、2つのトリッキーなタイプのアセット参照、 TAssetPtrおよびTAssetSubclassOfを使用する機能を提供しました (唯一の違いは、 クラスアセットがTAssetSubclassOf <class A>に入ることができないことですA、それからの子のみ、これはクラスAが抽象の場合に便利です)。

これらのタイプを使用することの特徴は、リソースをメモリに自動的にロードせず、それらへの参照のみを保存することです。 したがって、リソースはアセンブルされたプロジェクトに分類されます(たとえば、アセットへのテキストリンクの配列の形式で文字ライブラリを保存する場合は発生しませんでした)が、メモリへの読み込みは開発者がそうする場合にのみ発生します。

ステップ2.リソースをオンデマンドでメモリにロードする


これを行うには、 FStreamableManagerのようなものが必要です 。 これについては、例の一部として以下で詳細に説明しますが、現時点では、アセットの読み込みは非同期または同期のいずれかであり、アセットへの「通常の」リンクを完全に置き換えることができます。


この記事の主な目標は、「誰が責任を負うべきか?」(資産への直接リンク)および「何をすべきか?」( TAssetPtrを介してダウンロードする)の質問に実用的な答えを与えることです。 engine 、および実際のそのようなアプローチの実装例を示します。

例1.キャラクターの選択


多くのゲームでは、DOTA 2であろうとWorld of Tanksであろうと、戦闘外のキャラクターを見る機会があります。 カルーセルをクリックすると、画面に新しいモデルが表示されます。 使用可能なすべてのモデルへの直接リンクがある場合、既にわかっているように、それらのすべてが読み込み段階でメモリに格納されます。 想像してみてください-すべての120のDotaキャラクターとすぐにメモリに! :)

データ構造


キャラクターをロードしやすくするために、キャラクターの身元によって彼のアセットへのリンクを取得できるプレートを作成します。

/** * Example #1. Table for dynamic actor creation (not defined in advance) */ USTRUCT(Blueprintable) struct FMyActorTableRow : public FTableRowBase { GENERATED_USTRUCT_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) FString AssetId; UPROPERTY(EditAnywhere, BlueprintReadWrite) TAssetSubclassOf<AActor> ActorClass; FMyActorTableRow() : AssetId(TEXT("")), ActorClass(nullptr) { } }; 

FTableRowBaseクラスをデータ構造の親として使用したことに注意してください。 このアプローチにより、設計図で直接簡単に編集できるテーブルを作成できます。



注:特定の行名がある場合、なぜAssetIdを尋ねるかもしれません。 ゲーム内のエンティティをエンドツーエンドで識別するために追加のキーを使用します。この命名規則は、エンジンの作成者によって行名に課される制限とは異なりますが、これは必須ではありません。

アセットをダウンロードする


鈍感でテーブルを操作する機能は豊富ではありませんが、それで十分です:



キャラクターのアセットへのリンクを受け取った後、 スポーンアクター(非同期)ノードが使用されます。 これはカスタムノードであり、次のコードが記述されています。

 void UMyAssetLibrary::AsyncSpawnActor(UObject* WorldContextObject, TAssetSubclassOf<AActor> AssetPtr, FTransform SpawnTransform, const FMyAsyncSpawnActorDelegate& Callback) { //      FStreamableManager& AssetLoader = UMyGameSingleton::Get().AssetLoader; FStringAssetReference Reference = AssetPtr.ToStringReference(); AssetLoader.RequestAsyncLoad(Reference, FStreamableDelegate::CreateStatic(&UMyAssetLibrary::OnAsyncSpawnActorComplete, WorldContextObject, Reference, SpawnTransform, Callback)); } void UMyAssetLibrary::OnAsyncSpawnActorComplete(UObject* WorldContextObject, FStringAssetReference Reference, FTransform SpawnTransform, FMyAsyncSpawnActorDelegate Callback) { AActor* SpawnedActor = nullptr; //      ,     UClass* ActorClass = Cast<UClass>(StaticLoadObject(UClass::StaticClass(), nullptr, *(Reference.ToString()))); if (ActorClass != nullptr) { //     SpawnedActor = WorldContextObject->GetWorld()->SpawnActor<AActor>(ActorClass, SpawnTransform); } else { UE_LOG(LogMyAssetLibrary, Warning, TEXT("UMyAssetLibrary::OnAsyncSpawnActorComplete -- Failed to load object: $"), *Reference.ToString()); } //       Callback.ExecuteIfBound(SpawnedActor != nullptr, Reference, SpawnedActor); } 

ダウンロードプロセスの主な魔法は次のとおりです。

  FStreamableManager& AssetLoader = UMyGameSingleton::Get().AssetLoader; FStringAssetReference Reference = AssetPtr.ToStringReference(); AssetLoader.RequestAsyncLoad(Reference, FStreamableDelegate::CreateStatic(&UMyAssetLibrary::OnAsyncSpawnActorComplete, WorldContextObject, Reference, SpawnTransform, Callback)); 

FStreamableManagerを使用して、 TAssetPtrを介して転送されたアセットをメモリにロードします。 アセットをロードした後、 UMyAssetLibrary :: OnAsyncSpawnActorComplete関数が呼び出されます。 この関数では、既にクラスのインスタンスを作成しようとします。すべてが問題なければ、ectorをワールドにスポーンしようとします。

操作の非同期実行には、completion_ = B8の通知が含まれるため、最後に鈍いイベントをトリガーします。

Callback.ExecuteIfBound(SpawnedActor != nullptr, Reference, SpawnedActor);

ブループリント管理は次のようになります。





実際には、それだけです。 このアプローチを使用すると、ゲームのメモリを最小限にロードして、非同期的にセクターを生成できます。

例2.インターフェース画面


NeedMoreAppleボタンの例を思い出してください。また、プレーヤーが現時点では見ないメモリに他の画面の読み込みをどのように引き出したのでしょうか。

これを常に100%回避できるとは限りませんが、インターフェイスのウィンドウ間の最も重要な関係は、イベントによるオープン(作成)です。 私たちの場合、ボタンは生成されたウィンドウについて何も知りません。さらに、クリックされたときにどのウィンドウ自体をユーザーに表示する必要があるかはわかりません。

以前に得た知識を使用して、インターフェース画面の表を作成します。

 USTRUCT(Blueprintable) struct FMyWidgetTableRow : public FTableRowBase { GENERATED_USTRUCT_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) TAssetSubclassOf<UUserWidget> WidgetClass; FMyWidgetTableRow() : WidgetClass(nullptr) { } }; 

次のようになります。



インターフェースの作成はスポーンセクターとは異なるため、非同期にロードされたアセットからウィジェットを作成するための追加関数を作成しましょう。

 UUserWidget* UMyAssetLibrary::SyncCreateWidget(UObject* WorldContextObject, TAssetSubclassOf<UUserWidget> Asset, APlayerController* OwningPlayer) { // Check we're trying to load not null asset if (Asset.IsNull()) { FString InstigatorName = (WorldContextObject != nullptr) ? WorldContextObject->GetFullName() : TEXT("Unknown"); UE_LOG(LogMyAssetLibrary, Warning, TEXT("UMyAssetLibrary::SyncCreateWidget -- Asset ptr is null for: %s"), *InstigatorName); return nullptr; } // Load asset into memory first (sync) FStreamableManager& AssetLoader = UMyGameSingleton::Get().AssetLoader; FStringAssetReference Reference = Asset.ToStringReference(); AssetLoader.SynchronousLoad(Reference); // Now load object and check that it has desired class UClass* WidgetType = Cast<UClass>(StaticLoadObject(UClass::StaticClass(), NULL, *(Reference.ToString()))); if (WidgetType == nullptr) { return nullptr; } // Create widget from loaded object UUserWidget* UserWidget = nullptr; if (OwningPlayer == nullptr) { UWorld* World = GEngine->GetWorldFromContextObject(WorldContextObject); UserWidget = CreateWidget<UUserWidget>(World, WidgetType); } else { UserWidget = CreateWidget<UUserWidget>(OwningPlayer, WidgetType); } // Be sure that it won't be killed by GC on this frame if (UserWidget) { UserWidget->SetFlags(RF_StrongRefOnFrame); } return UserWidget; } 

注意する価値のあるものがいくつかあります。

1つ目は、参照によって渡されたアセットの検証チェックを追加したことです。

  // Check we're trying to load not null asset if (Asset.IsNull()) { FString InstigatorName = (WorldContextObject != nullptr) ? WorldContextObject->GetFullName() : TEXT("Unknown"); UE_LOG(LogMyAssetLibrary, Warning, TEXT("UMyAssetLibrary::SyncCreateWidget -- Asset ptr is null for: %s"), *InstigatorName); return nullptr; } 

すべてがゲーム開発者のハードワークに含まれている可能性があるため、そのようなケースを予測することは不要ではありません。

第二に 、ウィジェットは世界に出現せず、 CreateWidget関数を使用します

UserWidget = CreateWidget<UUserWidget>(OwningPlayer, WidgetType);

第三に 、セクターの場合、それが世界で生まれ、その一部になった場合、ウィジェットは通常の中断された「裸の」ポインターのままであり、Anrilovのガベージコレクターは喜んでそれを探します。 彼にチャンスを与えるために、現在のフレームでGCによる貪食からの保護を有効にします。

UserWidget->SetFlags(RF_StrongRefOnFrame);

したがって、だれも自分でバトンを使用しない場合(ウィンドウはユーザーに表示されず、作成されたばかりです)、ガベージコレクターはそれを削除します。

4番目に、デザートの場合、1ティック内でウィジェットを同期的にロードします。

AssetLoader.SynchronousLoad(Reference);

実践が示すように、これは携帯電話でも優れており、同期機能の処理が簡単です。追加の読み込みイベントを開始して何らかの方法で処理する必要はありません。 もちろん、このプラクティスでは、ウィジェットのコンストラクトですべての長い操作を行う必要はありません。必要に応じて、最初にプレーヤーに表示し、100,500のすべてのプレーヤーアイテムとキャラクターモデルが画面に読み込まれるまで「ダウンロード」を記述します。

例3.コードなしのデータテーブル


TAssetPtrを使用して多くのデータ構造を作成する必要があるが、各コードでクラスを作成してFTableRowBaseから継承したくない場合はどうでしょうか。 ブループリントにはこのタイプのデータがないため、コードなしでは実行できませんが、特定のタイプのアセットへのリンクを持つプロキシクラスを作成できます。 たとえば、テクスチャアトラスの場合、次の構造を使用します。

 USTRUCT(Blueprintable) struct FMyMaterialInstanceAsset { GENERATED_USTRUCT_BODY() UPROPERTY(EditAnywhere, BlueprintReadWrite) TAssetPtr<UMaterialInstanceConstant> MaterialInstance; FMyMaterialInstanceAsset() : MaterialInstance(nullptr) { } }; 

これで、 blueprintsFMyMaterialInstanceAssetタイプを使用でき、それに基づいて、テーブルで使用される独自のカスタムデータ構造を作成できます。



他のすべての点で、このタイプのデータを使用した作業は上記と変わりません。

おわりに


TAssetPtrを介してアセットリンクを使用すると、ゲームのメモリ使用量を冷却し、読み込み時間を短縮できます。 このアプローチを使用した最も実用的な例を挙げようとしましたが、それらがあなたに役立つことを願っています。

すべての例の完全なソースコードは、 ここから入手できます

コメントや質問は大歓迎です。

Source: https://habr.com/ru/post/J309228/


All Articles