Unity のアセット配信を約14倍高速化した「Asset Ball」の設計
現在、転職活動中です。この記事を読んで興味を持っていただけた方は、X(@harusann2) に DM を送っていただけるとうれしいです。
追記
サンプル実装を公開しました
https://github.com/nakajimakotaro/AssetBall
前提:Unity 製ソーシャルゲームの起動時ダウンロードを改善する
本記事では、ある程度規模の大きい Unity 製ソーシャルゲームで、ゲーム起動時のアセット配信を改善した事例を紹介します。
対象プロジェクトでは、1万ファイル以上のアセットを Android / iOS / Windowsの3プラットフォームに配信していました。従来はアセットを1ファイルずつダウンロードしており、初回ダウンロードには約3時間かかっていました。
アセットの総容量に加えて、ファイルごとに繰り返される HTTP リクエストのオーバーヘッドが問題でした。1回あたりの待ち時間が小さくても、1万回以上積み重なれば無視できません。
そこで設計・実装したのが、全アセットを1つのファイルにまとめる Asset Ball です。
単に結合するだけではありません。「更新されたアセットを常に末尾へ移動する」という配置ルールによって、初回ダウンロードだけでなく、任意の過去バージョンからの差分更新も1回の HTTP Range Request にまとめます。
この方式により、初回ダウンロード時間を 約3時間から約13分へ、約14倍高速化しました。
| 項目 | 対象・成果 |
|---|---|
| エンジン | Unity |
| 改善対象 | ゲーム起動時のアセットダウンロード・更新 |
| アセット数 | 1万ファイル以上 |
| プラットフォーム | Android / iOS / Windows |
| 初回ダウンロード時間 | 約3時間 → 約13分 |
| クライアント実装 | 3プラットフォーム共通 |
初回ダウンロードだけを速くしても解決しない
このプロジェクトでは、初回だけ全アセットを ZIP にまとめても十分ではありませんでした。
Unity のバージョンアップや社内ツールの都合により、1万ファイルを超える大規模なアセット更新が頻繁に発生していたためです。初回ダウンロードを高速化しても、その後の更新で同じ問題が起きます。
さらに、ユーザーの起動頻度も一定ではありません。毎日起動するユーザーもいれば、1週間に1回起動し、数バージョン分をまとめて更新するユーザーもいます。
必要だったのは、次の条件を同時に満たす仕組みです。
- 初回ダウンロードと、運用中の大規模更新の両方を高速化する
- 最新に近いユーザーだけでなく、任意の過去バージョンから効率よく更新できる
- Android / iOS / Windowsで共通のクライアント実装を使える
- 実装・保守を複雑にしない
この条件に対して、通信処理の多重化ではなく、配信するデータの配置そのものを変える方針を取りました。
コアアイデア:更新されたアセットを末尾へ移動する
Asset Ball は、全アセットを連結した1つのバイナリファイルです。各アセットの位置・サイズ・ハッシュ値は、別の Index File に記録します。
運用ルールはシンプルです。
更新されていないアセットの相対的な順序を保ち、更新されたアセットを新しい内容で末尾に配置する。
これを更新のたびに繰り返すと、古いアセットは前方に、新しく更新されたアセットは後方に集まります。
5つのアセットで見る配置の変化
初期状態に A・B・C・D・E の5つのアセットがあり、B、D、A の順に更新されたとします。
| バージョン | 更新内容 | Asset Ball の配置(左が先頭、右が末尾) |
|---|---|---|
| v1 | 初期状態 | A │ B │ C │ D │ E |
| v2 | B を更新 | A │ C │ D │ E │ B′ |
| v3 | D を更新 | A │ C │ E │ B′ │ D′ |
| v4 | A を更新 | C │ E │ B′ │ D′ │ A′ |
X′ は、更新後のアセットを表します。更新対象の旧データを元の並びから外し、新しいデータを末尾へ配置しています。
どのバージョンからでも、必要な差分が後方に集まる
この状態で、各バージョンのユーザーが v4 に更新する場合を考えます。
| ユーザーの状態 | 必要なアセット | v4 内の取得位置 | Range 数 |
|---|---|---|---|
| v1 を保持 | B′・D′・A′ | 3〜5番目 | 1 |
| v2 を保持 | D′・A′ | 4〜5番目 | 1 |
| v3 を保持 | A′ | 5番目 | 1 |
| 初回起動 | C・E・B′・D′・A′ | 全域 | 1 |
ユーザーが持っているバージョンによって、ダウンロードを開始する位置は変わります。しかし、必要なアセットはどのケースでも後方の連続領域にまとまっています。
これは例の中だけで成立する性質ではありません。ある過去バージョンを基準にすると、それ以降に更新されたアセットは、そのたびに末尾へ移動します。更新されなかったアセットは前方に残るため、過去のどのバージョンを基準にしても、そこからの更新対象が後方に集まります。
したがって、ユーザーごとに差分ファイルを用意しなくても、同じ Asset Ball から1 Range で必要な差分を取得できます。
初回ユーザーも特別扱いする必要はありません。ローカルにアセットがなければ、同じ仕組みで全域を取得するだけです。
実装:Index と標準 HTTP だけで取得する
サーバーが配信するもの
サーバー側で用意するのは、次の2つです。
| ファイル | 内容 |
|---|---|
| Asset Ball | 全アセットを連結したバイナリファイル |
| Index File | 各アセットの識別子・開始バイト位置・バイトサイズ・ハッシュ値を記録した JSON |
Index が、Asset Ball 内のどこに何が入っているかを示す対応表になります。
アセットを更新する際は、未更新アセットの順序を維持し、更新・追加されたアセットを末尾に配置した新しい Asset Ball を生成します。その配置に合わせて Index も再生成します。
| 操作 | 処理 |
|---|---|
| アセットの更新 | 旧データを配置から外し、新しい内容を末尾に配置 |
| 同一アセットの再更新 | 同じルールで再び末尾に配置 |
| 新規追加 | 末尾に追加 |
| 削除 | Asset Ball から対象データを除去し、Index からも除外 |
削除時は、残るアセットの相対的な順序を維持して詰め直し、新しい位置に合わせて Index を更新します。
クライアントの処理
クライアントは、起動時や更新チェック時に次の処理を行います。
- Index File をダウンロードする
- 各アセットのハッシュを、ローカルに保持しているハッシュと比較する
- 不一致、または未取得のアセットの位置とサイズを取り出す
- 連続した取得範囲をまとめて HTTP Range Request でダウンロードする
- 取得したバイナリを Index の位置情報に従って分割し、個別のファイルとして保存する
クライアントに「どのリリースから更新するのか」を基に差分パッケージを選ぶ処理はありません。必要なのは、ハッシュの比較と、連続した範囲をまとめる処理だけです。
クライアントは普通に連続範囲をまとめるだけで、サーバーの配置ルールによって結果的に1 Rangeになる。 ここが本方式のポイントです。
配信には結合ファイルを使いますが、ローカルでは 1アセット=1ファイル として保存します。巨大な Asset Ball を端末に保持し続ける必要はなく、アセット単位の更新や削除も容易です。
特別な通信プロトコルは不要
配信に使うのは、標準 HTTP の Range Request です。独自の通信プロトコルや専用ミドルウェアは必要ありません。
サーバーはファイルを連結して配置を管理し、クライアントは指定したバイト範囲を取得します。この単純な構成で、3プラットフォーム共通のクライアント実装を実現しました。
導入リスクが低い:最適化が崩れても、同じ仕組みで更新できる
この設計は、通常時に速いだけでなく、配置の最適化に失敗しても、配信の正確性を損なわないという強みがあります。高速化のための配置ルールが、更新を成功させるための必須条件にはなっていません。
例えば、前回の Asset Ball の生成失敗・破損・消失などにより、以前の配置を引き継げなくなる場合があります。それでも、元のアセットから正しい Asset Ball と Index を再生成できれば、配信は継続できます。Index に新しい配置が記録されるため、クライアントはいつもどおり、必要な範囲を取得するだけです。
配置順を引き継げなければ、更新対象が分散し、Range Request の回数が増える場合があります。しかし、最悪でも従来の個別ダウンロードに相当する取得方式へ戻るだけで、配置順の乱れ自体がアセットの破損や更新エラーを引き起こすことはありません。
しかも、別のダウンロード方式への切り替えや、例外専用の復旧処理は不要です。「ハッシュを比較し、連続する取得範囲をまとめる」という通常の処理が、1 Range の場合も、複数 Range の場合もそのまま動きます。
最適化が効けば速くなり、効かなくても同じ仕組みで正しく取得できる。 この性質は、導入・運用時のリスクを低く抑えられる大きなメリットです。配置の履歴を失った場合にも、過去の並びを完全に復元しなければ配信を再開できない、という依存関係がありません。
取得の正確性は Index が担い、取得の効率は配置ルールが担います。配信する本体と Index が正しく対応していれば、配置の最適化が崩れても更新は成立します。
補足:配置の引き継ぎ以外で、取得範囲が分かれるケース
一度変更したアセットを過去と同じ内容へ戻すと、後方の更新領域に、古いクライアントのローカルデータとハッシュが一致するアセットが混ざる場合があります。また、ローカルファイルの一部欠損などにより、前方のアセットを再取得する場合もあります。
これらも同じ仕組みで処理できます。ハッシュ不一致または未取得のアセットについて、連続する範囲ごとにダウンロードするだけで、問題なく更新できます。
S3 アップロードも、未変更データを再送しない
クライアントへの配信を改善すると、今度は社内サーバーから S3 へのアップロードが課題になりました。
Asset Ball は1プラットフォームあたり10 GB超、3プラットフォーム合計で30 GB超あります。更新のたびにこの全量を社内サーバーから S3 に送信していたため、アップロードに非常に時間がかかっていました。
そこで、S3 のマルチパートアップロードにおける UploadPartCopy を利用しました。
未変更部分は S3 内でコピーする
未変更アセットのデータは、すでに前バージョンの Asset Ball として S3 に存在します。同じデータを社内サーバーから再送する必要はありません。
新しい Asset Ball は、次のように組み立てます。
| データ | 取得元・処理 |
|---|---|
| 未変更アセット | 旧 Asset Ball の該当範囲を S3 内でコピー(UploadPartCopy) |
| 更新・追加アセット | 社内サーバーからアップロード(UploadPart) |
| 完成したファイル | マルチパートアップロードを完了し、新しい Asset Ball として確定 |
例えば、A │ B │ C │ D │ E から B を更新し、A │ C │ D │ E │ B′ を作る場合、旧ファイルの A と C │ D │ E に対応する範囲を再利用し、B′ を新たに送信します。
新しいファイルの未変更部分は、旧ファイル上でも必ず1つの連続領域になっているわけではありません。旧ファイル内の必要な範囲をコピーして、新しい配置を組み立てるということです。実際のコピー・アップロード単位は、マルチパートアップロードのパート制約に合わせて構成します。
補足:最小アップロード単位の制約があるため、更新差分だけでなく、未変更部分も一部含めてアップロードする場合があります。転送量が厳密に更新差分だけになるわけではありません。
これにより、社内サーバーから送信するデータを更新差分中心に抑えられます。大容量データを社内から再送する処理と比べ、S3 内コピーの時間・コスト負担は極めて小さく、アップロード時間の大幅な短縮につながりました。
1つの配置ルールが、配信とデプロイの両方に効く
| レイヤー | 活用する性質 | 仕組み |
|---|---|---|
| クライアントへの配信 | 更新対象が後方に集まる | HTTP Range Request |
| S3 へのデプロイ | 未変更データを旧ファイルから再利用できる | UploadPartCopy |
未変更アセットの順序を維持し、更新アセットを末尾に集める。この配置によって、ダウンロードはまとまった範囲になり、アップロードでは旧ファイルの未変更領域を再利用しやすくなります。
1つの運用ルールが、ユーザーの待ち時間と開発側の待ち時間の両方を減らしました。
他の方式を採用しなかった理由
Asset Ball に至るまでに、いくつかの方式を検討しました。判断基準は、初回だけでなく、大規模更新・任意バージョンからの更新・3プラットフォーム対応を同時に満たせるかです。
初回だけ ZIP にまとめる
初回ダウンロードのリクエスト数は減らせます。しかし、このプロジェクトでは運用中も1万ファイルを超える更新が頻発していました。
初回だけを特別扱いする方式では、その後の大規模更新に対応できません。
リリースごとに差分 ZIP を生成する
直前のバージョンから更新するユーザーには有効です。一方、数バージョン前のユーザーには複数の差分 ZIP が必要になり、途中で何度も更新された同じアセットを重複して取得することもあります。
過去の各バージョンから最新への専用 ZIP を用意する方法もありますが、対応する過去バージョンの数だけ生成・管理が増えます。全バージョン間の組み合わせまで用意すれば、その負担はさらに大きくなります。
Asset Ball では、最新の結合ファイルと Index を用意するだけで、ユーザーごとに異なる差分を取得できます。
HTTP/2 による多重化
HTTP/2 は、複数のリクエストを効率よく処理する方式です。ただし、アセットごとのリクエスト自体がなくなるわけではありません。
また、本プロジェクトの Unity 環境では、Android / iOS / Windowsそれぞれへのネイティブ対応が必要で、実装・保守コストも課題でした。
そこで、リクエストの処理効率を上げるよりも、リクエスト数そのものを減らす方式を採用しました。
比較
| 観点 | 初回のみ ZIP | リリースごとの差分 ZIP | HTTP/2 多重化 | Asset Ball |
|---|---|---|---|---|
| 初回ダウンロード | 1リクエストに集約 | 全量配信を別途用意 | ファイル単位で取得 | 1 Range に集約 |
| 運用中の大規模更新 | 別の対応が必要 | 対応可能 | ファイル単位で取得 | 1 Range に集約 |
| 古いバージョンからの更新 | 別の対応が必要 | 複数 ZIP、または専用 ZIP が必要 | 必要なファイルを個別取得 | 同じ Asset Ball から取得 |
| 本プロジェクトのクライアント対応 | 共通実装が可能 | 共通実装が可能 | 各プラットフォームへの対応が必要 | 共通実装 |
| 運用上の主な管理対象 | 初回用 ZIP と更新方式 | 差分 ZIP と適用順序 | 個別ファイルと通信処理 | Asset Ball と Index |
成果:シンプルな仕組みで約14倍高速化
最終的に、初回ダウンロード時間は 約3時間から約13分になりました。3プラットフォームすべてで、標準 HTTP Range Request を使う共通のクライアント実装で達成した結果です。
この方式では、ファイルごとに繰り返していた HTTP リクエストのオーバーヘッドを排除し、必要な差分を1回の連続転送にまとめています。
必要なアセットのバイト列をそのまま配信するという前提で、リクエスト回数を最小の1回にする。その意味で、理論上最速を目指せる構造です。 実際の転送時間は回線帯域や端末の I/O にも依存しますが、ファイル数に比例するリクエスト待ちは取り除けます。
設計上の要点は、次の4つです。
- 配置で高速化する: 更新アセットを末尾に集め、任意バージョンからの差分を1 Rangeにする
- クライアントを単純に保つ: ハッシュ比較と連続範囲の取得だけで動作する
- 最適化が崩れても配信を継続する: 配置順を引き継げなくても、Index に従って正しく取得できる
- デプロイにも同じ構造を活用する: 未変更データを S3 内で再利用し、社内からの転送量を減らす
新しい通信技術を導入したわけではありません。枯れた既存技術に、現場の要件に合った配置ルールを組み合わせました。
この設計の価値は、使った技術の新しさではなく、制約を読み解き、単純な仕組みで複数の課題を同時に解決したことにあります。
付記:AI の利用と、設計者としての価値について
本記事の文章構成と表現の整理には、生成 AI を利用しています。ただし、Asset Ball の設計そのものは、AI から得られたアイデアではありません。 課題の整理、方式の考案・比較、設計、実装は、私自身が行いました。
この点を明記するのは、AI の出力をそのまま掲載した記事として読まれたくないからです。文章を整えることと、現場の問題を解決することは、それぞれ異なる仕事です。
「大量のアセット配信を高速化したい」と AI に相談すれば、HTTP/2、並列ダウンロード、ZIP 化といった一般的な改善案は得られます。しかし、本件では、Unity の3プラットフォーム対応、頻繁な大規模更新、ユーザーごとに異なる更新元バージョン、実装・保守コストを同時に考える必要がありました。
私は、こうした現場の制約を読み解く仕事まで AI に丸投げしても、この答えは出てこないと考えています。一般的な技術候補を並べるだけでは、どこを変えれば問題の構造そのものが変わるのか、という判断には届きません。
今回、私が行ったのは、単にダウンロード処理のコードを書くことではありません。必要な要件を見極め、複数の案を比較し、アセットの配置ルールから解決策を組み立て、運用上の失敗時の挙動まで考え、実装して約14倍の高速化につなげたことです。さらに、同じ構造を S3 アップロードの改善にも展開しました。
AI がコードや文章を生成できることと、現場にとって適切な解決策を設計し、成果までつなげられることは同義ではありません。
現場の制約を理解し、技術を選び、必要なら独自の仕組みを考え、実装して結果を出す。私は、そこに人間の代替されない価値があると考えています。
現在、転職活動中です。現場の課題を整理し、技術で解決する仕事に関心があります。お仕事や採用のお話がありましたら、X(@harusann2) に DM を送っていただけるとうれしいです。