LoginSignup
11
11

More than 1 year has passed since last update.

UnrealEngine マップやキャラクターの裏読み方法 ~アセットロードのすすめ~

Last updated at Posted at 2022-08-22

環境:UE4.27.2

はじめに

裏読みをする前にロードの処理を見直しておきましょう。
C++で組む時、アセットを以下のようにロードしてないですか?

StaticLoadObject(USkeletalMesh::StaticClass(), nullptr, TEXT("/Game/Character/Mesh"));

アセットをロードしてくれるとても便利な機能ですね。
ですが、このままだと色々問題があります。

  • アセットのパスが変わるとコードも書き換えないといけない
  • 参照が付かないためパッケージにアセットが含まれずエラーになる場合がある
  • アセットのロードはとても重く、ヒッチの原因になります

まずはこれらの問題がおきないように対処してみましょう。

パスが変わっても大丈夫、パッケージにも含まれるようにする

絶対に必要なアセットの場合

ハード参照でBPに直接アセットをセットしてしまいましょう。
BPのロードと一緒にロードされるのでStaticLoadObjectする必要がなくなります。

UPROPERTY(EditDefaultsOnly)
USkeletalMesh* CustomMesh;

image.png

場合によって必要なかったり、使い分ける場合

ソフトオブジェクトポインタTSoftObjectPtr<>を使って必要なものだけLoadSynchronous()してやるとよいでしょう。

UPROPERTY(EditDefaultsOnly)
TSoftObjectPtr<USkeletalMesh> CustomMeshA;

UPROPERTY(EditDefaultsOnly)
TSoftObjectPtr<USkeletalMesh> CustomMeshB;
USkeletalMesh* CustomMesh;
if(条件)
{
    CustomMesh = CustomMeshA.LoadSynchronous();
}
else
{
    CustomMesh = CustomMeshB.LoadSynchronous();
}

image.png

アセットの数が沢山あって手動で設定するの面倒なんだけど

UPrimaryDataAssetを継承したクラスを作り、UpdateAssetBundleData関数で登録してあげると良いでしょう。

PrimaryAssetFoobar.h
UCLASS()
class UPrimaryAssetFoobar : public UPrimaryDataAsset
{
	GENERATED_BODY()

public:
	virtual FPrimaryAssetId GetPrimaryAssetId() const override { return FPrimaryAssetId(TEXT("Foobar"), GetFName()); }

#if WITH_EDITORONLY_DATA
	virtual void UpdateAssetBundleData() override;
#endif

public:
    // Keyにそのままアセット名を使っているけど本当はアセットパスの一部を抜き出してId化したほうがいい
	UPROPERTY(VisibleAnywhere)
	TMap<FName, TSoftObjectPtr<USkeletalMesh>> SkeletalMeshes;
};
PrimaryAssetFoobar.cpp
void UPrimaryAssetFoobar::UpdateAssetBundleData()
{
	Super::UpdateAssetBundleData();
	
	if (!UAssetManager::IsValid()) { return; }

	UAssetManager& Manager = UAssetManager::Get();

	TArray<FAssetData> DirectoryAssets;
	FName PackagePath = FName(*FPackageName::GetLongPackagePath(GetOutermost()->GetName()));
	Manager.GetAssetRegistry().GetAssetsByPath(PackagePath, DirectoryAssets, true);
	
	for (const FAssetData& AssetData : DirectoryAssets)
	{
		FSoftObjectPath AssetRef = Manager.GetAssetPathForData(AssetData);
		if (AssetRef.IsNull()) { continue; }

		// スケルタルメッシュを登録
		if (AssetData.GetClass()->IsChildOf(USkeletalMesh::StaticClass()))
		{
			SkeletalMeshes.Emplace(AssetData.AssetName, AssetRef);
		}
	}
}

先程作ったクラスを使ってデータアセットを作ります。
置き場所はロードしたいアセットがあるディレクトリ、またはその上のディレクトリになるように置きます。
image.png
image.png

保存ボタンを押すと自動でアセットをセットしてくれます。
以降はアセットが増えてもプレイするときやこのアセットを開いた時に自動的に追加してくれます。
image.png

後はこのPrimaryAssetをロードして、その中から必要なアセットをLoadSynchronous()してあげればOKです。

アセットロード時にヒッチが発生しないように事前に裏読みする

裏読みとはゲーム中に次に必要なデータを事前にロードしておく処理のことです。
今回はキャラとマップについて説明します。

キャラの場合

AssetManagerの機能を使います。ロードできる単位はUWorldとPrimaryAssetです。
なので、さっき説明したPrimaryAssetを活用することにしましょう!
ただ、今のままだと全キャラロードしてしまうため部分的にロードできるように改造します。
アセットバンドルという機能を使えるように1行追加しましょう。(Unityのそれとは別物ですw)

PrimaryAssetFoobar.cpp
// スケルタルメッシュを登録
if (AssetData.GetClass()->IsChildOf(USkeletalMesh::StaticClass()))
{
	SkeletalMeshes.Emplace(AssetData.AssetName, AssetRef);

    // 追加! バンドルにアセットのパスを追加
	AssetBundleData.AddBundleAsset(AssetData.AssetName, AssetRef);
}

このようにバンドル名を設定してロードするとそのバンドルのアセットだけを非同期でロードしてくれます。

const FPrimaryAssetId AssetId(TEXT("Foobar"), TEXT("Primaryアセット名"));

TArray<FName> LoadBundles;
LoadBundles.Add(TEXT("SK_Mannequin"));

UAssetManager::Get().LoadPrimaryAsset(AssetId, LoadBundles);

おっと、プロジェクト設定に追加するのを忘れてはいけません。
スキャンするプライマリアセットタイプに新しいタイプを追加しましょう。

  • PrimaryAssetTypeはGetPrimaryAssetId()で作った第一引数の文字(例だと"Foobar")を指定
  • Directorysは検索するディレクトリ
  • CookRuleは確実にパッケージに含めてほしいのでAlwaysCookにしておいたほうが安全

image.png
image.png

ロードされてるかどうかは以下のコマンドでチェックできます。
AssetManager.DumpLoadedAssets
image.png

マップの場合

スキャンするプライマリアセットタイプのMapのところを修正する必要があります。

  • Is Editor Onlyのチェックを外す!!
  • Directoriesにマップを置いてあるディレクトリを追加(必要あれば)

image.png

後はロードする処理を書きます。
これだけ!アセット名はフルパスでないと駄目っぽいので注意

UAssetManager::Get().LoadPrimaryAsset(FPrimaryAssetId(TEXT("Map"), TEXT("/Game/Maps/NewWorld")));

ですが、これだとパーシスタントレベルはロードされるのですがサブレベルがロードされません。
サブレベルもロードされるようにAssetManagerを改良していきましょう。

CustomAssetManager.h
UCLASS()
class UCustomAssetManager : public UAssetManager
{
	GENERATED_BODY()

public:
	static UCustomAssetManager& Get();

	void LoadLevel(FName AssetName);
private:
	void LoadSubLevel(TSharedPtr<FStreamableHandle> Handle);
};
CustomAssetManager.cpp
UCustomAssetManager& UCustomAssetManager::Get()
{
	if (UCustomAssetManager* Singleton = Cast<UCustomAssetManager>(GEngine->AssetManager)) { return *Singleton; }
	return *NewObject<UCustomAssetManager>();
}

void UCustomAssetManager::LoadLevel(FName AssetName)
{
	const TSharedPtr<FStreamableHandle>& Handle = LoadPrimaryAsset(FPrimaryAssetId(TEXT("Map"), AssetName));
	if (Handle.IsValid() == false) return;

    // パーシスタントレベルのロードが終わったらサブレベルをロードする
	if (Handle->HasLoadCompleted())
	{
		LoadSubLevel(Handle);
	}
	else
	{
		Handle->BindCompleteDelegate(FStreamableDelegate::CreateUObject(this, &UCustomAssetManager::LoadSubLevel, Handle));
	}
}

void UCustomAssetManager::LoadSubLevel(TSharedPtr<FStreamableHandle> Handle)
{
	const UWorld* PersistentLevel = Cast<const UWorld>(Handle->GetLoadedAsset());
	if (PersistentLevel == nullptr)
	{
		UE_LOG(LogTemp, Warning, TEXT("パーシスタントレベルの裏読みに失敗") );
		return;
	}

	for (const ULevelStreaming* SubLevel : PersistentLevel->GetStreamingLevels())
	{
		LoadPrimaryAsset(FPrimaryAssetId(TEXT("Map"), SubLevel->GetWorldAssetPackageFName()));
	}
}

次にプロジェクト設定で先程作ったクラスを登録します。
image.png

使うときはこう

UCustomAssetManager::Get().LoadLevel(TEXT("/Game/Maps/NewWorld"));

ロードの完了イベントが必要な場合はUCustomAssetManagerを適時改造してやると良いでしょう。

最後に

裏読み中にブロックロードすると非同期の裏読みも含めてブロックロードしてめちゃくちゃ長時間固まります。その場合は

"FlushAsyncLoading:なんとか"

というログが出るのでログが出てる所にブレークを貼り原因をとなっているブロックロードを探します。
そのアセットも裏読み対応するか、個別にAsyncLoadにしてブロックロードが同時に発生しないように頑張りましょう!

(追記)
後で分かったのですがPIEの"Play Standarone"で起動した後、別プロセスのDedicatedServerに接続して
上記のやりかたでレベルを裏読みしてSeamlessTravelするとクラッシュするようです。多分エンジンのバグ。

以上。

参考記事

11
11
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
11
11