えんじにあ雑記!

開発していて学んだことをまとめていきます!

Unityでデバッグ/本番を切り分けたビルド環境を構築する

f:id:flat-M_M:20210416014927p:plain

Unityのみならず、開発ではデバッグ環境と本番環境を区別して開発することが多いです。

その際にDebug環境でのみ含めたいコードなどがあり、Debug用と本番用を切り分けることができるビルド環境の構築方法をまとめました。

概要

BuildPipeline.csというビルドの手順を定義したスクリプトと、CustomBuildParameterというビルド時の設定情報を定義したスクリプトを作成して、ビルド環境の構築を実現します。

CustomBuildParameter.cs

ビルドの詳細設定などを管理するクラスとしてCustomBuildParameter.csを作成します。

ビルド設定を管理する変数を宣言

管理する設定はプロジェクトのビルド設定によりますが、ここでは下記の点を実装した例を紹介します。

  • 開発環境、本番環境どちらのビルドか判定するフラグ
  • ビルドターゲット
  • 開発環境時のみ含めるシーンの設定

まずは、上記の点を判定できるようなフィールド変数を定義します。

public class CustomBuildParameter
{
    /// <summary>
    /// 本番用ビルドかどうか
    /// </summary>
    public readonly bool IsProductionBuild;

    /// <summary>
    /// ビルドターゲット
    /// </summary>
    public readonly BuildTarget BuildTarget;

    /// <summary>
    /// ビルドに含めるシーン名一覧
    /// </summary>
    public readonly string[] BuildScenes;

    /// <summary>
    /// ビルド時の設定を保持する変数
    /// </summary>
    public BuildPlayerOptions BuildPlayerOptions;
    
    /// <summary>
    /// Debug時のみ含めたいシーン名一覧
    /// </summary>
    private static readonly string[] DebugSceneArray = new[] {"DebugScene"};
}

コンストラクタの定義

次にコンストラクタを定義します。

public CustomBuildParameter(bool isProductionBuild, BuildTarget buildTarget)
{
    IsProductionBuild = isProductionBuild;
    BuildTarget = buildTarget;
    // Debug用か本番用かを判定してビルドするシーンを取得する
    BuildScenes = GetScenes(isProductionBuild);
    
    // 下記で既知のパラメータからBuildPlayerOptionsの一部を設定していく
    BuildPlayerOptions.target = buildTarget;
    BuildPlayerOptions.scenes = BuildScenes;
    // Platformに対応したビルド成果物の保存先を設定する
    SetOutputLocationPath();
}

各種メソッドの定義

まずは、ビルドに含めるシーンをBuildSettingsから取得するコードを実装します。

BuildSettingsに登録しているシーンはEditorBuildSettings.scenesで取得できます。

private static string[] GetScenes(bool isProductionBuild)
{
    var sceneList = new List<string>();
    var scenes = EditorBuildSettings.scenes;
    foreach (var scene in scenes)
    {
        if (isProductionBuild && DebugSceneArray.Contains(scene.path))
        {
            Debug.Log($"{scene.path}はProductionビルドから除外しました。");
            continue;
        }
        Debug.Log($"{scene.path}はビルドに含まれています。");
        sceneList.Add(scene.path);
    }
    return sceneList.ToArray();
}

次に、ビルド成果物の出力先をプラットフォームに合わせて登録するメソッドを実装します。

iOSの場合はディレクトリ、Androidの場合はapkファイルになるのでその分岐を行います。

private void SetOutputLocationPath()
{
    if (BuildTarget == BuildTarget.iOS)
    {
        var locationPathName = "iOS/";
        locationPathName += IsProductionBuild ? "Production": "Debug";
        BuildPlayerOptions.locationPathName = locationPathName;
    } else if (BuildTarget == BuildTarget.Android)
    {
        var locationPathName = "Android/";
        locationPathName += IsProductionBuild ? "Production.apk" : "Debug.apk";
        BuildPlayerOptions.locationPathName = locationPathName;
    }
    else
    {
        throw new NotSupportedException($"{BuildTarget}へのビルドはサポートしていません。");
    }
}

BuildPipeline.cs

BuildPipeline.csではiOS/AndroidそれぞれのDebug用/本番用の合計4パターンのビルド手順を定義します。

ビルド時の詳細な設定はCustomBuildParameter.csの役割とするため、BuildPipelineではビルドフローのみを実装します。

実際のビルドメソッドは次のようなコードになっています。

[MenuItem(MENU_ITEM_PATH + nameof(DebugBuildForiOS))]
public static void DebugBuildForiOS()
{
    Debug.Log($"=========={nameof(DebugBuildForiOS)} will start==========");
    // BuildParameterの作成
    var customBuildParameter = new CustomBuildParameter(false, BuildTarget.iOS);
    
    // ScriptingDefineSymbolsの更新
    InsertScriptingDefineSymbols(customBuildParameter);
    
    // ビルド実行
    ExecuteBuild(customBuildParameter);
}

ビルドメソッドは3つのフローで構成されており、それらは下記の通りです。

  1. CustomBuildParameterの作成
  2. ScriptingDefineSymbolsの設定
  3. ビルド実行

各工程を一つずつ解説していきます。

CustomBuildParameterの作成

ビルドの設定情報を保持しているCustomBuildParameterのインスタンスを作成します。

第一引数にはDebug用か、本番用かのフラグを、

第二引数にはBuildTargetを指定します。

ScriptingDefineSymbolsの設定

ScriptingDefineSymbolsを設定することで、スクリプトの一部をビルドに含めないなどと言ったことが実現できます。

それをスクリプトからビルドモードに合わせて設定します。

/// <summary>
/// ビルドモードに合わせてScriptingDefineSymbolsの設定を上書きする
/// </summary>
/// <param name="customBuildParameter">CustomBuildParameter</param>
private static void InsertScriptingDefineSymbols(CustomBuildParameter customBuildParameter)
{
    var newScriptingDefineSymbols = new List<string>();
    var scriptingDefineSymbols = PlayerSettings.GetScriptingDefineSymbolsForGroup(customBuildParameter.GetBuildTargetGroup());
    if (!string.IsNullOrEmpty(scriptingDefineSymbols))
    {
        var symbols = scriptingDefineSymbols.Split(';');
        newScriptingDefineSymbols.AddRange(symbols);
    }
    if (customBuildParameter.IsProductionBuild)
    {
        // Production用ビルド時に設定したいScriptingDefineSymbolsが設定されていない場合は追加する
        foreach (var symbol in ProductionScriptingDefineSymbols)
        {
            if (!newScriptingDefineSymbols.Contains(symbol))
            {
                newScriptingDefineSymbols.Add(symbol);
            }
        }

        // Debug用ビルド時に設定したいScriptingDefineSymbolsが設定されている場合は削除する
        foreach (var symbol in DebugScriptingDefineSymbols)
        {
            if (newScriptingDefineSymbols.Contains(symbol))
            {
                newScriptingDefineSymbols.Remove(symbol);
            }
        }
    }
    else
    {
        // Debug用ビルド時に設定したいScriptingDefineSymbolsが設定されていない場合は追加する
        foreach (var symbol in DebugScriptingDefineSymbols)
        {
            if (!newScriptingDefineSymbols.Contains(symbol))
            {
                newScriptingDefineSymbols.Add(symbol);
            }
        }

        // Production用ビルド時に設定したいScriptingDefineSymbolsが設定されている場合は削除する
        foreach (var symbol in ProductionScriptingDefineSymbols)
        {
            if (newScriptingDefineSymbols.Contains(symbol))
            {
                newScriptingDefineSymbols.Remove(symbol);
            }
        }
    }

    PlayerSettings.SetScriptingDefineSymbolsForGroup(customBuildParameter.GetBuildTargetGroup(), newScriptingDefineSymbols.ToArray());
}

現在設定されているScriptingDefineSymbolsを取得するにはPlayerSettings.GetScriptingDefineSymbolsForGroup(BuildTargetGroup targetGroup)を使います。 BuildTargetからBuildTargetGroupへの変換は下記のブログを参考にしてください。

baba-s.hatenablog.com

ビルド実行

最後にビルド実行のメソッドを呼び出します。

スクリプトからビルドを呼び出すのは下記のコードになります。

private static void ExecuteBuild(CustomBuildParameter customBuildParameter)
{
    var report = UnityEditor.BuildPipeline.BuildPlayer(customBuildParameter.BuildPlayerOptions);
    var summary = report.summary;
    if (summary.result == BuildResult.Succeeded)
    {
        Debug.Log($"Build succeeded: {summary.totalSize} bytes at {summary.outputPath}");
    }
    else if (summary.result == BuildResult.Failed)
    {
        Debug.LogError($"Build failed");
    }
}

完成したCustomBuildParameter.cs

上記で解説している以外に、出力先のディレクトリチェックや、BuildOptionの設定などのメソッドを新たに追加したCustomBuildParameter.csを載せておきます。

public class CustomBuildParameter
{
    /// <summary>
    /// Production用ビルドかどうかのフラグ
    /// </summary>
    public readonly bool IsProductionBuild;

    /// <summary>
    /// ビルドターゲット
    /// </summary>
    public readonly BuildTarget BuildTarget;

    /// <summary>
    /// ビルドに含めるシーン名一覧
    /// </summary>
    public readonly string[] BuildScenes;

    /// <summary>
    /// ビルド時の設定を保持する変数
    /// </summary>
    public BuildPlayerOptions BuildPlayerOptions;
    
    /// <summary>
    /// Debug時のみ含めたいシーン名一覧
    /// </summary>
    private static readonly string[] DebugSceneArray = new[] {"DebugScene"};

    /// <summary>
    /// コンストラクタ
    /// </summary>
    /// <param name="isProductionBuild">Productionビルドかどうか</param>
    /// <param name="buildTarget">ビルドターゲット</param>
    public CustomBuildParameter(
        bool isProductionBuild, 
        BuildTarget buildTarget
    )
    {
        IsProductionBuild = isProductionBuild;
        BuildTarget = buildTarget;
        BuildScenes = GetScenes(isProductionBuild);
        
        // 下記で既知のパラメータからBuildPlayerOptionsの一部を設定していく
        BuildPlayerOptions.target = buildTarget;
        BuildPlayerOptions.scenes = BuildScenes;
        // Platformに対応したOutput場所を設定する
        SetOutputLocationPath();
    }
    
    /// <summary>
    /// ビルド成果物の出力先ディレクトリの存在をチェックし、なければ作成する
    /// </summary>
    /// <exception cref="NoNullAllowedException">Output Directoryが設定されていない場合は例外を発生させる</exception>
    public void CheckOutputDirectoryExist()
    {
        // Androidビルドの場合は.apk、iOSビルドの場合は一つのディレクトリが成果物としているので、その判定
        var hasExtension = Path.HasExtension(BuildPlayerOptions.locationPathName);
        var dirPath = hasExtension ? Path.GetDirectoryName(BuildPlayerOptions.locationPathName) : BuildPlayerOptions.locationPathName;
        
        Debug.Log($"dirPath: {dirPath}");
        if (string.IsNullOrEmpty(dirPath))
        {
            throw new NoNullAllowedException($"ビルド成果物のOutput先のディレクトリ名が設定されていません。");
        }
        if (!Directory.Exists(dirPath))
        {
            Directory.CreateDirectory(dirPath);
        }
    }

    /// <summary>
    /// BuildOptionsを追加登録する
    /// </summary>
    /// <param name="buildOptions">buildOptions</param>
    public void AppendBuildOption(BuildOptions buildOptions)
    {
        BuildPlayerOptions.options |= buildOptions;
    }

    /// <summary>
    /// BuildTargetからBuildTargetGroupを取得する
    /// </summary>
    /// <returns>BuildTargetから判定したBuildTargetGroup</returns>
    /// <exception cref="NotSupportedException">サポートしていないBuildTargetが設定されている場合</exception>
    public BuildTargetGroup GetBuildTargetGroup()
    {
        switch (BuildTarget)
        {
            case BuildTarget.iOS:
                return BuildTargetGroup.iOS;
            case BuildTarget.Android:
                return BuildTargetGroup.Android;
            default:
                throw new NotSupportedException($"現在のプラットフォーム({BuildTarget})には対応していません。");
        }
    }

    /// <summary>
    /// ビルドに含めるシーン名一覧を取得する
    /// </summary>
    /// <param name="isProductionBuild">Productionビルドかどうか</param>
    /// <returns>ビルドに含めるシーン名一覧</returns>
    private static string[] GetScenes(bool isProductionBuild)
    {
        var sceneList = new List<string>();
        var scenes = EditorBuildSettings.scenes;
        foreach (var scene in scenes)
        {
            if (isProductionBuild && DebugSceneArray.Contains(scene.path))
            {
                Debug.Log($"{scene.path}はProductionビルドから除外しました。");
                continue;
            }
            Debug.Log($"{scene.path}はビルドに含まれています。");
            sceneList.Add(scene.path);
        }
        return sceneList.ToArray();
    }

    /// <summary>
    /// ビルド成果物のアウトプット先を設定する
    /// </summary>
    private void SetOutputLocationPath()
    {
        if (BuildTarget == BuildTarget.iOS)
        {
            var locationPathName = BuildPipeline.IOS_OUTPUT_DIR;
            locationPathName += IsProductionBuild ? BuildPipeline.WORD_PRODUCTION : BuildPipeline.WORD_DEBUG;
            BuildPlayerOptions.locationPathName = locationPathName;
        } else if (BuildTarget == BuildTarget.Android)
        {
            var locationPathName = BuildPipeline.ANDROID_OUTPUT_DIR;
            locationPathName += IsProductionBuild ? BuildPipeline.WORD_PRODUCTION : BuildPipeline.WORD_DEBUG;
            locationPathName += ".apk";
            BuildPlayerOptions.locationPathName = locationPathName;
        }
        else
        {
            throw new NotSupportedException($"{BuildTarget}へのビルドはサポートしていません。");
        }
    }
}

完成したBuildPipeline.cs

BuildPipeline.csに関しても、完成したスクリプトの全体を載せておきます。

public static class BuildPipeline
{
    /// <summary>
    /// MenuItemに使用するベースのパス
    /// </summary>
    private const string MENU_ITEM_PATH = "Tools/Build Pipeline/";

    /// <summary>
    /// Debugビルド時のScriptingDefineSymbols
    /// </summary>
    private static readonly string[] DebugScriptingDefineSymbols = new[] {"DEBUG_BUILD"};

    /// <summary>
    /// Productionビルド時のScriptingDefineSymbols
    /// </summary>
    private static readonly string[] ProductionScriptingDefineSymbols = new[] {"PRODUCTION_BUILD"};

    /// <summary>
    /// iOS用Debugビルド
    /// </summary>
    [MenuItem(MENU_ITEM_PATH + nameof(DebugBuildForiOS))]
    public static void DebugBuildForiOS()
    {
        Debug.Log($"=========={nameof(DebugBuildForiOS)} will start==========");
        // BuildParameterの作成
        var customBuildParameter = new CustomBuildParameter(false, BuildTarget.iOS);
        
        // ScriptingDefineSymbolsの更新
        InsertScriptingDefineSymbols(customBuildParameter);
        
        // ビルド実行
        ExecuteBuild(customBuildParameter);
    }

    /// <summary>
    /// Android用Debugビルド
    /// </summary>
    [MenuItem(MENU_ITEM_PATH + nameof(DebugBuildForAndroid))]
    public static void DebugBuildForAndroid()
    {
        Debug.Log($"=========={nameof(DebugBuildForAndroid)} will start==========");
        // BuildParameterの作成
        var customBuildParameter = new CustomBuildParameter(false, BuildTarget.Android);

        // ScriptingDefineSymbolsの更新
        InsertScriptingDefineSymbols(customBuildParameter);
        
        // ビルド実行
        ExecuteBuild(customBuildParameter);
    }

    /// <summary>
    /// iOS用本番環境ビルド
    /// </summary>
    [MenuItem(MENU_ITEM_PATH + nameof(ProductionBuildForiOS))]
    public static void ProductionBuildForiOS()
    {
        Debug.Log($"=========={nameof(ProductionBuildForiOS)} will start==========");
        // BuildParameterの作成
        var customBuildParameter = new CustomBuildParameter(true, BuildTarget.iOS);
        
        // ScriptingDefineSymbolsの更新
        InsertScriptingDefineSymbols(customBuildParameter);
        
        // ビルド実行
        ExecuteBuild(customBuildParameter);
    }

    /// <summary>
    /// Android用本番環境ビルド
    /// </summary>
    [MenuItem(MENU_ITEM_PATH + nameof(ProductionBuildForAndroid))]
    public static void ProductionBuildForAndroid()
    {
        Debug.Log($"=========={nameof(ProductionBuildForAndroid)} will start==========");
        // BuildParameterの作成
        var customBuildParameter = new CustomBuildParameter(true, BuildTarget.Android);
        
        // ScriptingDefineSymbolsの更新
        InsertScriptingDefineSymbols(customBuildParameter);
        
        // ビルド実行
        ExecuteBuild(customBuildParameter);
    }

    /// <summary>
    /// ビルドモードに合わせてScriptingDefineSymbolsの設定を上書きする
    /// </summary>
    /// <param name="customBuildParameter">CustomBuildParameter</param>
    private static void InsertScriptingDefineSymbols(CustomBuildParameter customBuildParameter)
    {
        var newScriptingDefineSymbols = new List<string>();
        var scriptingDefineSymbols = PlayerSettings.GetScriptingDefineSymbolsForGroup(customBuildParameter.GetBuildTargetGroup());
        if (!string.IsNullOrEmpty(scriptingDefineSymbols))
        {
            var symbols = scriptingDefineSymbols.Split(';');
            newScriptingDefineSymbols.AddRange(symbols);
        }
        if (customBuildParameter.IsProductionBuild)
        {
            // Production用ビルド時に設定したいScriptingDefineSymbolsが設定されていない場合は追加する
            foreach (var symbol in ProductionScriptingDefineSymbols)
            {
                if (!newScriptingDefineSymbols.Contains(symbol))
                {
                    newScriptingDefineSymbols.Add(symbol);
                }
            }

            // Debug用ビルド時に設定したいScriptingDefineSymbolsが設定されている場合は削除する
            foreach (var symbol in DebugScriptingDefineSymbols)
            {
                if (newScriptingDefineSymbols.Contains(symbol))
                {
                    newScriptingDefineSymbols.Remove(symbol);
                }
            }
        }
        else
        {
            // Debug用ビルド時に設定したいScriptingDefineSymbolsが設定されていない場合は追加する
            foreach (var symbol in DebugScriptingDefineSymbols)
            {
                if (!newScriptingDefineSymbols.Contains(symbol))
                {
                    newScriptingDefineSymbols.Add(symbol);
                }
            }

            // Production用ビルド時に設定したいScriptingDefineSymbolsが設定されている場合は削除する
            foreach (var symbol in ProductionScriptingDefineSymbols)
            {
                if (newScriptingDefineSymbols.Contains(symbol))
                {
                    newScriptingDefineSymbols.Remove(symbol);
                }
            }
        }

        PlayerSettings.SetScriptingDefineSymbolsForGroup(customBuildParameter.GetBuildTargetGroup(), newScriptingDefineSymbols.ToArray());
    }
    
    /// <summary>
    /// ビルドの実行
    /// </summary>
    /// <param name="customBuildParameter">CustomBuildParameter</param>
    private static void ExecuteBuild(CustomBuildParameter customBuildParameter)
    {
        var report = UnityEditor.BuildPipeline.BuildPlayer(customBuildParameter.BuildPlayerOptions);
        var summary = report.summary;
        if (summary.result == BuildResult.Succeeded)
        {
            Debug.Log($"Build succeeded: {summary.totalSize} bytes at {summary.outputPath}");
        }
        else if (summary.result == BuildResult.Failed)
        {
            Debug.LogError($"Build failed");
        }
    }
}