🎃

【Unity/Addressables】DLC頒布するための実装方法サンプル

2024/02/11に公開

はじめに

最近DLsiteで同人ゲーム販売を始めたのですが、同業サークル様のストアを見ていて、DLC販売も結構盛んなのだということを知りました。これをきっかけにぜひDLCを作ってみたいと思い、色々調査して自分なりの簡単なDLC実装方法を考えてみました。

正直ゲーム制作のノウハウをあまり持っていないので自信がないのですが、「Unity DLC 実装」等で調べてもなかなか情報にたどり着けず苦労したので、実装方法の一案として記事にしたいと思います。

もし、もっと良い方法や「現場ではこう実装してる」みたいな手法があれば、コメントにてお教え頂けると幸いです。

対象読者

  • Unity初心者
    • 最低限エディタなどの基礎やC#はわかる
    • Addressablesとかの追加パッケージについてはあまり知らない
  • UnityでDLCを作りたいと思っている方
  • とりあえず見様見真似で良いからDLCを実現したい方

筆者の環境

  • Windows
  • Unity 2022.3.8f1
  • Addressables 1.21.19

完成イメージ

リポジトリ → https://github.com/tigrig29/unity-dlc-sample

「DLC購入者のみ(DLCファイルをゲーム内に配置した場合のみ)DLC画像を見られる」というイメージ。

DLCファイル未配置の場合……

DLCファイル配置後の場合……

DLC実装方針(ざっくり)

  • あらかじめ機能自体はゲーム内に組み込んでおく
  • Addressable Asset Systemを使い、特定のフォルダにDLCデータを配置することで、機能がONになるように実装する

事前準備

環境構築などは省きます。

まずUnity2022.3.8f1あたりのバージョンでプロジェクトを立ち上げます。

Addressablesの導入

PackageManagerを開き、Packages: Unity Registry から Addressables をインストールします。

画像表示欄と画像の用意

次に、適当なシーンを作って、UI > Image を配置しておきます。

位置やサイズは適当に調整。

またこのImageに後々設定する画像を用意しておきます。

1: デフォルト画像

↑の画像を Assets/DefaultResources フォルダに入れておきます。

2: DLC画像

↑の画像は Assets/DlcResources フォルダに入れておきます。

Addressablesを使ってデフォルト画像を表示

Addressable Asset System についての詳しい解説については、参考記事 を御覧ください。

※デフォルト画像の表示についてはAddressablesを使わなくても実現可能です。本記事では様々なリソースをAddressablesで管理することを想定して、DLC以外のリソースもAddressableAssetとして設定しています。

まずAddressablesGroupウィンドウを開き、 Create Addressables Settings をクリックします。

すると、 Default Local Group (Default) というグループが表示されるので、ここに DefaultResources フォルダをドラッグアンドドロップします。

これでデフォルト画像のAddressablesの設定は以上です。

続いて、実際にAddressablesを使ってデフォルト画像をロードし、画面上に表示する仕組みを作っていきます。

ここでは、適当なボタンを配置し、ボタンを押したら画像が表示されるという処理を作ることにします。

まず適当なボタンを配置します(詳細な手順は省きます)。

次に以下のスクリプトをボタンにアタッチします。

ShowImageButton.cs
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.UI;

public class ShowImageButton : MonoBehaviour
{
    [SerializeField]
    private Image _image;

    private Button _button;

    private void Awake()
    {
        _button = GetComponent<Button>();
        _button.onClick.AddListener(ShowImage);
    }

    private async void ShowImage()
    {
        var sprite = await LoadSprite("Assets/DefaultResources/default-image.png");
        _image.sprite = sprite;
    }

    private async Task<Sprite> LoadSprite(string address)
    {
        var handle = Addressables.LoadAssetAsync<Sprite>(address);
        await handle.Task;
        return handle.Result;
    }
}

また SerializeField の Image に Canvas > Image をアタッチしておきます。

以上でデフォルト画像の表示処理は実装完了です。

ゲームを実行し [Show image] ボタンをクリックすると、デフォルト画像が表示されることが確認できるかと思います。

DLC部分の実装

ここから本題のDLC実装部分です。

DLCリソース用AddressablesGroupの作成

まずはDLCリソース用のAddressablesGroupを作ります。

AddressablesGroupウィンドウで右クリックし、 Create New Group > Packed Assets を選択します。

作成されたグループを右クリックし Rename でグループ名を DLC Group に変更しておきます。

DLC Group には DlcResources フォルダ(DLC画像を格納したフォルダ)をドラッグアンドドロップして設定しておきます。

DLCGroupの各種設定

AddressablesGroupウィンドウの DLC Group を選択し、Inspectorウィンドウの [Inspect Top Level Settings] をクリックします。

するとInspectorにて『Addressable Asset Settings』が編集できるようになります。

Catalog欄を以下の通りに編集します。

  • Build Remote Catalog: ONにする(チェックつける)
  • Build & Load Paths: Remote に設定

次にProfiles欄の [Manage Profiles] ボタンをクリックします。

『Addressables Profiles』のウィンドウが開くので、 Default プロファイルを次のように編集します。

  • Remote のプルダウンを Custom に設定
  • Remote.LoadPath を DlcBundles/[BuildTarget] に変更

最後に DLC Group の設定に戻り、 Content Packing & Loading > Build & Load Paths を Remote に変更します。

以上で DLC Group の設定は完了です。

試しにDLC画像を表示させてみる

一旦ここまでのAddressablesの設定を活用して、DLC画像を表示させる処理を作ります。

ShowImageButton.csShowImage メソッドを以下のように変更します。

ShowImageButton.cs
    private async void ShowImage()
    {
        var sprite = await LoadSprite("Assets/DefaultResources/default-image.png");
        _image.sprite = sprite;
+
+       await Task.Delay(500);
+
+       var dlcSprite = await LoadSprite("Assets/DlcResources/dlc-image.png");
+       _image.sprite = dlcSprite;
    }

これでデフォルト画像表示の0.5秒後にDLC画像に切り替わります。

少し設定を変えます。

AddressablesGroupウィンドウの上部にある PlayModeScript を Use Existing Build(Windows) に変更します。

これにより DLCGroupの各種設定 で Remote.LoadPath に設定した DlcBundles/[BuildTarget] から実際にデータをロードするようになります。

また PlayModeScript を Use Existing Build(Windows) に設定した場合は、アセットのビルドが必要となるため、AddressablesGroupウィンドウの上部にある Build を実行しておきます。

Build 実行後、プロジェクトフォルダのルートに ServerData/StandaloneWindows64 フォルダが生成され、その中にCatalogファイル(catalog_xxx.hash, catalog_xxx.json)とAssetBundle(dlcgroup_assets_xxx.bundle)、計3つのファイルが出力されていればOKです。(ファイル名のxxxの部分は設定やビルド内容によって異なります)

この状態でゲームを実行し、 [Show image] ボタンをクリックすると……

下図のようなエラーが出るかと思います。

これは Remote.LoadPath である DlcBundles/[BuildTarget] に、CatalogファイルとAssetBundleがないために発生しているエラーです。

以下の手順でファイルを配置します。

  • プロジェクトフォルダのルートに DlcBundles/StandaloneWindows64 フォルダを作成
  • ServerData/StandaloneWindows64 フォルダに出力されている3つのファイル(catalog_xxx.hash, catalog_xxx.json, dlcgroup_assets_xxx.bundle)をコピー
  • DlcBundles/StandaloneWindows64 フォルダ内に貼り付け

この状態で再度ゲームを実行すると、エラー発生せずDLC画像が表示されるかと思います。

一旦ここまでで、ビルドしてCatalog+AssetBundle出力 → Catalog+AssetBundleをゲームフォルダ内に配置 → データが読み込める、といった流れが実現できました。

だいぶDLCっぽい流れになってきたかと思いますが、現時点ではDLC購入者かどうかに関わらずDLC画像が見える状態になっています。

だからといって、DLC未購入者にCatalog+AssetBundleがない状態のゲームを提供してしまうと、ゲーム実行時にエラーが発生してしまいます。

これでは困るので、

  • DLC購入、未購入に関わらず、Catalog+AssetBundleは提供する
  • ただし未購入者にはDLC画像が見えない(購入者のみDLC画像が見える)ように分岐処理を実装する

といった対応をしていきます。

DLC購入者のみDLC画像が見えるようにする

まず Assets/DlcResources(DLC画像を格納したフォルダ) に以下の dlc-settings.json ファイルを配置します。

dlc-settings.json
{
  "enabled": false
}

するとAddressablesGroupウィンドウの DLC Groups 内に、自動で上記jsonファイルが追加されます。

DLC Groups に属するデータが更新されたので、前述と同じ手順で再度ビルドしておきます。(出力されたCatalog+AssetBundleは DlcBundles\StandaloneWindows64 フォルダにコピーしておきます)

次に、上記のjsonファイルをC#側で読み込むため、以下のC#ファイルを追加します。

DlcSettings.cs
public class DlcSettings
{
    public bool enabled;
}

続いて ShowImageButton.cs で「DLC購入者のみDLC画像が見えるようにする」ための処理を実装します。

ShowImageを編集
    private async void ShowImage()
    {
        var sprite = await LoadSprite("Assets/DefaultResources/default-image.png");
        _image.sprite = sprite;

        await Task.Delay(500);

+       var dlcSettings = await LoadDlcSettings("Assets/DlcResources/dlc-settings.json");
+       if (dlcSettings.enabled)
+       {
            var dlcSprite = await LoadSprite("Assets/DlcResources/dlc-image.png");
            _image.sprite = dlcSprite;
+       }
    }
}
LoadDlcSettingsを追加
    private async Task<Sprite> LoadSprite(string address)
    {
        var handle = Addressables.LoadAssetAsync<Sprite>(address);
        await handle.Task;
        return handle.Result;
    }
+
+   private async Task<DlcSettings> LoadDlcSettings(string address)
+   {
+       var handle = Addressables.LoadAssetAsync<TextAsset>(address);
+       await handle.Task;
+       return JsonUtility.FromJson<DlcSettings>(handle.Result.ToString());
+   }
}
ShowImageButton.cs 全文
ShowImageButton.cs
using System.Threading.Tasks;
using UnityEngine;
using UnityEngine.AddressableAssets;
using UnityEngine.UI;

public class ShowImageButton : MonoBehaviour
{
    [SerializeField]
    private Image _image;

    private Button _button;

    private void Awake()
    {
        _button = GetComponent<Button>();
        _button.onClick.AddListener(ShowImage);
    }

    private async void ShowImage()
    {
        var sprite = await LoadSprite("Assets/DefaultResources/default-image.png");
        _image.sprite = sprite;

        await Task.Delay(500);

        var dlcSettings = await LoadDlcSettings("Assets/DlcResources/dlc-settings.json");
        if (dlcSettings.enabled)
        {
            var dlcSprite = await LoadSprite("Assets/DlcResources/dlc-image.png");
            _image.sprite = dlcSprite;
        }
    }

    private async Task<Sprite> LoadSprite(string address)
    {
        var handle = Addressables.LoadAssetAsync<Sprite>(address);
        await handle.Task;
        return handle.Result;
    }

    private async Task<DlcSettings> LoadDlcSettings(string address)
    {
        var handle = Addressables.LoadAssetAsync<TextAsset>(address);
        await handle.Task;
        return JsonUtility.FromJson<DlcSettings>(handle.Result.ToString());
    }
}

ここまでで一旦ゲームを実行してみます。

デフォルト画像が表示され、その後DLC画像は表示されません。(当然、DlcBundles\StandaloneWindows64 フォルダにCatalog+AssetBundleを配置しているので、エラーも出ません)

これがDLC未購入者に提供するゲームの状態となります。

そしてDLC購入者に提供するゲームの状態 は、 dlc-settings.jsonenabledtrue に設定すれば実現できます。

DLC導入の再現

最後に、実際にユーザーにゲームを頒布し、DLC展開することを想定した一連の流れを再現してみます。

1. ゲーム本体の頒布(DLC未導入)

あらかじめ dlc-settings.jsonenabledfalse に設定されていることを確認し、ゲームをビルドします。

※本記事ではエラーがわかりやすいように Development Build をONにしています

この時点ではゲーム実行後 [Show image] ボタン押下時にエラーが出ます

プロジェクトフォルダの ServerData\StandaloneWindows64 に出力されたCatalog+AssetBundleを、ゲームフォルダの DlcBundles\StandaloneWindows64 に配置します。

以上でゲーム本体完成です。動作確認します。

エラーが出ないことが確認できたので、これをZIP化等して頒布します。

2. DLCの頒布

上記ゲーム本体を所持しているユーザーに対して、DLCデータを頒布します。

dlc-settings.jsonenabledtrue に設定し、Addressablesのビルドを実行します。

プロジェクトフォルダの ServerData\StandaloneWindows64 に出力されたCatalog+AssetBundleをZIP化等して頒布します。

実際にユーザーの立場になって、導入、動作確認をしてみます。

DLCとして頒布されるCatalog+AssetBundleを DlcBundles\StandaloneWindows64 に上書きで配置します。

ゲームを実行します。

正しくDLCが反映されました!

あとがき

Addressables初心者なりに、手探りでDLC実装方法を考えてみました。ただこうして改めて記事にしてみて、まだまだ改善点があるなぁと感じております。

例えば、DLC機能部分をゲーム本体に組み込むなら、Addressablesを使うのは enabled を管理するjsonだけにして、リソースにはAddressablesを使わないという選択もあったなぁ……などです。

これから実際にDLC有りのゲームを作ってみて、その知見をもとにもう少し実践的な使いやすいDLCの仕組みを考えていきたいなと思います。

参考記事

https://qiita.com/k7a/items/d27640ac0276214fc850
https://qiita.com/k7a/items/b4fd298bcb64dc968ad1
https://qiita.com/k7a/items/df6dd8ea66cbc5a1e21d
https://note.com/graffity/n/na33fd638be48
https://light11.hatenadiary.com/entry/2021/01/13/203111
https://www.hanachiru-blog.com/entry/2022/03/24/120000

Discussion