Why not login to Qiita and try out its useful features?

We'll deliver articles that match you.

You can read useful information later.

4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

フリューAdvent Calendar 2024

Day 9

UnityのAddressableアセットをビルドしてからCloudflare R2へとアップロードするエディタ拡張を作成する

Last updated at Posted at 2024-12-08

前置き

アセットのビルドを行ったとき、そのアセットで動作確認をしたいことがあります。その場合、ビルドしたら自動でアップロードしてくれるとありがたいですよね。

ということで、今回はAddressableAssetsSystem(以降AAS)のアセットをビルドして、その後そのままCloudflare R2(以降R2)へとアップロードするためのエディタ拡張を作成したいと思います。

R2はエグレス料金が無料となっておりますので、ユースケースにあうのであればなるべく使っていきたいと思い、検証のためにも採用しました。

なお、R2を使うにはサブスクリプション登録のためにクレジットカードなど支払い方法が必要なる点にご注意ください。ただし、サブスクリプション登録をしても実際に請求されるかどうかは使用量次第になります。
(今回の動作確認の範囲内であればほぼほぼ請求されないとは思いますが、Cloudflareのダッシュボードを確認しましょう。)

対象

  • AASを使ったことがある方
    • 基本的な紹介は省くので「導入記事とかは読んだ」以降のレベル感です
  • R2に興味がある方
    • 「S3や他のサービスは使っている」という方も参考にどうぞ

やること

  1. R2のセットアップ
  2. スクリプトからAASのビルドを実行する処理を追加
  3. スクリプトからR2へのアップロードを行う処理を追加
  4. ビルド処理とアップロード処理をまとめる

1. R2のセットアップ

アカウント作成処理は省略します

1. R2の支払いの設定

  • すいません。スクショ撮影し忘れました。
  • 必要な支払い情報を入力するだけだったかと思います。

2. バケットを作成

CloudflareのダッシュボードよりR2 オブジェクトストレージ概要から作成できます

バケット名はなんでもよいので適当に入力してバケットを作成するを選びましょう

3. R2 APIトークンを作成

R2 オブジェクトストレージ概要からR2 API トークンの管理から作成できます

トークン名はなんでもよいです。今回は管理者用の設定にしています。TTLは好きなものでよいです。今回は記事用なので適当に1週間(で書けるだろうというてい)にしておきました。

作成後の画面にて表示されるアクセスキーIDシークレットアクセスキーエンドポイントは後ほど使用しますので覚えておいてください。また、このとき表示される情報は1回限りなのでご注意ください。(とはいえ、メモするのを忘れたら消して作り直せばOKです)

これでR2へのアップロードを行う準備が整いました。

2. スクリプトからAASのビルドを実行する処理を追加

ビルド処理自体は単純です。

AddressableAssetSettings.BuildPlayerContent(out var result);

上記のコードを実行することでAddressableAssetSettingsBuild & Load Pathsに指定されているパスへと出力されます。(Remoteに指定している場合はServerData/[BuildTarget]と、デフォルトで入力されているかと思います。)

スクリーンショット 2024-12-04 14.26.04.png

上記の図の場合[ProjectHome]/ServerData/[BuildTarget]以下にアセットが出力されるようになります。
(図の場合[BuildTarget]の部分がStandaloneOSXとなっていますが、これは自動的に今のプラットフォームに合わせて表示が切り替えて表示されているからです)

なお、ContentStateDataを使用した更新の場合は下記の通りです。

ContentUpdateScript.BuildContentUpdate(AddressableAssetSettingsDefaultObject.Settings, "addressables_content_state.bin");

addressables_content_state.binには前回実行時に生成されているはずのファイルへのパスを指定してください。

ちなみに、実行するにはAddressablesのパッケージが必要ですので、Package ManagerよりAddressablesをインストールしておきましょう。(今更感ありますが)

3. スクリプトからR2へのアップロードを行う処理を追加

AWS SDK for .NETを使用してアップロードを行います。
「ん?AWS?R2では?」となりますが、R2はAWSのS3互換のAPIを有しているためAWS SDKのS3用の処理を利用して操作を行うことが出来ます。

1. AWS SDK for .NETのインストール

1-a. NuGetForUnityのインストール

DLLの用意が面倒なので、NuGetForUnityを使用します。
インストール手順は公式githubのインストール方法をご確認ください。
以下はInstall as GIT dependency via Package Managerを使うパターンの例です。

1-b. AWSSDK.S3のインストール

1.NuGetを選択します

2.NuGet For UnityのウィンドウよりAWSSDK.S3SearchしてInstallをします

3.正常に完了するとInstalledとなります。

2. アップロードを行うEditor拡張用のクラスを作成

  1. UnityプロジェクトディレクトリのAssetsディレクトリ配下の任意の場所にEditorディレクトリを作成します
  2. 作成したEditorディレクトリ以下に任意の名前のC#ファイルを作成します(今回は仮にAssetBuildAndUploader.csとします)
  3. 以下のコードを追加します(このソースの説明は次で解説します)
アップロード処理のサンプル
using System;
using System.IO;
using System.Threading.Tasks;
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
using UnityEditor;
using UnityEngine;
using Directory = System.IO.Directory;
using File = System.IO.File;

public class AssetBuildAndUploader 
{
    // R2への接続設定がかかれたJSONへのパス
    private static string R2_CONFIG_JSON_PATH => Application.dataPath + "/../r2_config.json";
    // ビルドされたアセットの出力先
    private static string ServerAssetsPath => Application.dataPath + "/../ServerData/";

    // R2への接続設定がかかれたJSONに対応するObject
    [Serializable]
    class R2Config 
    {

        public string accessKey;  // R2 APIのアクセスキー
        public string secretAccessKey; // R2 APIのシークレット
        public string r2Url;  // R2へのエンドポイント
        public string bucketName; // アップロード先のBucket名

    }
    
    [MenuItem("MyTools/AAS Upload To R2")]
    public static async void UploadAssets() 
    {
        Debug.Log("UploadAssets Start.");
        // 出力先のディレクトリにあるアセットを全て取得してアップロードする
        var files = Directory.GetFiles(ServerAssetsPath, "*.*", SearchOption.AllDirectories);
        foreach (var file in files) 
        {
            if (file.EndsWith(".DS_Store") || file.EndsWith("Thumbs.db")) 
            {
                continue; // OS固有の不要なファイルは無視する
            }
            var fileInfo = new FileInfo(file);
            // 保存先のキーは出力先の親ディレクトリの名前を全て除外したものにする
            var directoryName = Path.GetDirectoryName(file).Replace(ServerAssetsPath, "");
            Debug.Log($"Local: {file} ---> Remote: {directoryName}/{fileInfo.Name}");
            // 実際のアップロード処理
            await UploadToR2(file, $"{directoryName}/{fileInfo.Name}");
        }
        Debug.Log("UploadAssets Finished.");
    }

    private static async Task UploadToR2(string filePath, string keyName) 
    {
        // アップロード先のR2の情報を読み込み
        var r2Config = JsonUtility.FromJson<R2Config>
        (
            await File.ReadAllTextAsync(R2_CONFIG_JSON_PATH)
        );
        // R2のアップロード処理はS3と互換性があるのでS3Clientを利用する
        var s3 = new AmazonS3Client
        (
            new BasicAWSCredentials(r2Config.accessKey, r2Config.secretAccessKey),
            new AmazonS3Config 
            {
                ServiceURL = r2Config.r2Url
            }
        );
        try 
        {
            // 指定されたファイルをアップロード
            await s3.PutObjectAsync(new PutObjectRequest 
            {
                BucketName = r2Config.bucketName,
                Key = keyName,
                InputStream = new FileStream(filePath, FileMode.Open),
                DisablePayloadSigning = true
            });
        }
        catch (AmazonS3Exception e) 
        {
            Debug.Log($"e: {e.Message}");
        }
    }
       
}
4. `r2_config.json`という名前のファイルをプロジェクト直下に配置します。内容はサンプルを参照してください。
r2_config.jsonのサンプル
{
	"accessKey": "XXXXX(ここにR2 APIのアクセスキーID)",
	"secretAccessKey": "YYYYY(ここにR2 APIのシークレットアクセスキー)",
	"r2Url": "https://[account_id].r2.cloudflarestorage.com(ここにR2 APIのエンドポイント)",
	"bucketName": "your-bucket-name(作成したバケットの名前)"
}
5. Unityのメニューより`MyTools` > `AAS Upload To R2`を実行すると、プロジェクト直下の`ServerData`以下のファイルがR2の指定バケットにアップロードされます

3. アップロード処理の説明

// R2への接続設定がかかれたJSONへのパス
private static string R2_CONFIG_JSON_PATH => Application.dataPath + "/../r2_config.json";

// 〜略〜

// R2への接続設定がかかれたJSONに対応するObject
[Serializable]
class R2Config 
{

    public string accessKey;  // R2 APIのアクセスキー
    public string secretAccessKey; // R2 APIのシークレット
    public string r2Url;  // R2へのエンドポイント
    public string bucketName; // アップロード先のBucket名

}

// 〜略〜

var r2Config = JsonUtility.FromJson<R2Config>
(
    await File.ReadAllTextAsync(R2_CONFIG_JSON_PATH)
);

上記はR2へのアップロード用の署名を保存しておくためのJSONファイルへのパスを定義し、それを読み込んでいます。このJSON仕様は今回の拡張機能用の独自仕様となります。ServiceURLを書き換えるのが面倒だったのでこのようにしています。実運用の際にはファイルの取り扱いにはご注意ください。

// ビルドされたアセットの出力先
private static string ServerAssetsPath => Application.dataPath + "/../ServerData/";

上記はAASのビルド処理にて記載した通り、プロジェクトフォルダ直下のServerData以下に出力されているというていでの設定になります。ここはプロジェクトのAASのビルド後の出力先の設定を元に書き換えてください。

var s3 = new AmazonS3Client
(
    new BasicAWSCredentials(r2Config.accessKey, r2Config.secretAccessKey),
    new AmazonS3Config 
    {
        ServiceURL = r2Config.r2Url    
	}
);

上記のBasicAWSCredentialsにはR2のセットアップにて取得したR2 APIトークンのアクセスキーIDとシークレットアクセスキーを指定しています。また、ServiceURLにはR2のセットアップにて取得したエンドポイントを指定しています。

await s3.PutObjectAsync(new PutObjectRequest 
{
	BucketName = r2Config.bucketName,
	Key = keyName,
	InputStream = new FileStream(filePath, FileMode.Open),
	DisablePayloadSigning = true
});

上記では最終的に指定のファイルパスにあるデータを指定のbucketへとアップロードしています。

BucketNameはアップロード先のバケット名、Keyはバケット以下に配備されるときの名前、InputStreamはアップロードする対象のファイルのStreamです。

DisablePayloadSigningR2では使えない署名を、AWS SDKが使わないようにするため設定しています。これがtrueとなっていない場合STREAMING-AWS4-HMAC-SHA256-PAYLOAD not implementedのエラーが発生しました。

4. ビルド処理とアップロード処理をまとめる

ビルド処理が成功した場合に、アップロード処理を行うようにすればよいでしょう。
単純に、以下のようになるでしょう。

AddressableAssetSettings.BuildPlayerContent(out var result);
if (!string.IsNullOrEmpty(result.Error)) 
{
    Debug.LogError(result.Error);
}
else 
{
    AssetUploader.UploadAssets();
}

まとめ

以下は全体のソースコードになります

全体ソースコードのサンプル
using System;
using System.IO;
using System.Threading.Tasks;
using Amazon.Runtime;
using Amazon.S3;
using Amazon.S3.Model;
using UnityEditor;
using UnityEditor.AddressableAssets.Settings;
using UnityEngine;
using Directory = System.IO.Directory;
using File = System.IO.File;

public class AssetBuildAndUploader 
{
    // R2への接続設定がかかれたJSONへのパス
    private static string R2_CONFIG_JSON_PATH => Application.dataPath + "/../r2_config.json";
    // ビルドされたアセットの出力先
    private static string ServerAssetsPath => Application.dataPath + "/../ServerData/";

    // R2への接続設定がかかれたJSONに対応するObject
    [Serializable]
    class R2Config 
    {

        public string accessKey;  // R2 APIのアクセスキー
        public string secretAccessKey; // R2 APIのシークレット
        public string r2Url;  // R2へのエンドポイント
        public string bucketName; // アップロード先のBucket名

    }

    [MenuItem("MyTools/AAS Build And Upload")]
    private static async void BuildAndUpload() 
    {
        AddressableAssetSettings.BuildPlayerContent(out var result);
        if (!string.IsNullOrEmpty(result.Error)) 
        {
            Debug.LogError(result.Error);
        }
        else 
        {
            await UploadAssets();
        }
    }
    
    private static async Task UploadAssets() 
    {
        Debug.Log("UploadAssets Start.");
        // 出力先のディレクトリにあるアセットを全て取得してアップロードする
        var files = Directory.GetFiles(ServerAssetsPath, "*.*", SearchOption.AllDirectories);
        foreach (var file in files) {
            if (file.EndsWith(".DS_Store") || file.EndsWith("Thumbs.db")) 
            {
                continue; // OS固有の不要なファイルは無視する
            }
            var fileInfo = new FileInfo(file);
            // 保存先のキーは出力先の親ディレクトリの名前を全て除外したものにする
            var directoryName = Path.GetDirectoryName(file).Replace(ServerAssetsPath, "");
            Debug.Log($"Local: {file} ---> Remote: {directoryName}/{fileInfo.Name}");
            // 実際のアップロード処理
            await UploadToR2(file, $"{directoryName}/{fileInfo.Name}");
        }
        Debug.Log("UploadAssets Finished.");
    }

    private static async Task UploadToR2(string filePath, string keyName) 
    {
        // アップロード先のR2の情報を読み込み
        var r2Config = JsonUtility.FromJson<R2Config>
        (
            await File.ReadAllTextAsync(R2_CONFIG_JSON_PATH)
        );
        // R2のアップロード処理はS3と互換性があるのでS3Clientを利用する
        var s3 = new AmazonS3Client
        (
            new BasicAWSCredentials(r2Config.accessKey, r2Config.secretAccessKey),
            new AmazonS3Config 
            {
                ServiceURL = r2Config.r2Url
            }
        );
        try 
        {
            // 指定されたファイルをアップロード
            await s3.PutObjectAsync(new PutObjectRequest 
            {
                BucketName = r2Config.bucketName,
                Key = keyName,
                InputStream = new FileStream(filePath, FileMode.Open),
                DisablePayloadSigning = true
            });
        }
        catch (AmazonS3Exception e) 
        {
            Debug.Log($"e: {e.Message}");
        }
    }
       
}

ビルドしてアップロードを行うメソッドを新たに追加し、MenuItemの属性を追加しています。UnityのメニューよりMyTools > AAS Build And Uploadを実行すると、ビルドしてアップロードが行われるようになります。

なお、今回で作成したR2のバケットは削除しておくと安心かと思います。
(ストレージ容量が超えていたり、外部アクセスをONにしてたりすると勝手に課金されてしまうかもしれないので)

以上となります。

バージョン情報

  • Unity6000.0.25f1
  • Addressables 2.2.2
  • NuGetForUnity 4.1.1
  • AWSSDK.S3 3.7.408.1

補足

R2へは初期設定のままだとURLでアクセスできません。
AASからアクセスするにはR2のバケットをパブリックアクセス(要ドメイン)にするかCloudflare WorkersなどからR2へと経由させる必要があります。

開発中の動作確認用などに関してはr2.dev サブドメインが利用できます。
こちらはバケットの設定からr2.dev サブドメインの項目から設定可能です。

あとはAddressables ProfilesよりRemoteCostumにして、Remote.LoadPathパブリック r2.dev バケットURLを指定すればOKです。

4
0
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
4
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?