LoginSignup
1
2

【Unity】Addressablesでリソースプロジェクトと本体プロジェクトをローカル環境で分ける方法

Last updated at Posted at 2024-05-28

はじめに

  • リソースプロジェクトでアセットバンドルをビルドして、本体プロジェクトではアセットを直接参照しない、というような開発体制に対応できます

  • ゲームでリモートでリソースを取得する必要がない場合などに有用です

    • ゲーム上でリモートでダウンロードする場合、このような処置を取らずに直接ロードするだけでOKです
  • 直接アセットを追加せずリソースを切り分けることで、Gitなどの動作も軽量になります。

対処法

リソースプロジェクトから本体プロジェクトにバンドルファイルとcatalog.jsonをコピーする

  • 結構原始的なやり方ですが、調べた感じほかに手段がなかったので、消去法でこの方法で実装する必要があります

作り方

準備

  • リソースプロジェクトと本体プロジェクトを作成する

    • ディレクトリ構成はこんな感じです
      AddressablesTest
      ├ ResourceProject
      └ MainProject
      
  • Unityバージョン、Addressablesバージョンを揃える

リソースプロジェクト

  • リソースプロジェクト側で、アセットをAddressablesに登録してビルドします

本体プロジェクト

  • 本体プロジェクト側で、エディタ拡張を利用してリソースプロジェクトのバンドル情報をコピーします
  • コードについては後ほど紹介します

結果

実行結果

本体プロジェクト内のAddressables Groupsウィンドウには何も登録されていませんが…
image.png

実行すると、リソースプロジェクト側でGroupsに追加したアセットが適用されます!
AddressablesTest (1).gif

エディタ

こんな感じのエディタウィンドウで操作できます。
ここでバンドルとカタログをコピーすれば、リソースプロジェクトのバンドルデータが含まれます!
image.png

…が、ゲームをビルドすると、自動でバンドルの方のビルド処理がされてしまい、コピーされたバンドルファイルが上書きされてしまいます。

そこで、ビルド後にコピーするという処理が必要になります。
以下のコード説明で紹介します

エディタ拡張コード

CopyBundleFromResourceProject.cs

コード
using System.Collections;
using System.Collections.Generic;
using UnityEngine;
using UnityEditor;
using UnityEditor.AddressableAssets.Build;

/// <summary> リソースプロジェクトからバンドルをコピーする </summary>
public class CopyBundleFromResourceProject : EditorWindow
{
    // 設定ファイル
    [SerializeField] CopyBundleSettings settings;
    [SerializeField] bool isAbsolutePath = false;       // 絶対パスフラグ

    // ビルド時の設定
    [SerializeField] bool isCopyOnBuild = true;

    const string CATALOG_FILE_NAME = "catalog.json";

    const int SPACE_SIZE = 32;

    const string SETTING_FILE_PATH = "Assets/ScriptableObjects/CopyBundleSettings.asset";

    //------------------------------------------------------------
    [MenuItem("Addressable/CopyBundleFromResourceProject")]
    static void ShowWindow()
    {
        var window = GetWindow<CopyBundleFromResourceProject>();
        window.titleContent = new GUIContent("CopyBundleFromResourceProject");
        window.Show();

        Debug.Log("ShowWindow");
    }

    void OnEnable()
    {
        BuildScript.buildCompleted = null;
        BuildScript.buildCompleted += OnBuildCompleted;

        Debug.Log("CopyBundleFromResourceProject");
    }

    void OnGUI()
    {
        // 設定ScriptableObject設定
        EditorGUILayout.LabelField("Copy Bundle Settings", EditorStyles.boldLabel);
        settings = EditorGUILayout.ObjectField("Settings", settings, typeof(CopyBundleSettings), false) as CopyBundleSettings;
        if (settings == null || settings.IsNullValue) return;

        // 絶対パスフラグ
        isAbsolutePath = EditorGUILayout.Toggle("Is Absolute Path", isAbsolutePath);

        EditorGUILayout.Space(SPACE_SIZE);
        EditorGUILayout.LabelField("Build Settings", EditorStyles.boldLabel);
        isCopyOnBuild = EditorGUILayout.Toggle("Copy On Build", isCopyOnBuild);

        EditorGUILayout.Space(SPACE_SIZE);
        EditorGUILayout.LabelField("Copy Bundle", EditorStyles.boldLabel);
        // パス指定
        var sourceBundlePath = settings.GetSourceBundlePath(isAbsolutePath);
        GUILayout.Label("Source Bundle Path: " + sourceBundlePath, EditorStyles.boldLabel);

        var destBundlePath = settings.GetDestinationBundlePath(isAbsolutePath);
        GUILayout.Label("Destination Bundle Path: " + destBundlePath, EditorStyles.boldLabel);

        //------------------------------------------------------------
        // バンドルファイルのコピー
        if (GUILayout.Button("Copy"))
        {
            CopyBundles(sourceBundlePath, destBundlePath);
        }

        //------------------------------------------------------------
        EditorGUILayout.Space(SPACE_SIZE);
        EditorGUILayout.LabelField("Copy Catalog", EditorStyles.boldLabel);

        // カタログパス
        var sourceCatalogPath = settings.GetSourceCatalogPath(isAbsolutePath);
        GUILayout.Label("Source Catalog Path: " + sourceCatalogPath, EditorStyles.boldLabel);

        var destCatalogPath = settings.GetDestinationCatalogPath(isAbsolutePath);
        GUILayout.Label("Destination Catalog Path: " + destCatalogPath, EditorStyles.boldLabel);

        // カタログのコピー
        if (GUILayout.Button("Copy Catalog"))
        {
            CopyCatalog(sourceCatalogPath, destCatalogPath);
        }
    }

    //------------------------------------------------------------
    // ビルド完了時の処理
    static void OnBuildCompleted(AddressableAssetBuildResult result)
    {
        // 設定ファイルをAssetDatabaseで読み込む
        var settings = AssetDatabase.LoadAssetAtPath<CopyBundleSettings>(SETTING_FILE_PATH);

        if (settings == null || settings.IsNullValue)
        {
            throw new System.Exception("Not found settings.");
        }

        CopyBundles(settings.GetSourceBundlePath(false), settings.GetDestinationBundlePath(false));
        CopyCatalog(settings.GetSourceCatalogPath(false), settings.GetDestinationCatalogPath(false));

        Debug.Log("CopyBundleFromResourceProject: OnBuildCompleted");
    }

    //------------------------------------------------------------
    // コピー
    static void CopyBundles(string sourcePath, string destPath)
    {
        try
        {
            // コピー元パスチェック
            if (!System.IO.Directory.Exists(sourcePath))
            {
                Debug.LogError("Not found source directory.: " + sourcePath);
                return;
            }

            // コピー先パス取得
            Debug.Log("Copy: " + sourcePath + " -> " + destPath);

            // 指定されたディレクトリ内のファイルをコピー
            System.IO.Directory.CreateDirectory(destPath);
            foreach (var file in System.IO.Directory.GetFiles(sourcePath))
            {
                var fileName = System.IO.Path.GetFileName(file);
                var targetFile = System.IO.Path.Combine(destPath, fileName);
                System.IO.File.Copy(file, targetFile, true);

                Debug.Log("Copy: " + targetFile);
            }
        }
        catch (System.Exception e)
        {
            Debug.LogError(e);
        }
    }

    // catalog.jsonのコピー
    static void CopyCatalog(string sourceCatalogPath, string destPath)
    {
        try
        {
            // コピー元パスチェック
            if (!System.IO.File.Exists(sourceCatalogPath))
            {
                Debug.LogError("Not found catalog file.: " + sourceCatalogPath);
                return;
            }

            // コピー
            System.IO.Directory.CreateDirectory(System.IO.Path.GetDirectoryName(destPath));
            System.IO.File.Copy(sourceCatalogPath, destPath, true);

            Debug.Log("Copied catalog: " + sourceCatalogPath + " -> " + destPath);
        }

        catch (System.Exception e)
        {
            Debug.LogError(e);
        }

        AssetDatabase.Refresh();
    }
}

設定ScriptableObject

コード
using System.Collections;
using System.Collections.Generic;
using UnityEditor;
using UnityEngine;

[CreateAssetMenu(fileName = "CopyBundleSettings", menuName = "CopyBundleSettings")]
public class CopyBundleSettings : ScriptableObject
{
    [SerializeField] string sourceProjectPath;
    [SerializeField] string catalogDirectoryName;

    const string CATALOG_FILE_NAME = "catalog.json";

    //------------------------------------------------------------
    public bool IsNullValue => string.IsNullOrEmpty(sourceProjectPath) || string.IsNullOrEmpty(catalogDirectoryName);

    //------------------------------------------------------------
    /// <summary> コピー元のバンドルパスを取得 </summary>
    public string GetSourceBundlePath(bool isAbsolutePath, bool checkExist = false)
    {
        var completelyPath = System.IO.Path.Combine(sourceProjectPath, UnityEngine.AddressableAssets.Addressables.RuntimePath, EditorUserBuildSettings.activeBuildTarget.ToString());

        GetPathProcess(ref completelyPath, isAbsolutePath, checkExist);

        return completelyPath;
    }

    /// <summary> コピー先のバンドルパスを取得 </summary>
    public string GetDestinationBundlePath(bool isAbsolutePath, bool checkExist = false)
    {
        var completelyPath = System.IO.Path.Combine(UnityEngine.AddressableAssets.Addressables.RuntimePath, EditorUserBuildSettings.activeBuildTarget.ToString());

        GetPathProcess(ref completelyPath, isAbsolutePath, checkExist);

        return completelyPath;
    }

    /// <summary> コピー元のカタログパスを取得 </summary>
    public string GetSourceCatalogPath(bool isAbsolutePath, bool checkExist = false)
    {
        var targetPath = System.IO.Path.Combine(sourceProjectPath, UnityEngine.AddressableAssets.Addressables.RuntimePath, CATALOG_FILE_NAME);

        GetPathProcess(ref targetPath, isAbsolutePath, checkExist);

        return targetPath;
    }

    /// <summary> コピー先のカタログパスを取得 </summary>
    public string GetDestinationCatalogPath(bool isAbsolutePath, bool checkExist = false)
    {
        var targetPath = System.IO.Path.Combine(System.IO.Path.GetRelativePath(Application.dataPath, Application.streamingAssetsPath), catalogDirectoryName, CATALOG_FILE_NAME);


        GetPathProcess(ref targetPath, isAbsolutePath, checkExist);

        return targetPath;
    }

    //------------------------------------------------------------
    void GetPathProcess(ref string path, bool isAbsolutePath, bool checkExist = false)
    {
        if (checkExist && !System.IO.Directory.Exists(path))
        {
            throw new System.Exception("Not Found Directory: " + path);
        }

        if (isAbsolutePath)
        {
            SetAbsolutePath(ref path);
        }
    }

    void SetAbsolutePath(ref string path)
    {
        path = System.IO.Path.GetFullPath(path);
    }
}

アセット読み込み処理

コード
using System.Collections;
using System.Collections.Generic;
using System.IO;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.UI;

public class AssetLoader : MonoBehaviour
{
    [SerializeField] Image image;

    [Header("Catalog")]
    [SerializeField] string catalogPath = "Addressables/catalog.json";
    [SerializeField] bool loadCatalog;

    async void Start()
    {
        if (loadCatalog)
        {
            // カタログ追加
            var path = Path.Combine(Application.streamingAssetsPath, catalogPath);

            var catalog = Addressables.LoadContentCatalogAsync(path).Task;
            await catalog;

            if (catalog.Status == System.Threading.Tasks.TaskStatus.RanToCompletion)
            {
                Debug.Log("Catalog loaded successfully");
            }

            Addressables.AddResourceLocator(catalog.Result);
        }

        // アセット読み込み
        var spriteHandler = Addressables.LoadAssetAsync<Sprite>("UnityLogo");
        await spriteHandler.Task;

        image.sprite = spriteHandler.Result;
    }
}

説明

ビルド完了時にコピー処理を走らせる

    // ビルド完了時の処理
    static void OnBuildCompleted(AddressableAssetBuildResult result)
    {
        // 設定ファイルをAssetDatabaseで読み込む
        var settings = AssetDatabase.LoadAssetAtPath<CopyBundleSettings>(SETTING_FILE_PATH);

        if (settings == null || settings.IsNullValue)
        {
            throw new System.Exception("Not found settings.");
        }

        CopyBundles(settings.GetSourceBundlePath(false), settings.GetDestinationBundlePath(false));
        CopyCatalog(settings.GetSourceCatalogPath(false), settings.GetDestinationCatalogPath(false));

        Debug.Log("CopyBundleFromResourceProject: OnBuildCompleted");
    }
    void OnEnable()
    {
        BuildScript.buildCompleted = null;
        BuildScript.buildCompleted += OnBuildCompleted;
    }
  • AssetDatabaseでファイルパスなどが指定された設定用ScriptableObject(CopyBundleSettings)を直接読み込み、パスなどの設定を取得してます
  • 設定ファイルがなかったり、値が設定されていなかったら警告を出します
  • 問題がなければバンドルとカタログをコピーします
  • この処理をOnEnableのタイミングで、BuildScript.buildCompletedイベントに登録します

利用側でカタログを登録して読み込む

カタログ追加
if (loadCatalog)
{
    // カタログ追加
    var path = Path.Combine(Application.streamingAssetsPath, catalogPath);

    var catalog = Addressables.LoadContentCatalogAsync(path).Task;
    await catalog;

    if (catalog.Status == System.Threading.Tasks.TaskStatus.RanToCompletion)
    {
        Debug.Log("Catalog loaded successfully");
    }

    Addressables.AddResourceLocator(catalog.Result);
}
  • StreamingAssetsに配置されたカタログを、Addressables.LoadContentCatalogAsync()で読み込みます
  • 読み込んだ後に、Addressables.AddResourceLocator()でカタログを本プロジェクトに追加します
アセット読み込み
// アセット読み込み
var spriteHandler = Addressables.LoadAssetAsync<Sprite>("UnityLogo");
await spriteHandler.Task;

image.sprite = spriteHandler.Result;
  • 読み込み時のキーは、リソースプロジェクト側で設定されたものを指定する必要があります

image.png

さいごに

こんな感じでプロジェクトを分けて、エディタ上の無駄な待機時間などを短縮しましょう~

参考

1
2
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
2