[UE4] Shippingのデバッグ

#1. はじめに
![2018-12-15_03h02_05.png](https://qiita-image-store.s3.amazonaws.com/0/101019/c3c24952-460f-7c6b-70e2-da1adae2ee4d.png)
UE4でゲームを開発している方はこのようなダイヤログを1度は目にしたことがあるのではないでしょうか。
これはShippingで動作する(出荷用にパッケージされた)ゲームなどで見られるクラッシュのケースの１つです。Shippingにはデバッグ情報が含まれないため、クラッシュの要因が追跡し辛いということがしばしばあります。本記事では、この要因を追跡するための手がかりとなる情報をここでいくつか記載しています。

我々がこのような(絶望的な)画面を見つけてしまったとしても、取り得る手段はいくつも用意されているため、安心して自分自身でデバッグ作業を開始することができます。「Fatal Error」という情報だけでは原因や問題点を特定することが困難なため、その問題が発生した環境で、できるだけ多くの情報を取得してから原因を推測・追求し、確実にその問題に対して修正を行う必要があるということです。

本記事は4.21.1を使用しています。

#2. Dumpファイルからの解析
クラッシュが発生した場合、**Dumpファイル**(OSやアプリケーションソフトが異常終了した際、原因特定の手がかりになるように最後の瞬間のメモリやレジスタの内容を記録したファイル)を以下の場所に出力します。

**【Dump fileの場所】**
C:\Users\\[USER_NAME]\AppData\Local\\[PROJECT_NAME]\Saved\Crashes
(※上記はWindowsの場合ですがPlatformによって出力先は異なります)
![2018-12-15_03h04_52.png](https://qiita-image-store.s3.amazonaws.com/0/101019/aeb970b2-e01c-598e-460f-f85acf78aa56.png)
このDumpファイルから**「どこでクラッシュしたか」**を知ることができます。
手順は以下の2つの手順を踏むだけです。

① .dmpファイルをVisualStudioに読み込む (ドラッグ＆ドロップ または ダブルクリックで開く)
この時点で**例外情報**から"アクセス違反"が起きているということが推測できます。
![2018-12-15_03h50_33.png](https://qiita-image-store.s3.amazonaws.com/0/101019/0b730670-eac2-157f-b0fc-389911119817.png)

② 「ネイティブのみでデバッグ」を選択し.pdbファイル(シンボルファイル)を選択
シンボルファイルを正しく読み込めた場合、以下のようにコールスタックと共に例外が発生した箇所でブレークします。以下の例の場合、***Temp[0] = 0;***でブレークしており、明らかにアクセス違反であることが分かります。
![2018-12-15_03h55_06.png](https://qiita-image-store.s3.amazonaws.com/0/101019/ff4be42f-9295-9d63-45ff-6784951c5272.png)

この2つの手順によって**どのような経路で**、**どこで**問題が発生したのかを確認することができ、対策や回避策を検討することができます。まずもって確認すべきはこのフェーズなのですが、次に紹介するCrash Reporterを利用することで、プロジェクトのコード一式などが無いようなケースにおいても問題をその場で確認することができます。

#3. Crash Reporterでの通知
2章で説明したDumpファイルを使用しない場合のケースについて記載します。何も設定せずにパッケージを作成すると上述のFatal Errorのポップアップが表示されますが、ここではCrash Reporterを表示することでエラーを通知する方法を記載しています。Crash ReporterはDumpファイルを解析しなくともクラッシュが発生した旨を通知できたり、デバッグ情報を確認することができるため便利です。
これらはデバッグ情報を含むため、アプリケーションの総サイズが大きくなることから、実際の出荷時にはOFFにしておく必要があります。

##3.1. Crash Reporterの表示
Project Settingsの**"Include Crash Reporter"**を有効にしておくと、以下のようにクラッシュ時にクラッシュレポーターで通知することができます。
![2018-12-15_04h23_42.png](https://qiita-image-store.s3.amazonaws.com/0/101019/664e3fa6-187a-11e8-b28e-2f3d47cf2f97.png)
![2018-12-15_04h19_14.png](https://qiita-image-store.s3.amazonaws.com/0/101019/ea371377-b34e-4312-d989-2310b6ffde98.png)

##3.2. Debug情報の表示
Project Settingsの**"Include Debug Files"**を有効にしておくと、以下のようにクラッシュ時にさらにコールスタック情報を出力することができます。
![2018-12-15_04h23_51.png](https://qiita-image-store.s3.amazonaws.com/0/101019/27d7ae6d-bd44-243f-41f2-6e8ae22039e0.png)
![2018-12-15_04h22_12.png](https://qiita-image-store.s3.amazonaws.com/0/101019/833e4824-577f-dd05-86a6-410027b573ea.png)

ここで記録されるデバッグ情報は、クラッシュレポーターを使用しない場合においてもDumpファイルと同フォルダにある**CrashContext.runtime-xml**ファイルに記録されます。

#4. Shippingで様々なデバッグ機能を使用する
仮にShippingで発生するような問題に遭遇した場合、以下のようなフラグなどを有効化することでより多くの情報を得ることができます。これらは本来Developmentで使用できるものなので、Developmentでその問題が再現できるのが理想的ですが、必要に応じてShippingにおいてもデバッグ情報を得ることができます。
**(本来はShippingに含まないデバッグ情報のため戻し忘れに注意する必要があります。)**

## 4.1. Logging
Shippingではデフォルトでログを出力しないため、以下のフラグを有効にすることでログ出力をできるようにします。
ログはC:\Users\[USER_NAME]\AppData\Local\[PROJECT_NAME]\Saved\Logsに出力されます。**Engineをフルビルドするため切替時にはビルド時間を要します。**

```cs:\Engine\Source\Programs\UnrealBuildTool\Configuration\TargetRules.cs
        /// <summary>
        /// Whether to turn on logging for test/shipping builds.
        /// </summary>
		[RequiresUniqueBuildEnvironment]
        public bool bUseLoggingInShipping = true;
```


以下のように書くEngineのビルドをせずにプロジェクト側で定義できます。

```cs:[Project].Target.cs
        if (Configuration == UnrealTargetConfiguration.Shipping)
        {
            bUseLoggingInShipping = true;
        }
```

## 4.2. Check
Shippingではcheck/verify/checkCodeなどのAssertaion Macroをトリガしませんが、以下のフラグを有効にすることで利用できます。**Engineをフルビルドするため切替時にはビルド時間を要します。**

```cs:\Engine\Source\Programs\UnrealBuildTool\Configuration\TargetRules.cs
		/// <summary>
		/// Whether to turn on checks (asserts) for test/shipping builds.
		/// </summary>
		[RequiresUniqueBuildEnvironment]
        public bool bUseChecksInShipping = true;
```

以下のように書くEngineのビルドをせずにプロジェクト側で定義できます。

```cs:[Project].Target.cs
        if (Configuration == UnrealTargetConfiguration.Shipping)
        {
            bUseChecksInShipping= true;
        }
```


## 4.3. PrintString
ShippingではPrint Stirngによる出力を行いませんが、以下のフラグを有効にすることで利用できます。

```cpp:\Engine\Source\Runtime\Engine\Private\KismetSystemLibrary.cpp
void UKismetSystemLibrary::PrintString(UObject* WorldContextObject, const FString& InString, bool bPrintToScreen, bool bPrintToLog, FLinearColor TextColor, float Duration)
{
//#if !(UE_BUILD_SHIPPING || UE_BUILD_TEST) // Do not Print in Shipping or Test
	...
//#endif
}
```

## 4.4. Console Command
ShippingではConsole Commandを使用することができませんが、以下のフラグを有効にすることでコマンドを入力することができるようになります。ただし、各コマンド単位でShippingの許可/非許可が定義されているため(#if !UE_BUILD_SHIPPINGによるガードが実装されているため)、一般的に全てのコマンドを利用可能にすることは多くの作業を要します。"stat unit"コマンドや"stat levels"コマンドなど一部のコマンドは利用することが出来るようになります。**Engineをフルビルドするため切替時にはビルド時間を要します。**

```cpp:\Engine\Source\Runtime\Core\Public\Misc\Build.h
#ifndef ALLOW_CONSOLE_IN_SHIPPING
	#define ALLOW_CONSOLE_IN_SHIPPING 1
#endif
```

以下のように書くEngineのビルドをせずにプロジェクト側で定義できます。

```cs:[Project].Target.cs
        if (Configuration == UnrealTargetConfiguration.Shipping)
        {
                GlobalDefinitions.Add("ALLOW_CONSOLE_IN_SHIPPING=1");
        }
```


以下、利用頻度が(個人的に)高いコマンドで実行した結果を並べたものになります。

**[利用可]**
stat unit, stat levels, stat fps などの一部のstatコマンド
dumpticks, dumplevelcollections などの一部のdumpコマンド
open, travel, log, r.xxx, a.xxx, t.xxx, gc.xxx など

**[利用不可]**
stat unitgprah, stat collision, stat startfile/stopfile などの一部のstatコマンド
showflag, obj, memreport などの一連のコマンド
shot/hiresshot,  toggledrawevents, profilegpu など

<!-- Showflag系の一部コマンドは\Engine\Source\Runtime\Engine\Public\ShowFlagsValues.inlを参照 -->

## 4.5. Stats
ShippingではStatsコマンドを使用することができませんが、以下のフラグを有効にすることでコマンドを入力することができるようになります。ただし、各コマンド単位でShippingの許可/非許可が定義されているため(#if !UE_BUILD_SHIPPINGによるガードが実装されているため)、一般的に全てのコマンドを利用可能にすることは多くの作業を要します。**Engineをフルビルドするため切替時にはビルド時間を要します。**

```cpp:\Engine\Source\Runtime\Core\Public\Misc\Build.h
/** Compile flag to force stats to be compiled */
#ifndef FORCE_USE_STATS
	#define FORCE_USE_STATS 1
#endif
```

以下のように書くEngineのビルドをせずにプロジェクト側で定義できます。

```cs:[Project].Target.cs
        if (Configuration == UnrealTargetConfiguration.Shipping)
        {
                GlobalDefinitions.Add("FORCE_USE_STATS=1");
        }
```

関連するShippingマクロ(UE_BUILD_SHIPPING)をコメントアウトすることで、Network Profilerなども利用することも出来るようになります。しかしながら、これらはあくまでデバッグ用の用途であるためShippingでは利用できない前提となっています。

#5. まとめ
 Shippingのみで発生する問題というのは非常に極々稀なケースだと思うので、もしShippingで問題が発生したとしてもその問題はおそらくDevelopmentでも再現が可能であるはずです。まずはDumpファイルなどから問題を分析し、Developmentでも発生することが確認できれば、その問題への対応はより容易なものになります。そのためにも、テストは十分な期間で実施し、かつ定期的に実施することということが重要です。