yotiky Tech Blog

とあるエンジニアの備忘録

Azure API Management

API Management (APIM) は、既存のバックエンドのサービスに対して一貫性のある最新の API ゲートウェイを迅速に作成する手段です。

API Management が組織にもたらす利点は、外部のパートナーや社内の開発者に API を公開することによって、社内に眠っているデータやサービスの可能性を発掘できることです。 どの企業も、その業務をデジタル プラットフォームで拡大し、新しい販路と顧客を開拓すると共に、既存の顧客との絆を深めようと模索しています。 API Management は、開発者の取り組み、ビジネス インサイト、分析、セキュリティ、保護を通じて API プログラムの価値を高め、企業にコア コンピテンシーをもたらします。 Azure API Management を任意のバックエンドで実行し、それに基づいて本格的な API プログラムを起動できます。

Azure Functions で Azure Cosmos DB (Table) を操作する

目次

検証環境

  • Azure Functions v3
  • Microsoft.Azure.Cosmos.Table v1.0.8

古いライブラリに注意

  • WindowsAzure.Storage は非推奨
  • Microsoft.Azure.CosmosDB.Table はまもなく非推奨

実装

NuGet でライブラリをインストールする。

using Microsoft.Azure.Cosmos.Table;

データ定義は、TableEntity を継承したクラスを作成する。 取得時にパラメータなしのコンストラクタが必要になる。

public class CustomerEntity : TableEntity
{
    public CustomerEntity() { }
    public CustomerEntity(string lastName, string firstName)
    {
        PartitionKey = lastName;
        RowKey = firstName;
    }

    public string Email { get; set; }
    public string PhoneNumber { get; set; }
}

CloudTableインスタンスを取得する。

var connectionString = "ConnectionString";
var tableName = "TableA";

var storageAccount = CloudStorageAccount.Parse(connectionString);
var tableClient = storageAccount.CreateCloudTableClient();
var cloudTalbe = tableClient.GetTableReference(tableName);

テーブル作成

// なければ作成する
await table.CreateIfNotExistsAsync(IndexingMode.Consistent, throughput: 400, null, CancellationToken.None);

プロビジョニングされたスループットの場合、throughput を指定せずに作成すると 800 で作成されてしまうので注意。最低コストの2倍発生する。 IndexingModedefaultTimeToLive はこの指定で手動で作成した時のデフォルト値と同じになる。

テーブルに設定されたスループットは、ポータル上の下図から確認できる。 f:id:yotiky:20210227183357p:plain

挿入

1件挿入する場合。

var entity = new CustomerEntity("Tanaka", "Taro")
{ 
    Email = "aaa", 
    PhoneNumber = "001"
};

await cloudTalbe.ExecuteAsync(TableOperation.InsertOrReplace(entity));

まとめて挿入する場合。

var entities = new[]
{
    new CustomerEntity("Tanaka", "Taro"){ Email = "aaa", PhoneNumber = "001"},
    new CustomerEntity("Tanaka", "Jiro"){ Email = "bbb", PhoneNumber = "002"},
};

var operation = new TableBatchOperation();
foreach (var entity in entities)
{
    operation.Add(TableOperation.InsertOrReplace(entity));
}

await cloudTalbe.ExecuteBatchAsync(operation);

取得

PartitionKeyとRowKeyを指定して1件取得する場合。

var entity = new CustomerEntity("Tanaka", "Taro")
{ 
    Email = "aaa", 
    PhoneNumber = "001"
};

var tableResult = await cloudTalbe.ExecuteAsync(TableOperation.Retrieve<CustomerEntity>(entity.PartitionKey, entity.RowKey));
var result = tableResult.Result as CustomerEntity;

クエリを使う場合。

var entity = new CustomerEntity("Tanaka", "Taro")
{ 
    Email = "aaa", 
    PhoneNumber = "001"
};


var query = new TableQuery<CustomerEntity>();
query.FilterString = TableQuery.CombineFilters(
    TableQuery.GenerateFilterCondition(nameof(CustomerEntity.PartitionKey), QueryComparisons.Equal, entity.PartitionKey),
    TableOperators.And,
    TableQuery.GenerateFilterCondition(nameof(CustomerEntity.RowKey), QueryComparisons.Equal, entity.RowKey));

var tableResult = await cloudTalbe.ExecuteQuerySegmentedAsync(query, null);
var results = tableResult.Results;

ソートしたい場合。

var where = TableQuery.GenerateFilterCondition(nameof(CustomerEntity.PartitionKey), QueryComparisons.Equal, entity.PartitionKey);

var query = new TableQuery<CustomerEntity>()
    .Where(where)
    .Order(nameof(CustomerEntity.Timestamp));

var tableResult = await cloudTalbe.ExecuteQuerySegmentedAsync(query, null);

cloudTalbe.CreateQuery<CustomerEntity>() を使おうとすると NotSupportedException が発生するので注意*1

削除

取得時の Entity が書き換わってないかチェックして削除する場合。

await cloudTalbe.ExecuteAsync(TableOperation.Delete(entity));

気にせず削除する場合は ETag にワイルドカードを使う。

var entity = new CustomerEntity("Tanaka", "Taro");
entity.ETag = "*";
await cloudTalbe.ExecuteAsync(TableOperation.Delete(entity));

補足

パフォーマンスや料金の基準となる RU だが、クエリでの使用量を確認するにはいくつかの方法がある。 ただ Table API で用意されているのは今の所 SDK を使う方法しかない。

CosmosDBのリソース開いて Insights(プレビュー) で統計情報は見れる。(Appliction Insights ではない)

f:id:yotiky:20210327175828p:plain:w300

参考

関連記事

Azure Functions で Azure Queue Storage を操作する

目次

検証環境

  • Azure Functions v3
  • Azure.Storage.Queues v12.6.0

実装

NuGet でライブラリをインストールする。

using Azure.Storage.Queues;
using Azure.Storage.Queues.Models;

キューの作成

var connectionString = "ConnectionString";
var queueName = "QueueName”;

var queueClient = new QueueClient(connectionString, queueName);

await queueClient.CreateAsync();

メッセージの追加

await queueClient.SendMessageAsync("First");
await queueClient.SendMessageAsync("Second");

// 最近はBinaryも追加できるらしい
var binary = new BinaryData(new byte[0]);
await queueClient.SendMessageAsync(binary);

メッセージの表示

Azure.Response でラップされてるので Value プロパティを参照する。

var message = await queueClient.PeekMessageAsync();
log.LogInformation(message.Value.MessageId + ":" + message.Value.MessageText);

var messages = await queueClient.PeekMessagesAsync(maxMessages:5);
foreach (var msg in messages.Value)
{
    log.LogInformation(msg.MessageId + ":" + msg.MessageText);
}

メッセージの更新

キューは積み直しになる。 ReceiveMessageAsync ではなく SendMessageAsync の戻り値を使って更新しても同じ。

var receipt = await queueClient.ReceiveMessageAsync();
await queueClient.UpdateMessageAsync(receipt.Value.MessageId, receipt.Value.PopReceipt, "Updated");

メッセージの受信

受信するとキューからは削除される。 Azure.Response でラップされてるので Value プロパティを参照する。

var message = await queueClient.ReceiveMessageAsync();
log.LogInformation(message.Value.MessageId + ":" + message.Value.MessageText);

複数件受信する場合。

var messages = await queueClient.ReceiveMessagesAsync(maxMessages: 5);
foreach (var msg in messages.Value)
{
    log.LogInformation(msg.MessageId + ":" + msg.MessageText);
}

メッセージの削除

ReceiveMessageAsync で消えてしまうので、PeekedMessagePopReceipt が取れないので使いみちあるのか謎。 SendMessageAsync 時に取り消すくらいには使えるかもしれない。

var message = await queueClient.ReceiveMessageAsync();
await queueClient.DeleteMessageAsync(message.Value.MessageId, message.Value.PopReceipt);

キューの削除

// キューが存在しない場合は例外発生
await queueClient.DeleteAsync();

// キューが存在しない場合に例外が出ない
await queueClient.DeleteIfExistsAsync();

その他

メッセージのエンコード

Azure Functions のキュートリガーが文字列を受け取る場合、Base64エンコードされた値が想定されている。エンコードされていない値だと例外が発生する。 そのためキューに追加する時に、Base64エンコードした文字列を設定する必要がある。

SDK を使って文字列を追加する場合、SDK の v12 と v11 以前では動作が異なるので注意が必要。 v11 以前では自動的にエンコードされていたが、v12 ではエンコードされなくなったため明示的にエンコードする。

関連記事

Azure Functions で Azure Table Storage を操作する

目次

検証環境

  • Azure Functions v3
  • Microsoft.Azure.Cosmos.Table v1.0.8

古いライブラリに注意

  • WindowsAzure.Storage は非推奨
  • Microsoft.Azure.CosmosDB.Table はまもなく非推奨

実装

NuGet でライブラリをインストールする。

using Microsoft.Azure.Cosmos.Table;

データ定義は、TableEntity を継承したクラスを作成する。 取得時にパラメータなしのコンストラクタが必要になる。

public class CustomerEntity : TableEntity
{
    public CustomerEntity() { }
    public CustomerEntity(string lastName, string firstName)
    {
        PartitionKey = lastName;
        RowKey = firstName;
    }

    public string Email { get; set; }
    public string PhoneNumber { get; set; }
}

CloudTableインスタンスを取得する。

var connectionString = "ConnectionString";
var tableName = "TableA";

var storageAccount = CloudStorageAccount.Parse(connectionString);
var tableClient = storageAccount.CreateCloudTableClient();
var cloudTalbe = tableClient.GetTableReference(tableName);

// なければ作成する
await cloudTalbe.CreateIfNotExistsAsync();

挿入

1件挿入する場合。

var entity = new CustomerEntity("Tanaka", "Taro")
{ 
    Email = "aaa", 
    PhoneNumber = "001"
};

await cloudTalbe.ExecuteAsync(TableOperation.InsertOrReplace(entity));

まとめて挿入する場合。

var entities = new[]
{
    new CustomerEntity("Tanaka", "Taro"){ Email = "aaa", PhoneNumber = "001"},
    new CustomerEntity("Tanaka", "Jiro"){ Email = "bbb", PhoneNumber = "002"},
};

var operation = new TableBatchOperation();
foreach (var entity in entities)
{
    operation.Add(TableOperation.InsertOrReplace(entity));
}

await cloudTalbe.ExecuteBatchAsync(operation);

取得

PartitionKeyとRowKeyを指定して1件取得する場合。

var entity = new CustomerEntity("Tanaka", "Taro")
{ 
    Email = "aaa", 
    PhoneNumber = "001"
};

var tableResult = await cloudTalbe.ExecuteAsync(TableOperation.Retrieve<CustomerEntity>(entity.PartitionKey, entity.RowKey));
var result = tableResult.Result as CustomerEntity;

クエリを使う場合。

var entity = new CustomerEntity("Tanaka", "Taro")
{ 
    Email = "aaa", 
    PhoneNumber = "001"
};

var query = new TableQuery<CustomerEntity>();
query.FilterString =
    TableQuery.CombineFilters(
        TableQuery.GenerateFilterCondition(nameof(CustomerEntity.PartitionKey), QueryComparisons.Equal, entity.PartitionKey),
        TableOperators.And,
        TableQuery.GenerateFilterCondition(nameof(CustomerEntity.RowKey), QueryComparisons.Equal, entity.RowKey)
    );

var tableResult = await cloudTalbe.ExecuteQuerySegmentedAsync(query, null);

var result = tableResult.Results.SingleOrDefault();

大量に取得する場合。

var entity = new CustomerEntity("Tanaka", "Taro")
{ 
    Email = "aaa", 
    PhoneNumber = "001"
};

var query = new TableQuery<CustomerEntity>();
query.FilterString = 
    TableQuery.GenerateFilterCondition(nameof(CustomerEntity.PartitionKey), QueryComparisons.Equal, entity.PartitionKey);

var list = new List<CustomerEntity>();

TableContinuationToken token = null;
do
{
    var segment = await cloudTalbe.ExecuteQuerySegmentedAsync(query, token);
    list.AddRange(segment.Results);
    token = segment.ContinuationToken;
} while (token != null);

参考:1 件のクエリで大量のエンティティを取得する

削除

取得時の Entity が書き換わってないかチェックして削除する場合。

await cloudTalbe.ExecuteAsync(TableOperation.Delete(entity));

気にせず削除する場合は ETag にワイルドカードを使う。

var entity = new CustomerEntity("Tanaka", "Taro");
entity.ETag = "*";
await cloudTalbe.ExecuteAsync(TableOperation.Delete(entity));

参考

関連記事

Azure Functions で Azure Blob Storage にファイルを保存する

目次

検証環境

  • Azure Functions v3
  • Azure.Storage.Blobs v12.8.0

実装

Azure Functions のプロジェクトに NuGet で「Azure.Storage.Blobs」をインストールする。

using Azure.Storage.Blobs;
using Azure.Storage.Blobs.Models;
private static string connectionString = "YOUR CONNECTION STRING";
private static string blobContainerName = "CONTAINER_NAME";
private static string blobName = "BLOB_NAME";

private static async Task Upload(HttpRequest req)
{
    var blobServiceClient = new BlobServiceClient(connectionString);
    var blobContainerClient = blobServiceClient.GetBlobContainerClient(blobContainerName );
    await blobContainerClient.CreateIfNotExistsAsync(PublicAccessType.None);
    var blobClient = blobContainerClient.GetBlobClient(blobName);
    
    await blobClient.UploadAsync(req.Body);
}

関連記事

Azure Functions で Shared Access Signatures (SAS) を発行する

目次

検証環境

  • Azure Functions v3
  • Azure Storage Blobs v12.8.0

概要

Shared Access Signatures (SAS) は、リソースへのアクセス権に制限を付けてトークンを生成し、Shared Access Signatures URI を使ってリソースへのアクセスを許可する。

SASの種類は3種類。

  • ユーザー委任 SAS
  • サービス SAS
  • アカウント SAS

制御できる項目。

  • アクセスできるリソース
  • 書き込み、読み取りなどのアクセス許可
  • トークンの有効期限
  • IP制限

生成するには、ポータルや Microsoft Azure Storage Explorer を使う方法、プログラムで生成するなどがある。

ここではサービス SAS をプログラムで発行するサンプルとなる。 また、保存されているアクセス ポリシーにサービス SAS が関連付けられないため、強制的に失効させることができないので取り扱いは注意が必要。 有効期限を 1 時間以下に設定することが推奨されている。

docs.microsoft.com

実装

コンテナの SAS トーク

private static string _connectionString  = "connectionString";
private static string _container = "container";

[FunctionName("GetAccessToken")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
    ILogger log)
{
    var (url, token) = await GetContainerSasToken();
    log.LogInformation(url + token);

    return new OkObjectResult(token);
}

public static async Task<(string, string)> GetContainerSasToken()
{
    var account = CloudStorageAccount.Parse(_connectionString);
    var client = account.CreateCloudBlobClient();
    var container = client.GetContainerReference(_container);

    await container.CreateIfNotExistsAsync();

    var policy = new SharedAccessBlobPolicy()
    {
        SharedAccessStartTime = DateTime.UtcNow.AddMinutes(0),
        SharedAccessExpiryTime = DateTime.UtcNow.AddMinutes(3),
        Permissions = SharedAccessBlobPermissions.Read | SharedAccessBlobPermissions.List,
    };

    var token = container.GetSharedAccessSignature(policy, null, null, null);

    // IP制限
    //var ipRange = new IPAddressOrRange("168.1.5.65");
    //var ipRange = new IPAddressOrRange("168.1.5.60", "168.1.5.70");
    //var token = container.GetSharedAccessSignature(policy, null, null, ipRange);

    return (container.Uri.AbsoluteUri, token);
}

ログに出力される URI:%3Aエンコードされているので : に戻すとそのままの URI でブラウザでアクセスできる。

Blob の SAS トーク

private static string _connectionString  = "connectionString";
private static string _container = "container";
private static string _blobName = "blobName";

[FunctionName("GetAccessToken")]
public static async Task<IActionResult> Run(
    [HttpTrigger(AuthorizationLevel.Function, "get", "post", Route = null)] HttpRequest req,
    ILogger log)
{
    var (url, token) = await GetBlobSasToken();
    log.LogInformation(url + token);

    return new OkObjectResult(token);
}
public static async Task<(string, string)> GetBlobSasToken()
{
    var account = CloudStorageAccount.Parse(_connectionString);
    var client = account.CreateCloudBlobClient();
    var container = client.GetContainerReference(_container);

    await container.CreateIfNotExistsAsync();

    var blob = container.GetBlockBlobReference(_blobName);

    var exists = await blob.ExistsAsync();
    if (!exists) { return (null, null); }

    var policy = new SharedAccessBlobPolicy()
    {
        SharedAccessStartTime = DateTime.UtcNow.AddMinutes(0),
        SharedAccessExpiryTime = DateTime.UtcNow.AddMinutes(3),
        Permissions = SharedAccessBlobPermissions.Read,
    };

    var token = blob.GetSharedAccessSignature(policy, null, null, null, null);

    // IP制限
    //var ipRange = new IPAddressOrRange("168.1.5.65");
    //var ipRange = new IPAddressOrRange("168.1.5.60", "168.1.5.70");
    //var token = blob.GetSharedAccessSignature(policy, null, null, null, ipRange);

    return (blob.Uri.AbsoluteUri, token);
}

ログに出力される URI:%3Aエンコードされているので : に戻すとそのままの URI でブラウザでアクセスできる。

SAS トークンの生成と同時に、受け取ったパラメータに応じて Blob のパスをサーバー側で決めることもできるため、SAS URI 自体を返すようにするとクライアントは Blob や対象のファイルなど詳細を知らなくてよくなる。

参考

関連記事

Unity - Addressable Assets System

目次

検証環境

  • Unity 2019.4.3f1
  • Addressables 1.8.5

概要

Addressable Asset System は、Prefab や Scene、Texture、Shader、Material、TextMeshPro の FontAsset などのリソースに付与したアドレス(Addresable Name)を使ってアセットを管理するシステム。
従来からある AssetBundle で、各自実装しなければならなかった「管理する仕組み」などを提供している。
ローカルから生のアセットやビルドしたAssetBundle、リモートに配置した AssetBundle など、読み込む配置先を設定で切り替えることで、統一的なインターフェースでアセットを扱うことができるようになる。

以前は Resources フォルダを使ったアセットの読み込みがあった。 こちらはメタ情報を付与したり、圧縮したり、リソースは変換されてアプリに埋め込まれる。現在は非推奨な方法。

StreamingAssets フォルダを使ってもアセットを読み込める。 こちらは余計な変換処理は挟まれないが、Resources フォルダと同様にアプリに埋め込まれる。

AssetDatabase はプロジェクトフォルダ内にあるアセットならどれでも読み込めるが UnityEditor でしか動かない。

AssetBundle は、予めアセットをプラットフォームごとにビルドして、サーバーや StreamingAssets フォルダに配置することで、ロードできるようになる。 外部に配置するためアプリの容量は小さくなるが、ビルドや管理に関する機能が提供されておらず各自で行う必要があり手間がかかってしまう。

インストール

Window > Package Manager を開いて、[Addressables] からインストールする。

アセットを読み込む

AssetDatabase を使って UnityEditor で読み込む

対象となるアセットの Inspector を開いて、[Addresable] にチェックを付ける。

f:id:yotiky:20210204225106p:plain

もしくは、Window > Asset Management > Addresables > Groups を開いて、対象のアセットをドラッグ&ドロップする。

f:id:yotiky:20210204225406p:plain

スクリプトからは Addresable Name を用いてアセットをロードするため、任意の名前をつける。

Play Mode Scripts で [Use Asset Database (faster)] を選択すると、実行時に AssetDatabase を使って直接アセットを読み込むことができる。

f:id:yotiky:20210205002320p:plain

続いてスクリプトから読み込む方法。

public AssetReference assetReference;

async Task Start()
{
    var m = await Addressables.LoadAssetAsync<Material>("Assets/Materials/PulseCopy.mat").Task;

    var go = await Addressables.LoadAssetAsync<GameObject>("Assets/Prefabs/Sphere.prefab");
    var o1 = Instantiate(go);

    var o2 = await Addressables.InstantiateAsync("Assets/Prefabs/Sphere.prefab");
    o2.GetComponent<Renderer>().material = m;

    var o3 = await assetReference.InstantiateAsync();
}
void OnDestroy()
{
    if (m != null)
        Addressables.Release(m);
    if (go != null)
        Addressables.Release(go);
    if (o2 != null)
        Addressables.ReleaseInstance(o2);
    if (o3 != null)
        Addressables.ReleaseInstance(o3);
}

アセットのロードは、リリースと対となる。Addressables は内部で参照カウントによって管理しているため、GameObject を単に Destroy すると不整合が起きる。Addressables からロードしたアセットは必ずアンロードする。(手動自動は問わない)*1

アセットを読み込むには、アドレス(Addresable Name)を指定して Addressables.LoadAssetAsync() を呼び出す。開放する時は、ロードの戻り値として受け取ったオブジェクトを Release() に渡す。

GameObject を生成するには2パターンある。
1つ目は Addressables.LoadAssetAsync() してから Instantiate() する方法。 こちらは前述した通りRelease() でリリースする。
2つ目は直接 Addressables.InstantiateAsync() する方法。 こちらは ReleaseInstance()GameObject を渡す。 また、InstantiateAsync() で生成された GameObject のライフサイクルはシーンに紐づくので、シーンが破棄される時は同時自動で破棄される。

AssetReference を使うと Inspector 上から Addressable に登録したアセットを設定できる。

ロード処理は非同期のため、戻り値である AsyncOperationHandleTask プロパティを await する。
UnitTask を使った場合は、InstantiateAsync() を直接 await できる。

Play Mode Script

  • Use Asset Database (faster)
  • Simulate Groups (advanced)
  • Use Existing Build (requires built groups)

[Use Asset Database] は、AssetDatabase を使って直接アセットをロードする。アセットのビルドが不要ですぐに実行できるが、 Addressables Event Viewer には AssetBundle の情報は表示されない。

f:id:yotiky:20210205014348p:plain:w250

[Simulate Groups] は、AssetDatabase を使ってロードされるためアセットのビルドは不要だが、AssetBundle の依存関係を分析してシュミレーションすることができる。

f:id:yotiky:20210205014523p:plain

[Use Existing Build] は、実際にビルドした AssetBundle をロードする。

f:id:yotiky:20210205014744p:plain

AssetBundle をローカルから読み込む

Play Mode Script で [Use Existing Build] を選択する。

f:id:yotiky:20210204225628p:plain:w250

Addressables Groups ウィンドウでグループを選択すると、Inspector に設定が表示される。 Build Path と Load Path がどちらも Local になっていることを確認する。

f:id:yotiky:20210205020457p:plain:w350

Addressables Groups ウィンドウで New Build > Default Build Script を実行するとビルドが実行され AssetBundle が生成される。

f:id:yotiky:20210204225709p:plain

スクリプトは、「基本的な使い方 (faster)」の項のものがそのまま使える。

AssetBundle をリモート(ローカルサーバー)から読み込む

Addressables Groups ウィンドウで Tools > Hosting Services を開いて、[Local Hosting] を作成する。

f:id:yotiky:20210205023109p:plain:w200 f:id:yotiky:20210205023147p:plain:w200

任意の [Service Name] を入力し、[Enable] にチェックを付けるとポートが割り当てられる。

f:id:yotiky:20210205023313p:plain

続いて、Addressables Group ウィンドウで Profile > Manage Profiles で Addressables Profile ウィンドウを開いて、[Profile] を作成する。

f:id:yotiky:20210205023528p:plain:w200 f:id:yotiky:20210205023712p:plain

f:id:yotiky:20210205023744p:plain

任意の [Profile Name] を付けて、RemoteBuildPath に [LocalHostData/[BuildTarget]]、RemoteLoadPath に [http://[PrivateIpAddress]:[HostingServicePort]] と入力する。

f:id:yotiky:20210205025746p:plain

Addressables Group ウィンドウで Profile から作成したプロファイルを選択する。

f:id:yotiky:20210205025720p:plain:w200

Addressables Group ウィンドウでグループを選択し、 Inspector 上で、 Build Path と Load Path に Remote を選択する。それぞれプロファイルで設定した内容が反映される。

f:id:yotiky:20210205030004p:plain:w350
f:id:yotiky:20210205020607p:plain:w250

同 Inspector から [Inspect Top Level Settings] をクリックして、AddressableAssetSettings を開く。 [Build Remote Catalog] にチェックを付け、Build Path と Load Path に Remote を選択する。

f:id:yotiky:20210205034729p:plain

[Player Version Override] は指定しないとビルド日時で名前が生成されるため、ビルドするたびに新しいファイルが生成される。同じバージョン番号を指定しているうちはコンテンツカタログが上書き更新されるのでファイルが増え続けることはない。

f:id:yotiky:20210205035737p:plain

Play Mode Script で [Use Existing Build] を選択する。

f:id:yotiky:20210204225628p:plain:w250

アセットをビルドすると、プロジェクト直下に LocalHostData フォルダが作成され Catalog と AssetBundle が出力される。

f:id:yotiky:20210205032453p:plain:w200

スクリプトは、「基本的な使い方 (faster)」の項のものがそのまま使える。

AssetBundle をリモート(Azure Blob Storage)から読み込む

今回は、Azure Blob Storage にパブリックアクセスレベルを「BLOB」にした assets という名前でコンテナを用意する。(危険なので使い終わったらさっさと除去しすること) 実際は SAS などを生成してセキリティに気をつける必要がある。

コンテナのプロパティから URL をメモしておく。

f:id:yotiky:20210205102202p:plain


Addressables Groups ウィンドウで Create > Group > Packed Assets を選択して、グループを作成する。

f:id:yotiky:20210205052315p:plain

任意のアドレス(Addressable Name)を付け、対象のアセットを登録する。

f:id:yotiky:20210205052715p:plain

Group を選択して Inspector を開いて、Build Path と Load Path に Remote を選択する。

f:id:yotiky:20210205053117p:plain:w300

同 Inspector から Inspect Top Level Settings をクリックして、AddressableAssetSettings を開く。 [Build Remote Catalog] にチェックを付け、Build Path と Load Path に Remote を選択する。

f:id:yotiky:20210205034729p:plain

Addressables Group ウィンドウで Profile > Manage Profiles で Addressables Profile ウィンドウを開いて、[Profile] を作成する。

f:id:yotiky:20210205023528p:plain:w200 f:id:yotiky:20210205023712p:plain

任意の [Profile Name] を付けて、RemoteLoadPath に用意した Blob の URL を使ってhttps://xxx.blob.core.windows.net/assets/[BuildTarget] と設定する。[BuildTarget] はビルドしたときのプラットフォームの文字列が入る。

f:id:yotiky:20210205023744p:plain

Addressables Group ウィンドウで Profile から作成したプロファイルを選択する。

f:id:yotiky:20210205101418p:plain:w200

Play Mode Script で [Use Existing Build] を選択する。

f:id:yotiky:20210204225628p:plain:w250

アセットをビルドすると、プロジェクト直下に ServerData フォルダが作成され Catalog と AssetBundle が出力される。

f:id:yotiky:20210205101550p:plain f:id:yotiky:20210205101746p:plain:w300

作成した Catalog と AssetBundle を、Azure Blob Storage にアップロードする。 コンテナのパスなど RemoteLoadPath が変わった場合は再度アセットをビルドしてアップロードし直す。

f:id:yotiky:20210205102437p:plain

スクリプトは、「基本的な使い方 (faster)」の項のものがそのまま使える。

グループ

フォルダをドラッグ&ドロップすることもできる。階層を維持する場合、フォルダに対してアドレス(Addresable Name)とラベルを設定できる。フォルダを指定してまとめて読み込むことはできなそう。 ラベルは配下のアセットにも適用されるので問題なくまとめて読み込むことができる。 アドレス(Addresable Name)もラベルも配下のアセットに個別に設定することはできないが、決められたアドレス(Addresable Name)を使えば個別に読み込むことは可能である。

f:id:yotiky:20210205132108p:plain

フォルダが不要な場合はグループの直下に移動する。

f:id:yotiky:20210205132229p:plain

グループとプロファイルの関係

Addressables Groups

グループでは、アセットにアドレス(Addresable Name)とラベルを付与して、グループに登録する。 アセットはいずれかのグループに所属し、複数のグループには登録できない。

f:id:yotiky:20210205152731p:plain

グループの設定

グループ単位で AssetBundle 化されるため、グループの設定では、Build Path と Load Path が選択できたり、圧縮やプロバイダーの選択など、Packing や Loading などの設定ができる。

f:id:yotiky:20210205153754p:plain:w300

AddressableAssetSettings

Assets/AddressableAssetsData/AddressableAssetSettings では、コンテンツカタログの設定ができる。コンテンツカタログを出力するかどうか、Build Path と Load Path をどこにするか、バージョンを上書きするかなどの設定が含まれる。

f:id:yotiky:20210205154027p:plain:w350

Addressables Profiles

プロファイルでは、ビルドとロードのパスをどこにするかの設定を作り分けられるようになっている。Unity Editor で実行する時、ローカルホストや開発サーバー、本番サーバーで実行する時などプロファイルを切り替えるだけでビルド先とロード先を変更できるようになる。

f:id:yotiky:20210205153238p:plain

ビルド

各グループはグループ毎に Build Path / Load Path の設定を持つ。
コンテンツカタログも別途 Build Path / Load Path の設定を持ち、出力するように設定にする必要がある。
これらの Path は、選択した Profile によって値が切り替えられる。

コンテンツカタログは AddressableAssetSettings で Build Retemo Catalog にチェックを付けると生成されるようになる。

f:id:yotiky:20210708181908p:plain:w300

ビルドしたけど出力されていないと思った時は各グループ、コンテンツカタログの Build Path がどこになっているかを確認すると良い。

ラベル

アセットにラベルを付けてグルーピングすることができる。アセットでラベルを指定してまとめて読み込んだり、ダウンロードすることができる。

Addressables Groups ウィンドウで、Tools > Labels を開いて、ラベルを作成する。

f:id:yotiky:20210205125644p:plain:w200 f:id:yotiky:20210205125806p:plain:w250

Addressables Groups ウィンドウで任意のアセットにラベルを設定する。

f:id:yotiky:20210205130021p:plain:w250

Addressables.LoadAssetsAsync() の引数にラベルを渡すとまとめてロードできる。(Assets が複数形なので注意)

var objects = await Addressables.LoadAssetsAsync<Object>("Model", null);
foreach(var item in objects)
{
    Debug.Log(item);
}

f:id:yotiky:20210205130747p:plain:w300

コンテンツカタログ

ビルド結果のファイル構成は以下の通り。

f:id:yotiky:20210205155523p:plain:w300

  • catalog_<version>.hash
  • catalog_<version>.json
  • グループ毎の AssetBundle や依存関係のある AssetBundle など

hash ファイルは、コンテンツカタログの更新の有無を比較するために使われる。 hash ファイルに違いがある場合、リモートから json ファイルをロードする。 hash ファイルが同じ場合はローカルのキャッシュからロードされる。

これら hash ファイルと json ファイルの取得元やファイル名などの情報は、アプリのビルド時に settings.json を書き出され StreamingAssets に保存される。*2 アプリのビルドをしないと取得元を変更することができないため、[Player Version Override] はアプリのバージョンと同じにして、アプリが同じバージョンの間はコンテンツカタログの名前が変わらないようにして上書き更新し続けるのが良さそうである。

light11.hatenadiary.com

Bundle Mode (Packing Mode)

AssetBundle の分割単位を設定できる。 原則としてグループ毎に AssetBundle に分割され、グループ内であっても Scene とそれ以外では更に分割される。

f:id:yotiky:20210205162021p:plain:w300

  • Pack Together
  • Pack Separately
  • Pack Together By Label


Pack Together はグループ単位でまとめられる。同じグループでも Scene は別になる。

f:id:yotiky:20210205162829p:plain:w300

Pack Separately はアドレス単位ですべて個別に分割される。

f:id:yotiky:20210205163357p:plain

Pack Together By Label はラベル単位で分割される。同じグループでも Scene は別になる。また、ラベルの付いていないアセットはまとめて1つの AssetBundle になる。

f:id:yotiky:20210205163631p:plain:w300

Window > Asset Management > Addressables > Analyze を開くと分割の内容を解析できる。(前述の画像)

light11.hatenadiary.com

キャッシュ

設定

Project ウィンドウで Addressables > Initialization > Cache Initialization Settings を追加する。

f:id:yotiky:20210207170304p:plain

f:id:yotiky:20210207170435p:plain:w300

設定した CacheInitializationSettings を AddressableAssetSettings の Initialization Objects に追加する。

f:id:yotiky:20210207172035p:plain:w300

プロパティ 内容
Compress Bundles キャッシュに保存される AssetBundle を LZ4 形式で圧縮するかどうか
Cache Directory Override キャッシュの保存先
Expiration Delay キャッシュの保存期間(最大12960000秒(150日))
Limit Cache Size 最大キャッシュサイズを有効にするかどうか

保存先

Unity Editor (Windows)

リモートのコンテンツカタログがキャッシュされる場所。

C:\Users\<UserName>\AppData\LocalLow\<CompanyName>\<ProductName>\com.unity.addressables

f:id:yotiky:20210205205330p:plain:w200

ダウンロードした AssetBundle がキャッシュされる場所。

C:\Users\<UserName>\AppData\LocalLow\Unity\<CompanyName>_<ProductName>

f:id:yotiky:20210205205555p:plain:w250

グループの設定に以下の項目がある。(機能は未調査)

f:id:yotiky:20210205235518p:plain:w300

参考:qiita.com

HoloLens2

リモートのコンテンツカタログがキャッシュされる場所。

C:\Data\Users\<UserName>\AppData\Local\Packages\<ApplicationName_hash>\LocalState\com.unity.addressables

ダウンロードした AssetBundle がキャッシュされる場所。

C:\Data\Users\<UserName>\AppData\Local\Packages\MR-<ApplicationName_hash>LocalState\UnityCache\Shared

削除

ダウンロードした AssetBundle のキャッシュを削除する。コンテンツカタログは削除されない。

Caching.ClearCache();

補足

Addressables Event Viewer で参照カウントを可視化する

Addressables Event Viewer (旧名 RM Profiler)を使うと参照カウントを可視化したり、アセットが含まれている AssetBundle を確認することができる。

機能を有効にするには、Window > Asset Management > Addressables > Settings を開いて、[Send Profiler Events] にチェックを付ける。

f:id:yotiky:20210205021610p:plain:w300 f:id:yotiky:20210205021643p:plain:w200

light11.hatenadiary.com

Addressables で NullReferenceException

NullReferenceException when using Addressables.DownloadDependenciesAsync(label) - Unity Forum

Addressables のメソッドを呼び出して Taskawait しようとした時に Task 自体が nullNullReferenceException が発生する場合があるらしい。

以下のようにラップしてしまう拡張メソッドを用意するか、await して取り出すような拡張メソッドを用意する。

public static class AsyncOperationHandleExtensions
{
    public static Task<T> NotNullTask<T>(this AsyncOperationHandle<T> handle)
        => handle.Task ?? Task.FromResult(handle.Result);
}

別プロジェクトで作成したコンテンツカタログを読み込む

// ファイルパスかURL
var catalog = await Addressables.LoadContentCatalogAsync("http://169.254.37.215:12345/catalog.json");

// カタログは追加しなくても動きそう
//Addressables.AddResourceLocator(catalog);

await Addressables.InstantiateAsync("Assets/Prefabs/Sphere.prefab");

tsubakit1.hateblo.jp

ダウンロード処理のカスタマイズ

light11.hatenadiary.com

便利ツール

スクリプトからの操作

Addressables クラス

// 手動での初期化
// 呼び出していない場合は、LoadAssetAsync などが呼ばれた時に実行される
// https://docs.unity3d.com/Packages/com.unity.addressables@1.13/manual/InitializeAsync.html
await Addressables.InitializeAsync();

// 追加するコンテンツカタログをロードする
// パスはファイルの絶対パスやURLなど
// https://baba-s.hatenablog.com/entry/2020/03/19/063000
await Addressables.LoadContentCatalogAsync("CatalogPath");

// AssetBundle のサイズを取得する
await Addressables.GetDownloadSizeAsync("key");

// 事前に AssetBundle をダウンロードする
// https://baba-s.hatenablog.com/entry/2020/03/19/042000_1
await Addressables.DownloadDependenciesAsync("key");

参考