Unityのマルチプラットフォームビルドスクリプトではまりがちなこと（特にiOS）

# はじめに

この記事は以下のような問題をかかえる人向けです

* スクリプトを利用したマルチプラットフォームビルドに手を染めている
* プラットフォームの切り替えとビルドの開始をEditorスクリプトで行おうとしている。
* スクリプトからプラットフォームを切り替えたときに、Unityでのビルド自体は成功しているのに、おかしなビルドができたり、ビルドが失敗したりするケースがある

スクリプトから `defineシンボル` を変更したとき についても同様の問題があるので参考になると思います。

# 環境

Unity2019.4.29f1 でためしています。もしかすると、後のバージョンでは違いがあるかもしれません。

# 結論

忙しい人向けに最初に結論を書きます。

* とあるプラットフォーム向けに正常にビルドするためには、そのプラットフォームに先にスイッチし、しかも、Editorスクリプトを再コンパイルした後でビルドしなければなりません。これができていない場合、異常なビルドになります。イメージとしては以下のような感じです。

<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71421/ad22200d-67de-67c1-6543-945ee25c9059.png" width="550px"><br>

* これを行うためには **プラットフォームスイッチとビルドの２つのステップに分ける** 必要があります。
* プラットフォームスイッチ後、ビルドする前に再コンパイルの完了を待つ必要があります。
  * **方法A**
    * プラットフォームスイッチ後、ビルド前に **Unityを一度非Active** にする（一度他のアプリをクリックする）ことを習慣づけるようにする。
  * **方法B**
    * プラットフォームスイッチ後、**UnityEditor.EditorApplication.isCompiling がFalseを返すようになった後**  ビルドを行う。

## 例外ケース

プロジェクト内のEditorスクリプトからプラットフォーム依存コンパイルのdefineシンボルを排除できている場合は、上記の限りではありません。ただし、パッケージの内容にも注意しましょう。

# なぜビルドは不安定なのか？

`defineシンボル` の変化がからむためです。（それ以外についてはここでは扱いません）

## defineシンボルの利用について

マルチプラットフォーム対応ビルドでは、 `UNITY_IOS` や `UNITY_ANDROID`, `UNITY_WEBGL` といった、 `defineシンボル` を利用して、プラットフォームに対応して処理を切り替えるということを行っているケースが多いとおもいます。
通常のスクリプト(Editorスクリプトではない)で利用している場合と、Editorスクリプトで利用している場合がありますが、ここで問題にしたいのは後者です。

## defineシンボルを利用する理由

特に必須となる用途としては、以下のようなケースがあります。

* `PBXProject` クラスを利用して、XCodeのプロジェクトファイルを修正する。
  * Frameworkやコンパイルオプションを追加する
  * `[PostProcessBuild]` アトリビュートと一緒に利用することが多い

上記の用途は、 `#if UNITY_IOS` で区切らなければコンパイルエラーになるため必須で、ネイティブの機能を利用する場合はよくある用途といえます。`ARFoundation` パッケージなどを利用している場合も、パッケージ内のEditorスクリプトでそういったことが行われています。

それ以外でも、単にプラットフォームごとに不要なコードを削除したりといったケースもあるかと思います。

## スクリプトからのビルドとdefineシンボルの相性の悪さ

スクリプトからビルドするとき、以下のようなコードになると思います。

```csharp
        UnityEditor.EditorUserBuildSettings.SwitchActiveBuildTarget(
            UnityEditor.BuildTargetGroup.iOS,
            UnityEditor.BuildTarget.iOS);
```

**ここに罠があります！**

現在のターゲットプラットフォームはAndroidだが、iOS向けにビルドする、ということが可能ということになります。便利と思うかもしれませんが、これは **大きな罠** です。それを以下で説明します。


## Q: ターゲットプラットフォームがAndroidになっているときに、EditorスクリプトでiOSビルドを実行するとどうなる？

やってみるとわかりますが、何事もなかったかのように（Unityでの)ビルドは完了します。
でも、問題があります。
たとえば、XCodeでのビルド時にフレームワークが足りない、とか、ビルドが失敗する、動かないビルドができる、といった異常があらわれることが多いです。

これは、前の項目の
`PBXProjectクラスを利用して、XCodeのプロジェクトファイルを修正する。`
ということをしているパターンで起こりがちです。

よくしらべると、Editorスクリプトで **`#if UNITY_IOS` で囲まれた部分が実行されていない** ことに気がつくでしょう。（これを見つけるにはログを仕込んだりする必要があります）

なぜこんなことがおこるのでしょうか？重要なのは以下です。

```
Androidプラットフォーム向けにコンパイルされたEditorスクリプトのまま
iOSプラットフォーム向けのビルドをしているから。
```

Editorスクリプトとしては、UNITY_ANDROID が ON で、UNITY_IOS が OFF のままiOSビルドをしているということになっています。なにせEditorスクリプトは、このビルド作業を動かしている張本人であるため、ビルド中にEditorスクリプト自体は変更されないことから、このようなことが起こります。


ちなみに、Editorスクリプト以外は、UNITY_ANDROID や UNITY_IOS を利用していても、ちゃんとビルドされています。このため、なんとなく動いてしまうことも多いのが、また罠っぽいところです。

## 試してみるには

以下のようなスクリプトを入れてみると、実験ができます。

Assets/Editor/PlatformSwitchSample.cs: ビルド時に、UNITY_IOS, UNITY_ANDROID のON/OFFを出力するスクリプトです。

```csharp
using UnityEditor;
using UnityEngine;

public class PlatformSwitchSample
{
    static bool IosDefined
    {
        get {
#if UNITY_IOS
            return true;
#else
            return false;
#endif
        }
    }
    
    static bool AndroidDefined
    {
        get {
#if UNITY_ANDROID
            return true;
#else
            return false;
#endif
        }
    }

    [PostProcessBuild]
    public static void OnPostProcessBuild(BuildTarget target, string pathToBuiltProject)
    {
        Debug.Log($"$$$$$ PlatformSwitchSample.OnPostProcessBuild target: {target} IosDefined:{IosDefined} AndroidDefined:{AndroidDefined}");
    }
}
```

Assets/Editor/PlatformSwitchMenu.cs: メニューからプラットフォームを切り替えます。


```csharp
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

public static class PlatformSwitchMenu
{
    [MenuItem("PlatformSwitch/Switch/Android")]
    public static void SwitchAndroid()
    {
        UnityEditor.EditorUserBuildSettings.SwitchActiveBuildTarget(
            UnityEditor.BuildTargetGroup.Android,
            UnityEditor.BuildTarget.Android);
    }
    
    [MenuItem("PlatformSwitch/Switch/iOS")]
    public static void SwitchIos()
    {
        UnityEditor.EditorUserBuildSettings.SwitchActiveBuildTarget(
            UnityEditor.BuildTargetGroup.iOS,
            UnityEditor.BuildTarget.iOS);
    }

    [MenuItem("PlatformSwitch/Build/Android")]
    public static void BuildAndroid()
    {
        UnityEditor.BuildPipeline.BuildPlayer(
            new[] {"Assets/Scenes/SampleScene.unity"}, 
            "Build/test.apk",
            UnityEditor.BuildTarget.Android,
            UnityEditor.BuildOptions.None);
        UnityEditor.EditorUtility.RevealInFinder("Build/test.apk");
    }
    
    [MenuItem("PlatformSwitch/Build/iOS")]
    public static void BuildIos()
    {
        UnityEditor.BuildPipeline.BuildPlayer(
            new[] {"Assets/Scenes/SampleScene.unity"}, 
            "Build/",
            UnityEditor.BuildTarget.iOS,
            UnityEditor.BuildOptions.None);
        UnityEditor.EditorUtility.RevealInFinder("Build/");
    }
}
```

メニューからプラットフォームの変更とビルドができると思います。
問題がある場合は、Unityに、AndroidとiOSのビルドモジュールが入っているかどうか確認してください。

試してみると、以下のような出力が出ると思います。

成功例:

```
$$$$$ PlatformSwitchSample.OnPostProcessBuild target: iOS IosDefined:True AndroidDefined:False
```
iOSターゲットで、`UNITY_IOS` が定義されているので正常です。

失敗例:

```
$$$$$ PlatformSwitchSample.OnPostProcessBuild target: iOS IosDefined:False AndroidDefined:True
```
iOSターゲットで、`UNITY_IOS` が定義されていないので異常です。


## 現象

上記で確かめると、以下のような現象が確認できます。

* 異常ケース:
    * プラットフォームをXからYに切り替えた後、すぐにY向けのビルドをした場合、EditorスクリプトはX向けのままになる。
* 異常ケース: 
    * 上記のあと、Unityウィンドウを非ActiveにしたりしてからY向けのビルドをしても、EditorスクリプトはX向けのまま。
* 正常ケース:
    * プラットフォームをXからYに切り替えた後、Unityウィンドウを非Activeにして、その後UnityをActiveにすると、EditorスクリプトがY向けにコンパイルされた状態になり、Y向けのビルドをした場合、EditorスクリプトはY向けになる。
* 正常ケース:
    * プラットフォームをXからYに切り替えた後、Unityの右下でぐるぐるのアイコンが回っており、これが終わるまで待ってからY向けのビルドをした場合、EditorスクリプトはY向けになる。

ここからわかることとしては以下です。

* <b>指定したプラットフォーム向けのビルドを正常にするには、なんらかの方法で、プラットフォームスイッチ後にEditorスクリプトの再コンパイルが完了している必要がある。</b>

つまり、

* <b>ビルド前に、Editorスクリプトを再コンパイルしなければならない</b>

ということです。


# 対策: ビルド前にEditorスクリプトを再コンパイルする方法

以下の方法であれば、Editorスクリプトを再コンパイルをしてからビルドする（＝正常なビルドにする）ことができます。

## 方法A: UnityのBuildSetting画面でプラットフォームスイッチするようにする

これはいってみれば標準の方法です。Unityは、プラットフォーム変更後、Editorスクリプトがコンパイルされるまで待ってから、UIが操作可能になります。Unityは、UIレベルではちゃんとここのところをケアしているということがわかります。

## 方法B: スクリプトからプラットフォーム変更後、一回Unityを非Activeにする

スクリプトでプラットフォーム変更をしたあと、一回別のアプリをクリックして、そのあとUnityのウィンドウをクリックします。このとき、Unityは、Editorスクリプトがコンパイル完了するまで操作できないようになっていますので、確実にEditorスクリプトのコンパイルを待つことができます。

※ UnityのPreference設定で、アクティブ時にリフレッシュするオプションがついていない場合はこの挙動がおこらないかもしれません。

## 方法C: UnityEditor.EditorApplication.isCompiling が false を返すようになった後 ビルドを行う。

Editorスクリプトがコンパイル中の場合、  UnityEditor.EditorApplication.isCompiling が trueをかえします。このため、ビルドスクリプトの手前に以下のコードを入れておけば、問題のあるケースを抑止できます。

```csharp
bool canBuild = !UnityEditor.EditorApplication.isCompiling;
if (!canBuild)
{
    UnityEditor.EditorUtility.DisplayDialog("Error", "Can't start build now. Please wait while compiling scripts...", "OK");
    return;
}
```

## 方法X: Editorスクリプトではプラットフォーム依存コンパイルを行わない

これが可能なケースでは、この記事で書いたような問題が発生しません。Androidプラットフォーム向けの場合のEditorスクリプトと、iOSプラットフォーム向けの場合のEditorスクリプトのコンパイル結果が同じになるためです。
これができると一番良いのですが、残念ながらそうはいかないケースが多いです。ともあれ、`#if` の利用をできるだけ抑制的にするのは常におすすめです。


# まとめ

Unityのマルチプラットフォームビルドは罠が多いので気をつけよう！