hyoromoのブログ

最近はVRSNS向けに作ったものについて書いています

Unity製ゲームで楽ちんな体験版の作り方

今回は体験版/製品版を1つのプロジェクトで楽に分けてリリースビルドする方法を書きます。

対応ゲーム

R18禁のゲームですが、以下ゲームにてこの方法で体験版/製品版をリリースしています。

banner


DLSite上では体験版と製品版のバイナリサイズは下図のように記載されています。
このように体験版の範囲で必須なAssetだけ絞って体験版バイナリを作る事も可能です。

環境

今回は以下の環境で行いました。

  • Unity2022.3.43f1
  • Addressables v1.22.2

対応方法

対応方法として2通りあります。

体験版用のスクリプトシンボルを設定


Project Setingsビューから、Other Setings > Scripting Define Symbols から設定可能です。
一見なんだかよく分からないかもしれませんが、よく使っている以下のようなDEVELOPMENT_BUILDUNITY_EDITORシンボルと同じです。

#if DEVELOPMENT_BUILD
    // ここにデバッグビルド時に実行したい処理を書く
#endif

#if UNITY_EDITOR
    // ここにUnityエディタ上の時に実行したい処理を書く
#endif

このScripting Define SymbolsはBuildPlayerで動的に追加する事が可能なため、体験版の時に DEMO_PLAY 等のシンボルを追加して以下のように体験版だけ通る処理を書くことが可能となります。

#if DEMO_PLAY
    // 体験版の範囲が終わったら、進行不能にして製品版の購入を誘導する
#endif

BuildPlayerへ追加する方法も簡単で、その部分だけ書くと以下のようになります。

BuildPlayerOptions buildOptions = new BuildPlayerOptions();
(略)
buildOptions.extraScriptingDefines = new[] { "DEMO_PLAY" };
(略)
BuildPipeline.BuildPlayer(buildOptions);

この方法は体験版だけではなく、例えば以下の場合に有用な手だと思います。

  • 【広告あり無料版】と【広告なし有料版】アプリを出し分けたい
  • 配信プラットフォームに合わせてストアへ誘導するURLを分けたい
Addressablesで体験版に含めないAsset Groupを作る

製品版では使うが、体験版では使わないので含めないAssetがあるかと思います。
そういったものはAddressablesでAsset管理している場合、Group分けしておくとBuildPlayerで【含めるGroup】【含めないGroup】を切り替えてバイナリ生成する事が出来ます。
※Addressables自体の説明は割愛します。検索すると沢山ヒットするかと思いますので、もしご存知なければ先に調べてみてください。

ちなみにDebug用のGroupも作成して分けておくと、リリースビルドに余計なアセットが紛れなくなります。以降にDebugと表記されていたらDebug用Assetだと思ってください。

Group作成手順

1. 分かりやすいようにBaseAssets(全てのビルドで含む)/ReleaseAssets(製品版だけに含む)/ReleaseAssets(DebugBuild時のみ含む)の3つフォルダを作成し、Addressable管理下に配置

2. ProjectビューからAddressable管理下のAssetを選択し、Inspector上のSelectを押下

3. Addressables Groupsビューの左上にある New > Packed Assets を選択
4. Groupが新規作成されるので、右クリックからRenameして分かりやすい名称に変更
5. 初期Groupは Default Local Group に所属しているので、新規作成したGroupへ移動させる必要があります。移動対象を 右クリック > Move Addresasbles to Group... 選択
移動先のGroupを選択
6. 分けたいGroup分だけ(3-5)を繰り返す

これでGroupを新規作成し、Group分けまでが完了しました。

ビルド時に含めるGroupを分けるのは、またBuildPlayerで行います。
Releaseを製品版だけ同梱するコードは以下のようになります*1

/// <summary>
/// Addressables設定をビルド内容に応じて変更する
/// </summary>
/// <param name="isDemo">体験版であればtrue</param>
private static void changeAddressableSettings(bool isDemo)
    var setting = AddressableAssetSettingsDefaultObject.Settings;
    if (setting != null) {
        var releaseGroup = setting.FindGroup("Release");
        if (releaseGroup != null) {
            var schema = releaseGroup.GetSchema<BundledAssetGroupSchema>();
            if (isDemo) {
                schema.BuildPath.SetVariableByName(setting, AddressableAssetSettings.kRemoteBuildPath);
                schema.LoadPath.SetVariableByName(setting, AddressableAssetSettings.kRemoteLoadPath);
            } else {
                schema.BuildPath.SetVariableByName(setting, AddressableAssetSettings.kLocalBuildPath);
                schema.LoadPath.SetVariableByName(setting, AddressableAssetSettings.kLocalLoadPath);
            }
        }
    }
}

Group毎にアプリに同梱(Local)するかアプリ起動時にDownload(Remote)してくるかを設定する事が出来ます。
Localとすべき設定を、Remote設定にする事でアプリへは同梱されなくなるという訳です。
もし前述した体験版用のシンボルを追加しないのであれば、Release GroupのAssetが存在するかを判断材料に出来ます*2

なお、当たり前ですが体験版のプレイ範囲内でRelease GroupのAssetへアクセスしようとすると存在しないためエラーになります。なので、体験版と製品版の範囲が明確になってから対応ください。

Tips

Addressables Group分けを行ったら読込速度が低下した


CRCチェックを有効にしていると遅くなる場合があるようです。
Assets/AddressableAssetsData/AssetGroups/(Group名) を選択し、Inspectorから Asset Bundle CRCDisabled に変更する事で問題が解決するかもしれません。これをONにしておくとBundleの整合性チェックが行われるのですが、アプリ同梱Bundleでチェックする必要性もないためDisabledにしても問題ないかと思われます*3

BuildPipeline.BuildPlayerのフルコード

Androidだけですが、デバッグ版/体験版/製品版で分けてビルドしてapkファイルを書き出すコードは以下の通りです*4AndroidをベースにiOS/Windows向けに書き換えることで、各プラットフォームへのBuildでも動作するかと思います。

using UnityEngine;
using UnityEditor;
using UnityEditor.Build.Reporting;
using System.IO;
using System.Linq;
using UnityEditor.AddressableAssets;
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.AddressableAssets.Settings.GroupSchemas;

public class BatchBuild
{
    private const string PRODUCTS_FOLDER_NAME = "Products";

    // Android デバッグ ビルド
    [MenuItem("Build/1.Android/Debug")]
    static void AndroidDebugBuild()
    {
        UnityEngine.Debug.Log("====== Android Debug Build Start ======");
        AndroidBuid(true);
        UnityEngine.Debug.Log("====== Android Debug Build End ======");
    }

    // Android リリース ビルド
    [MenuItem("Build/1.Android/製品版")]
    static void AndroidReleaseBuild()
    {
        UnityEngine.Debug.Log("====== Android Release Build Start ======");
        AndroidBuid(false);
        UnityEngine.Debug.Log("====== Android Release Build End ======");
    }

    // Android 体験版のリリース ビルド
    [MenuItem("Build/1.Android/体験版")]
    static void AndroidDemoReleaseBuild()
    {
        UnityEngine.Debug.Log("====== Android Demo Release Build Start ======");
        AndroidBuid(false, true);
        UnityEngine.Debug.Log("====== Android Demo Release Build End ======");
    }

    static void AndroidBuid(bool isDebug, bool isDemo = false)
    {
        EditorUserBuildSettings.SwitchActiveBuildTarget(BuildTargetGroup.Android, BuildTarget.Android);

        BuildPlayerOptions buildOptions = new BuildPlayerOptions();
        buildOptions.scenes = getBuildScenes();

        // 出力用のファイルを定義
        var outputFile = getAndroidFilePath(isDebug, isDemo);

        if (File.Exists(outputFile))
        {
            File.Delete(outputFile);
        }
        buildOptions.locationPathName = outputFile;

        buildOptions.target = BuildTarget.Android;
        if (isDebug)
        {
            buildOptions.options = BuildOptions.Development;
        }
        else
        {
            buildOptions.options = BuildOptions.None;
        }

        // 体験版
        if (isDemo)
        {
            buildOptions.extraScriptingDefines = new[] { "DEMO_PLAY" };
        }

        changeAddressableSettings(isDemo, isDebug);

        PlayerSettings.SetScriptingBackend(BuildTargetGroup.Android, ScriptingImplementation.IL2CPP);
        PlayerSettings.Android.targetArchitectures = AndroidArchitecture.ARM64 | AndroidArchitecture.ARMv7;

        var report = BuildPipeline.BuildPlayer(buildOptions);
        if (report.summary.result == BuildResult.Succeeded)
        {
            if (isDebug)
            {
                UnityEngine.Debug.Log("====== Android Debug Build Succeeded ======");
            }
            else
            {
                UnityEngine.Debug.Log("====== Android Release Build Succeeded ======");
            }
        }
        else
        {
            if (isDebug)
            {
                UnityEngine.Debug.Log("====== Android Debug Build Failed ======");
            }
            else
            {
                UnityEngine.Debug.Log("====== Android Release Build Failed ======");
            }
        }
    }

    /// <summary>
    /// Androidバイナリファイルのパスを返す
    /// </summary>
    private static string getAndroidFilePath(bool isDebug, bool isDemo)
    {
        // 書き出すパス指定(ご自身の環境で変更ください)
        var outputFile = Application.dataPath + "/../" + PRODUCTS_FOLDER_NAME + "/Android/" + Application.productName;
        if (isDemo)
        {
            outputFile += "_demo.apk";
        }
        else if (isDebug)
        {
            outputFile += "_debug.apk";
        }
        else
        {
            outputFile += ".apk";
        }
        return outputFile;
    }

    /// <summary>
    /// ビルドするSceneを返す
    /// </summary>
    /// <returns></returns>
    private static string[] getBuildScenes()
    {
        return EditorBuildSettings.scenes.Where(s => s.enabled).Select(s => s.path).ToArray();
    }

    /// <summary>
    /// Addressables設定をビルド内容に応じて変更する
    /// </summary>
    /// <param name="isDemo">体験版であればtrue</param>
    /// <param name="isDebug">デバッグ版であればtrue</param>
    private static void changeAddressableSettings(bool isDemo, bool isDebug)
    {
        var setting = AddressableAssetSettingsDefaultObject.Settings;
        if (setting != null)
        {
            var releaseGroup = setting.FindGroup("Release");
            if (releaseGroup != null)
            {
                var schema = releaseGroup.GetSchema<BundledAssetGroupSchema>();
                if (isDemo)
                {
                    schema.BuildPath.SetVariableByName(setting, AddressableAssetSettings.kRemoteBuildPath);
                    schema.LoadPath.SetVariableByName(setting, AddressableAssetSettings.kRemoteLoadPath);
                }
                else
                {
                    schema.BuildPath.SetVariableByName(setting, AddressableAssetSettings.kLocalBuildPath);
                    schema.LoadPath.SetVariableByName(setting, AddressableAssetSettings.kLocalLoadPath);
                }
            }

            var debugGroup = setting.FindGroup("Debug");
            if (debugGroup != null)
            {
                var schema = debugGroup.GetSchema<BundledAssetGroupSchema>();
                if (isDebug)
                {
                    schema.BuildPath.SetVariableByName(setting, AddressableAssetSettings.kLocalBuildPath);
                    schema.LoadPath.SetVariableByName(setting, AddressableAssetSettings.kLocalLoadPath);
                }
                else
                {
                    schema.BuildPath.SetVariableByName(setting, AddressableAssetSettings.kRemoteBuildPath);
                    schema.LoadPath.SetVariableByName(setting, AddressableAssetSettings.kRemoteLoadPath);
                }
            }
        }
    }
}

Build後に変更したGroup設定を戻すコードはありません。必要であればご自身で追加ください。

*1:Debug分を含めると可読性が落ちるため除外しています。後ほどスクリプト全体掲載時の方には掲載しているので、そちらを参照ください。

*2:存在チェックのコストを考えるとシンボル用意も行った方が良いかと思います

*3:DLする場合にチェックする必要が出てきます

*4:aabファイルはBuildSettingsがまた異なる点に注意ください