ブログ

[Unity] AddressableAssetSystem を複数ビルドターゲット(Develop, Product)、複数リモートサーバー(開発サーバー、本番サーバー)で使用する

背景

  • AddressableAssetSystem(以下、AAS)の基本的な解説は各所にある
  • が、実際の開発での適用例は少ない
  • 複数ビルドターゲット(Develop, Product)、複数リモートサーバー(開発サーバー、本番サーバー)での AAS 適用例を解説していく

実現したこと

  • Develop版apk(デバッグ機能入り)と Product版apk(製品)の作成
  • Develop版アセット(デバッグ機能アセット入り) と Product版アセット の作成時に、出力名や出力フォルダを分ける
    • ビルドターゲットを変更するだけで AAS の各設定が行われる
    • 変更するのは、OverridePlayerVersion、ContentStateBuildPath、LocalBuildPath、LocalLoadPath、RemoteBuildPath、RemoteLoadPath の6つ
  • 上記アセットを開発サーバーにアップロードして開発
  • 開発完了したらProduct版アセットを本番サーバーにアップロード
  • アプリバージョンを更新
  • リモートアセットのみを更新
  • 環境:Unity 2020.3.23, Addressables 1.19.19

方法

1. Develop版、Product版の環境を用意

以下を参考にする。(Release は Product と読み替える)

2. リモートアセットを使わずに AAS を適用

以下などを参考にしながら、ローカルアセットで完結する仕組みを構築する。

apk, ipa の中にすべてのアセットが入っている状態。

3. リモートアセットの設定(システム側)

AAS_11

  • Build Remote Catalog にチェック
  • Build Path を RemoteBuildPath に変更
  • Load Path を RemoteLoadPath に変更
  • Player Version Override と Content State Build Path は以下のスクリプトにて設定
using UnityEngine;
using UnityEditor;
using System.Linq;
using UnityEditor.AddressableAssets.Settings;
using UnityEditor.AddressableAssets.Settings.GroupSchemas;
using UnityEngine.Assertions;
using UnityEditor.AddressableAssets;

[InitializeOnLoad]
public class EditorMyAppMenu
{
    static EditorMyAppMenu()
    {
        EditorApplication.update += calc;
    }

    public enum EBuildTarget
    {
        Develop,
        Product
    }

    private static void calc()
    {
        // Develop/Productメニューが有効になったが両方にチェックが入っていない場合は、Rspファイルを参照してどちらかにチェックを入れる
        // コンストラクタではメニューが構築されておらず、Menu.SetChecked できないので、calc でポーリングしている
        if (Menu.GetEnabled(cMenu_BuildTarget_Develop) && Menu.GetEnabled(cMenu_BuildTarget_Product)
            && !Menu.GetChecked(cMenu_BuildTarget_Develop) && !Menu.GetChecked(cMenu_BuildTarget_Product))
        {
            EBuildTarget curBuildTarget = getBuildTargetFromRsp();
            if (curBuildTarget == EBuildTarget.Develop) OnBuildTarget_Develop();
            else if (curBuildTarget == EBuildTarget.Product) OnBuildTarget_Product();
            else throw new System.Exception("[EditorMyAppMenu] Illigal BuildTarget"); ;
        }
    }

    [MenuItem(cMenu_BuildTarget_Develop)]
    private static void OnBuildTarget_Develop()
    {
        sBuildTarget = EBuildTarget.Develop;

        // チェックマークを付ける
        Menu.SetChecked(cMenu_BuildTarget_Develop, true);
        Menu.SetChecked(cMenu_BuildTarget_Product, false);

        // PlayerSettingsの設定(Androidの場合。iOS も必要であれば追記する)
        var group = BuildPipeline.GetBuildTargetGroup(UnityEditor.BuildTarget.Android);
        PlayerSettings.SetManagedStrippingLevel(group, ManagedStrippingLevel.Low);
        PlayerSettings.SetApplicationIdentifier(group, "com.konden.myapp.develop");
        PlayerSettings.productName = "Develop";

        // slnファイルを削除
        deleteSln();

        // rspファイルを上書き保存
        createRsp("-define:UNDER_DEVELOP");

        // デバッグリソースをインポート
        AddressableImporter.FolderImporter.ReimportFolders(new string[] { "Assets/Main/Debug" });

        // AddressableAsset の Develop/Product対応
        var settings = AddressableAssetSettingsDefaultObject.Settings;
        settings.OverridePlayerVersion = Application.version.Replace(".", "_") + "_Develop";
        settings.ContentStateBuildPath = settings.ConfigFolder + "/Develop";
        settings.profileSettings.SetValue(settings.activeProfileId, "LocalBuildPath", "[UnityEngine.AddressableAssets.Addressables.BuildPath]/Develop/[BuildTarget]");
        settings.profileSettings.SetValue(settings.activeProfileId, "LocalLoadPath", "{UnityEngine.AddressableAssets.Addressables.RuntimePath}/Develop/[BuildTarget]");
        settings.profileSettings.SetValue(settings.activeProfileId, "RemoteBuildPath", "ServerData/Develop/[BuildTarget]");
        settings.profileSettings.SetValue(settings.activeProfileId, "RemoteLoadPath", "https://本番サーバーURL/MyApp/Develop/[BuildTarget]");
    }

    [MenuItem(cMenu_BuildTarget_Product)]
    private static void OnBuildTarget_Product()
    {
        sBuildTarget = EBuildTarget.Product;

        // チェックマークを付ける
        Menu.SetChecked(cMenu_BuildTarget_Develop, false);
        Menu.SetChecked(cMenu_BuildTarget_Product, true);

        // PlayerSettingsの設定(Androidの場合。iOS も必要であれば追記する)
        var group = BuildPipeline.GetBuildTargetGroup(UnityEditor.BuildTarget.Android);
        PlayerSettings.SetManagedStrippingLevel(group, ManagedStrippingLevel.Low);
        PlayerSettings.SetApplicationIdentifier(group, "com.konden.myapp");
        PlayerSettings.productName = "MyApp";

        // slnファイルを削除
        deleteSln();

        // rspファイルを上書き保存
        createRsp("");

        // デバッグリソースを削除
        deleteAssetGroup("Debug");

        // AddressableAsset の Develop/Product対応
        var settings = AddressableAssetSettingsDefaultObject.Settings;
        settings.OverridePlayerVersion = Application.version.Replace(".", "_") + "_Product";
        settings.ContentStateBuildPath = settings.ConfigFolder + "/Product";
        settings.profileSettings.SetValue(settings.activeProfileId, "LocalBuildPath", "[UnityEngine.AddressableAssets.Addressables.BuildPath]/Product/[BuildTarget]");
        settings.profileSettings.SetValue(settings.activeProfileId, "LocalLoadPath", "{UnityEngine.AddressableAssets.Addressables.RuntimePath}/Product/[BuildTarget]");
        settings.profileSettings.SetValue(settings.activeProfileId, "RemoteBuildPath", "ServerData/Product/[BuildTarget]");
        settings.profileSettings.SetValue(settings.activeProfileId, "RemoteLoadPath", "https://本番サーバーURL/MyApp/Product/[BuildTarget]");
    }

    private static void deleteSln()
    {
        System.IO.FileInfo fileInfo = new System.IO.FileInfo("MyApp.sln");
        fileInfo.Delete();
    }

    private static void createRsp(string text)
    {
        System.IO.StreamWriter sw = new System.IO.StreamWriter("Assets/csc.rsp", false);
        sw.WriteLine(text);
        sw.Flush();
        sw.Close();
    }

    private static EBuildTarget getBuildTargetFromRsp()
    {
        EBuildTarget ret = EBuildTarget.Develop;
        System.IO.StreamReader sr = new System.IO.StreamReader("Assets/csc.rsp");
        string text = sr.ReadToEnd();
        if (text.Contains("-define:UNDER_DEVELOP")) ret = EBuildTarget.Develop;
        else ret = EBuildTarget.Product;
        sr.Close();
        return ret;
    }

    private static AddressableAssetSettings getSettings()
    {
        var guidList = AssetDatabase.FindAssets("t:AddressableAssetSettings");
        var guid = guidList.FirstOrDefault();
        var path = AssetDatabase.GUIDToAssetPath(guid);
        var settings = AssetDatabase.LoadAssetAtPath<AddressableAssetSettings>(path);
        return settings;
    }

    private static void deleteAssetGroup(string groupName)
    {
        var settings = getSettings();
        var group = settings.FindGroup(groupName);
        settings.RemoveGroup(group);
    }

    private const string cMenu_BuildTarget_Develop = "MyApp/Build Target/Develop(VS再起動が必要)";
    private const string cMenu_BuildTarget_Product = "MyApp/Build Target/Product(VS再起動が必要)";
    private static EBuildTarget sBuildTarget = EBuildTarget.Develop;
}

Unityエディタでのビルドターゲット切り替えスクリプトを上記のようにする。

Unity でデバッグリソースを除外してビルドする からの差分は「AddressableAsset の Develop/Product対応」の部分。
ここで OverridePlayerVersion、ContentStateBuildPath、LocalBuildPath、LocalLoadPath、RemoteBuildPath、RemoteLoadPath の6つを設定している。

エディタからビルドターゲットを Develop にすると、下図の赤枠がスクリプトにより自動設定される。

ちなみに、アプリバージョンは 0.8.1 とする。(Edit - Project Settings - Other Settings - Version)

AAS_3

AAS_12

AAS_13

 

4. リモートアセットの設定(アセット側)

AAS_14

リモートアセットにしたい Group に対して、以下の設定を行う

  • Build Path を RemoteBuildPath に変更
  • Load Path を RemoteLoadPath に変更

5. ローカルアセットの設定

AAS_15

必須ではないが、リモートアセットではないアセットは「Cannot Change Post Release」に変更しておく。

リモートアセットを更新する際の確認がやりやすくなるため。(後述)

6. アセットビルド

AAS_7

Build - New Build - Default Build Script でアセットビルドする。

ローカルアセットもリモートアセットも両方がビルドされる。

スプライトアトラスの修正がうまく反映されない場合は、Clean Build - All してからアセットビルド。

Develop版であれば ServerData/Develop/Android フォルダにリモートアセットが出力される。

このときに Assets/AddressableAssetsData/Develop/Android に addressables_content_state.bin なる全アセットのカタログが生成されるので、コピーして addressables_content_state_0_8_1_Develop.bin として同階層に保存しておく。

リモートアセットのみ更新する際の確認時に、addressables_content_state_0_8_1_Develop.bin を指定しての差分チェックを行うため。(Update a Previous Build)

ちなみに、addressables_content_state.bin はエディタで上記目的で使用されるので、該当ファイルを削除してもエディタ実行や実機実行には影響はない。

7. リモートアセットを開発サーバーにアップロード

ServerData/Develop フォルダを、開発サーバーにアップロードする。

ローカルのディレクトリは以下のようになる。

AAS_17

また、サーバーのディレクトリは以下のようになる。(ただし、本番サーバーは Productフォルダのみ)

AAS_16

8. カタログ内の本番サーバーURLをランタイムで開発サーバーURLに置換

エディタスクリプトの RemoteLoadPath の設定のとおり、エディタや実機での実行時に参照するカタログ内には本番サーバーURLが記載されている。

Develop版 + 開発サーバー、Product版 + 開発サーバー の場合は、ランタイムで開発サーバーURLに置換する。

スクリプトは以下。(参考:TransformInternalId

void Start()
{
    // キャッシュ削除テスト
    //Caching.ClearCache();

    // リモートアセットのURLをカスタマイズ
    Addressables.ResourceManager.InternalIdTransformFunc = TransformFunc;
}

string TransformFunc(IResourceLocation location)
{
    // ビルドターゲットと同じターゲットのリモートカタログを参照していることを保障
    if (location.PrimaryKey == "AddressablesMainContentCatalogRemoteHash")
    {
#if DEVELOP
        Assert.IsTrue(location.InternalId.Contains("Develop") && !location.InternalId.Contains("Product"));
#else
        Assert.IsTrue(!location.InternalId.Contains("Develop") && location.InternalId.Contains("Product"));
#endif
    }

    // Product版 + 本番サーバー 以外では開発サーバーURLに置換して、リソースロード先をカスタマイズする
    string ret = string.Empty;
#if DEVELOP
    ret = location.InternalId.Replace(cServerUrl_Asset, cServerUrl_Asset_Staging);
#else
    if (cIsProductServer)
    {
        ret = location.InternalId;
    }
    else
    {
        ret = location.InternalId.Replace(cServerUrl_Asset, cServerUrl_Asset_Staging);
    }
#endif

    // デバッグ用にログ出力しておく
    Debug.Log($"ResId: {ret}");
    return ret;
}

// 接続先サーバー
private static readonly string cServerUrl_Asset = "本番サーバーURL";
#if DEVELOP
private static readonly string cServerUrl_Asset_Staging = "開発サーバーURL";
#else
private static readonly bool cIsProductServer = false;
private static readonly string cServerUrl_Asset_Staging = "開発サーバーURL";

// 本番サーバーにアクセスするアプリを作成する場合に以下を有効にする
//private static readonly bool cIsProductServer = true;
//private static readonly string cServerUrl_Asset_Staging = "";
#endif

9. Develop版の動作確認

エディタや実機で動作確認する。

エディタでの確認時は「Use Existing Build」にチェックを入れて確認する。

Windows PC で Android アセットを確認する際はテクスチャがピンクになるが、リモートアセットのロード確認などはそれで充分。

AAS_9

10. Product版の動作確認

Product版に切り替えて、Develop は Product と読み替えて 手順6, 手順7 を行う。

11. Product版リモートアセットを本番サーバーにアップロード

ローカルの ServerData/Product(開発サーバーのProductフォルダと同じもの)を本番サーバーにアップロードする。(慎重に)

12. 本番サーバーにアクセスするProduct版アプリをビルド

手順8 のスクリプトの cIsProductServer = true を有効にして、アプリビルド。

これがユーザーに配布する製品版となる。

コードからもわかるように、開発サーバーURLの情報は一切含まれていない。

アプリバージョンを更新する場合

前回のアプリ(apk, ipa)ビルド時点のカタログからの差分などを気にしなくてよいので、ラク。

手順6 のように今回のアプリビルドを起点とする方法でOK。

サーバーにアップロード済の旧アセットは全て削除して、今回作成したものと入れ替える。

今回のアプリバージョンを 0.8.2 とすると、catalog_0_8_2_Product.json が作られるが、アセットに変化のないものは同じアセットバンドル名(例:param_assets_all_un239saa23eacc594e49e25ab0e5da8d.bundle)になるので、実機でダウンロード済であれば再ダウンロードは発生しない。(AAS のキャッシュ機構)

リモートアセットのみ更新する場合

1. リモートアセットビルド前の確認

アプリバージョンは 0.8.1 のまま、リモートアセットのみを更新する場合。

まずは、対象となるリモートアセットに変更を加える。

AAS_20

アセットビルド前に、上記のように「Check for Content Update Restrictions」で 0.8.1 時点のカタログからの差分が正しいかをチェックする。

クリックすると addressables_content_state.bin を求められるので、保存しておいた addressables_content_state_0_8_1_Develop.bin を指定。

リモートアセットのみの修正になっており、「Cannot Change Post Release」にしたローカルアセットに変更がなければ、以下の出力になり、チェックOKとなる。

AAS_19

ローカルアセットにも修正が入ってしまっている場合は、無視していいのか、リモートアセットにする必要があるのか、各ケースで考慮する。

2. リモートアセットをビルド

AAS_18

「Update a Previous Build」でリモートアセットのみをビルドできる。

クリックすると addressables_content_state.bin を求められるので、保存しておいた addressables_content_state_0_8_1_Develop.bin を指定。

catalog_0_8_1_Develop.hash, catalog_0_8_1_Develop.json, 変更したリモートアセットバンドル が出力されるので、それらを開発サーバーにアップロードする。

このビルドでは、旧リモートアセットバンドルは残りつつ新リモートアセットバンドルが追加されるので、リモートアセット更新回数だけアセットバンドルが増えていくことになる。

実機のストレージは AAS が旧アセットと新アセットを差し替えてくれるので問題ないが、手元の開発環境やサーバーにはアセットバンドルが増えていくことになる。

(個人開発など更新回数が少ない場合、更新したことの目印になるので、旧アセットは残しておいてもよいと思う。)

(そして、アプリバージョン更新のタイミングで、旧アセットを全て削除して、新アセットのみにする。)

備考

  • 上記は、アプリバージョンやアセットバージョンによらず、本番サーバーの同じディレクトリを使用するケース
    • 業務で AAS を使う場合は、エディタスクリプトの BuildPath, LoadPath にアプリバージョンとアセットバージョンを含めて、以下のようなサーバーディレクトリにするのがよさそう
      • data_0 は アセットバージョン 0 の意味。
      • 2d7daa などの接尾語は適当なハッシュ値。本番サーバーアップロードからユーザープレイまでに時間差がある場合に、フォルダ名を推測して暴露されないための細工
    • AAS_21
  • AAS はお世辞にもわかりやすいとは言えないので、トラブルはつきもの。間違いに気付ける仕組みを入れておくのが大事
    • 例えば、手順8 の Assert.IsTrue(resourceLocation.InternalId.Contains("Develop") && !resourceLocation.InternalId.Contains("Product")); など
    • 手順8 の Caching.ClearCache(); も有効
      • AAS はキャッシュ機能があるので、ロード成功後にスクリプト修正して間違ったリモートURLを埋め込んだとしてもうまくいってしまう
      • スクリプト修正直後の実行では Caching.ClearCache(); を有効にしておくことで間違いに気付ける
    • 手順8 の Debug.Log($"ResId: {ret}"); も有効
      • リソースロードの際にアクセス先がわかるようにログ出力しておく
      • cIsProductServer = true のときは Debug.unityLogger.logEnabled = false にするのをお忘れなく
  • 本番サーバーURLはバレてもいいが(製品に入るので)、開発サーバーURLはバレてはいけない
    • よって、エディタスクリプトの RemoteLoadPath には本番サーバーを記載することでカタログには本番サーバーURLを埋め込んでおき、開発時はランタイムで開発サーバーURLに差し替えている
  • Addressable.LibraryPath もDevelop/Productで分けたかったがうまくいかなかった
    • エディタスクリプトとランタイムの両方で設定したが、Library\com.unity.addressables 直下にビルド成果物が出力されてしまうことがあったのでやめた
    • なので、Develop/Product切り替え時は手動でアセットビルドも行う手順になった

-ブログ

© 2022 墾田ええねん! Powered by AFFINGER5