【UE5】Max UObject count is invalidが表示され、パッケージしたアプリが起動しなくなる

:::note info
確認したバージョンは UE5.3.2です。
:::

# はじめに

プロジェクト設定を色々といじった後、アプリをパッケージして.exeを起動すると、Max UObject Count is invalid. It must be a number that is greater than 0.と表示されて.exeが終了してしまう状況に陥ってしまった。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358170/7e37f45c-b263-c32c-7521-219fc61b62fa.png)

原因はちょっと罠っぽいものだったので、備忘録として残しておく。

状況は：
- エディタ上で実行した場合は問題なく動く。
- そのアプリをパッケージしてできた.exeファイルを起動すると、上記のダイアログが表示される。ダイアログ右下の OKボタンを押すと、アプリが終了する。(アプリを正しく起動する手段がない)
- 当時、Garbage Collectionに関する試行錯誤のため、プロジェクト設定をあれこれいじっていた。

# 原因

まずは結論から。

:::note alert
この Fatalエラーは、プロジェクト設定の Engine - Garbage Collectionの Optimizationカテゴリにある「Maximum number of UObjects that can exist in cooked game」(デフォルト値０)を、<b>一度他の数値に変えてから０に戻した場合</b>に発生する。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358170/770bb5c3-577e-04a0-2b42-6319ff13474c.png)
:::

プロジェクト設定の各項目は「現在表示されている値と異なる値を書き込む」ことで、.iniファイルに即座に反映される。
例えば、今回問題となっている「Maximum number of UObjects that can exist in cooked game」を、0 → 1 → 0と書き換えると、.iniファイルの内容は以下のように変化する。

※当該.iniファイルはプロジェクトディレクトリの Confing/DefaultEngine.ini

１）初期状態。当該パラメータを書き換える前 (値は0のまま)
```ini:ProjectDir/Config/DefaultEngine.ini
[/Script/Engine.GarbageCollectionSettings]
```

２）当該パラメータを 0 → 1 に書き換えた後
```ini:ProjectDir/Config/DefaultEngine.ini
[/Script/Engine.GarbageCollectionSettings]
gc.MaxObjectsInGame=1
```

３）当該パラメータを 1 → 0 に戻した後
```ini:ProjectDir/Config/DefaultEngine.ini
[/Script/Engine.GarbageCollectionSettings]
gc.MaxObjectsInGame=0
```

つまり：

- プロジェクト設定でいう「Maximum number of UObjects that can exist in cooked game」は、.iniファイルでは「gc.MaxObjectsInGame」である。
- 何もしなければ DefaultEngine.iniには ```gc.MaxObjectsInGame```に関する項目は無いはずだった。
- いじった本人は「元の値に戻した」つもりだったのに、DefaultEngine.iniには元々無かったはずの ```gc.MaxObjectsInGame=0```が書き込まれてしまった。
- ```gc.MaxObjectsInGame=0```はパッケージしたアプリ専用のパラメータのため、パッケージ版でしか問題が発現しなかった。
- ```gc.MaxObjectsInGame```の値は 0であってはならないため(本来、何らかの大きな数値を書くべきである)、.exe起動時に Fatalエラーダイアログが起動した。

…という現象が起きてしまったのである。

# 解決方法

元の状態に戻してやれば解決する。
つまり、テキストエディタで DefaultEngine.iniから```gc.MaxObjectsInGame=0```の行を削除すればよい。
※パッケージを作り直す必要はある。

# 深掘り

ここから先は、この問題について深掘りした内容を書いていく。

## gc.MaxObjectsInGameのデフォルト値はいくつか？

プロジェクト設定「Maximum number of UObjects that can exist in cooked game (＝gc.MaxObjectsInGame)」のデフォルト値がゼロであることから、「パッケージしたアプリではメモリが許す限り無限」だと連想してしまう。
だが、```gc.MaxObjectsInGame```の実際の初期値は 2 * 1024 * 1024 (＝2,097,152)である。
これはエンジンソースの InstallDir\Engine\Source\Runtime\CoreUObject\Private\UObject\UObjectBase.cpp の984行目、```UObjectBaseInit()```関数で指定されている。
エンジンソースをコピペするのは憚られるので、ダミーコードで流れを示す。

```c++:UObjectEngine.cpp (986)
void UObjectBaseInit()
{
	int32 MaxUObjects = 2 * 1024 * 1024; // Default to ~2M UObjects

	if (/*クックされたデータ*/)
	{
		// Maximum number of UObjects in cooked game
		GConfig->GetInt(TEXT("/Script/Engine.GarbageCollectionSettings"), TEXT("gc.MaxObjectsInGame"), MaxUObjects, GEngineIni);
	}
	else
	{
		// Maximum number of UObjects in the editor
		GConfig->GetInt(TEXT("/Script/Engine.GarbageCollectionSettings"), TEXT("gc.MaxObjectsInEditor"), MaxUObjects, GEngineIni);
	}

	GUObjectArray.AllocateObjectPool(MaxUObjects);
}
```

1. まずローカル変数```MaxUObjects```が 2 * 1024 * 1024 (＝2,097,152)で初期化される。
1. クックされたデータが要求されている場合、つまりパッケージ版のアプリの場合は、if の中に入る。その場合 .iniファイルから```gc.MaxObjectsInGame```の値が取得される。
    - ```GConfing->GetInt()```は .iniファイルにそのパラメータの記述がない場合は何もしない。初期状態では、DefaultEngine.iniをはじめとするどのEngine.iniファイルにも```gc.MaxObjectsInGame```の記述はない。したがって、ローカル変数```MaxUObjects```は 2 * 1024 * 1024のままである。
    - もし、DefaultEngine.iniなどのどれかのEngine.iniファイルに```gc.MaxObjectsInGame```の記述があれば、```GConfing->GetInt()```はその値を取得する。今回私がハマったように、手動でゼロを書き込んでしまっていた場合、ローカル変数```MaxUObjects```はゼロになり、そのあと呼び出される```GUObjectArray.AllocateObjectPool()```から、冒頭に載せたスクショのような Fatalエラーのダイアログが表示され、アプリは終了する。

:::note info
なお、エディタから起動した場合は、elseの中に入る。
この場合、```gc.MaxObjectsInEditor```の値がローカル変数```MaxUObjects```に入る。
ちなみに、```gc.MaxObjectsInEditor```は InstallDir\Engine\Config\BaseEngine.iniにデフォルト値が定義されており、その値は 25,165,824である。
```ini:EngineInstallDir/Config/BaseEngine.ini
[/Script/Engine.GarbageCollectionSettings]
gc.MaxObjectsInEditor=25165824
```
この値は、プロジェクト設定の Engine - Garbage Collectionの Optimizationカテゴリにある「Maximum number of UObjects that can exist in editor」に相当する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358170/45c3762c-cead-f58b-f2fd-c58f831ade48.png)
:::

## gc.MaxObjectsInGameもしくはgc.MaxObjectsInEditorの値を超えるとどうなるか

これを実験するのは比較的たやすい。UObjctを大量に生成(NewObject)するだけで発現させられる。
この場合、次のようなダイアログが表示され、OKボタンを押すとアプリが終了する。
![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/358170/7d40adeb-fd40-41dc-8338-879a4026299e.png)

つまり、```gc.MaxObjectsInGame```や```gc.MaxObjectsInEditor```の値を超えるとアプリは強制終了することになる。

:::note info
上記のダイアログのスクショはパッケージ版で試したものである。
ここに書かれているオブジェクトの最大数は 2,162,688個であり、上述の```MaxUObjects = 2 * 1024 * 1024```(＝2,097,152)とは異なる。
オブジェクトはチャンク(１チャンク辺り 65,536個)で管理されており、実際のオブジェクト上限数は```gc.MaxObjectsInGame```や```gc.MaxObjectsInEditor```より大きくて最も近い数で管理されている。
InstallDir\Engine\Source\Runtime\CoreUObject\Public\UObject\UObjectArray.hの464行目、```PreAllocate()```関数内で以下のように算出されている。
```c++:UObjectArray.h(461)
void PreAllocate(int32 InMaxElements)
{
	// NumElementsPerChunkは 64 * 1024 (=65,536)
	MaxChunks = InMaxElements / NumElementsPerChunk + 1;
	MaxElements = MaxChunks * NumElementsPerChunk;
	Objects = new FUObjectItem*[MaxChunks];
	// 以下略...
}
```
要するに65,536の倍数に揃えられるということである。

※この計算に基づくと、エディタ版は ```gc.MaxObjectsInEditor```のデフォルト値 25,165,824から、実際の最大オブジェクト数は 25,231,360になる。(65,536の385倍)
:::

## では gc.MaxObjectsInGameはいくつに設定したらよいのか (※結論なし)

```gc.MaxObjectsInEditor```は、BaseEngine.iniに初期値 25,165,824が与えられている。
一方、```gc.MaxObjectsInGame```は、どの.iniファイルにも初期値が与えられておらず、UObjectBase.cppの```UObjectBaseInit()```内に設定されたローカル変数の初期値 (2 * 1024 * 1024)が流用されているのみである。ここで、エディタ版とパッケージ版のオブジェクト上限数を並べてみると… 
||上限数|
|:---|---:|
|パッケージ版|2,097,152|
|エディタ版|25,165,824|

縦に並べてみると、なんと１桁も違うことが分かる。
そうすると、果たしてパッケージ版(```gc.MaxObjectsInGame```)を現状のまま、つまり値を設定せずに放置しておいてよいものか不安になってしまう。これでは、エディタでは十分動いていたものが、パッケージ版では動かないという現象が起きてしまうかもしれない。
とは言え、エディタ版の25,165,824をそのまま、パッケージ版にも流用(コピペ)するのも気が引ける。なぜなら、エディタ版ではエディットの都合上、非常に多くのオブジェクトを使うことができるようにする必要があるが、パッケージ版では「必要最低限」にしておきたいからだ。

問題提起だけしておいて結論を出せないで申し訳ないが、ここで言えることは以下のような感じだろうか…
- 2 * 1024 * 1024 (=2,097,152)のままでも十分パッケージ版が動く場合は、まあ触らぬ神に祟りなしでスルーしても良いだろう。
- パッケージ版で、オブジェクト数の上限を超えてしまう場合は：
    - 安易にエディタ版の数値(25,165,824)をコピペして使うのだけはやめておきたい。その値はあくまでエディタでエディットするためのものである。
    - プラットフォームのメモリ量に応じており、かつそのアプリに必要十分な値を、試行錯誤して見つけて設定する必要があるだろう。そのためには、様々なケースを想定したテストプレイで、実際のオブジェクト数の遷移をプロファイリングする必要があるだろう。