#03 所持品の管理 | Game Server Services(GS2)

https://www.youtube.com/watch?v=B-y6uRFbWjU

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/6f9e4ffa-253f-ca1c-36f4-21b16aabc501.png)

第三回は GS2 を使ってアイテムの所持数量を管理する方法について解説します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/a19bf9df-4890-67f7-e930-2518d537aab2.png)

GS2 の所持品を管理するためのマイクロサービスは GS2-Inventory です。
今回はこの GS2-Inventory を中心に解説を進めたいと思います。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/aa20e760-dd99-994b-e06a-cfd958708175.png)

まず最初に GS2-Inventory のデータ構造について説明します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/6dbc4f04-25da-5aec-dabf-8a3450253f66.png)

こちらが、GS2-Inventory のデータ構造です。
色々と要素があり、複雑ですが一つずつ説明していきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/04fab95b-c2c9-ad07-3dea-1681b09d4cd4.png)

ネームスペース配下には、マスターデータとユーザーデータの2種類のデータがあります。
初期設定として GS2 に設定するのがマスターデータ、プレイヤーが遊ぶたびに蓄積されていくデータがユーザーデータです。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/3aa64b6d-e729-2d74-eae8-d03567a80e7b.png)

マスターデータの配下には、 Inventory Model データが複数あります。
Inventory Model は、プレイヤーが持っているカバンの種類を表現します。

たとえば、キャラクターを入れるカバン、強化素材を入れるカバン、消費型アイテムを入れるカバン のようにアイテムの大カテゴリごとに用意することになります。
Inventory Model にはカバンに入れられるアイテムの種類の最大数を指定できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/97838523-365c-2735-93e8-bd78610e2266.png)

Inventory Model 配下には Item Model があります。
Item Model は より具体的なアイテムの種類を表現します。
消費型アイテム用の Inventory Model の配下には ポーションの Item Model を配置するような感じです。

Item Model にはアイテムを 『ポーション x 99』 のように1つの領域でスタックして保持できる最大数を指定したり、同じ種類のアイテムを複数スタック所有できるかを設定できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/661344cf-b0c9-0e17-d1fa-cd9bd2fcd121.png)

ゲームプレイヤーが GS2-Inventory の機能を利用すると、Inventory が作成されます。

Inventory は Inventory Model と 1対1 の関係があり、現在の最大アイテム格納数が記録されています。
Inventory のキャパシティはユーザーごとに異なる値を設定できます。たとえば、初期状態では50種類のアイテムを所有できるが、拡張アイテムを使用することで最大60個に拡張するようなことも実現できます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/e04ff353-3a30-51db-7007-478ebbba3e41.png)

Inventory の配下には、Item Set があります。
Item Model と Item Set は 1対多 の関係があります。

たとえば、同一アイテムの種類でも、スタックが複数に分かれている場合や、アイテムの有効期限が異なる場合は複数の要素がぶら下がることになります。

アイテムのスタックが複数に分かれた時、Inventory のキャパシティはスタックの数だけ消費されることに注意してください。
例えば、Inventory に99個スタック可能なポーションを合計200個持っている場合について考えてみましょう。
99個のスタック、99個のスタック、2個のスタック と合計3種類のスタックが作成され、Inventory の使用状況は 3 となります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/3050cf30-5753-406d-b8a0-4e05f34fa09c.png)

GS2-Inventory の代表的なAPIを確認してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/bcc2d48d-026e-fce3-b305-7b5837478ecc.png)

GS2-Inventory の特性から大体想像がつく API が用意されていると思います。

これらの API を大きく分類すると2種類に分類することができます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/f9c73f1a-4a55-672e-39c4-9deba0de51c6.png)

左側に記載した API はゲーム内から呼び出しても安全な API です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/0ba04183-d323-dd71-8b61-daeb4cbc3fac.png)

右側に記載した API は、ゲーム内から呼び出せると不正行為につながる恐れのある API です。
これまでのセッションの中で GS2-Identifier で ApplicationAccess という権限を割り当ててきましたが、このような API は ApplicationAccess 権限のクレデンシャルでは呼び出せないようになっています。

それではアイテムを入手することはできないのか？という疑問が生まれます。

GS2 では「報酬を得るためには、何らかの対価を支払わなければならない」という思想をベースにシステムが設計されています。
たとえば「クエストをクリアすると、報酬としてアイテムを入手できる」というのはわかりやすい例でしょう。
クエストは1度しか受託できない場合、「クエストを攻略した」という状態のフラグを立てることを対価として、報酬を受け取れる という考え方になります。
このような対価と報酬の設定および操作は GS2 が提供する GS2-Quest や GS2-Showcase といった GS2-Inventory 以外のマイクロサービスで管理され、そのようなシステムを通じてアイテムを入手することが想定されています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/73a03460-2c7d-4cb5-c2cb-fa8a106dcc03.png)

マスターデータは事前にGS2に登録しておく必要があります。
Inventory Model や Item Model の情報を記録した JSONファイルをアップロードすることで設定が可能です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/ac133c5d-7103-ba15-06f8-a7ecca189e12.png)

GS2 のドキュメントページで JSON の形式を確認できます。

GS2-Inventory のマスターデータは、マスターデータのフォーマットバージョンと Inventory Model の配列です。
Inventory Model は Item Model の配列を持っており、インベントリに保持可能なアイテムの一覧を表現します。

このようなマスターデータは JSON ファイルを直接記述することもできますが、GS2 のマネージメントコンソールのマスターデータエディタ を使用すればUIで作成することもできます。
しかし、最終的には Excel などを使用してアイテムの一覧を管理し、マスターデータのJSON形式でエクスポートするマクロを用意するのが一番運営に向いているでしょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/224ff1e3-4e13-bb37-154e-4a68ac463804.png)

これで GS2 におけるアイテム管理の学習は終了です。

それでは、ここからは実際にコードを記述して GS2-Inventory の機能を利用してみましょう。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/781aca82-c202-2826-5604-094e2c98446c.png)

GS2-Inventory に関するリソースの作成を GS2 マネージメントコンソールを通して実行してみましょう。

サイドメニューから Inventory > Namespaces を選択します。
“Inventory” という名前でネームスペースを作成します。

ネームスペースの詳細ページを開いたら、Master Data Editor タブを選択します。
ここでマスターデータをUI上で作成し、JSONファイルを生成できます。

Inventory Model を Bag という名前で作成します。
キャパシティを入力する必要がありますので、適当に10種類までアイテムを所持できるようにします。

次に作成した Inventory Model を選択して、Item Model を作成します。
“Potion” という名前で、99個スタック可能なアイテムを作成します。
続けて、もう一種類 “Herb” という名前のアイテムを作成します。

ネームスペース詳細ページまで戻り、マスターデータをエクスポートします。
JSON ファイルが出来上がったら、マスターデータとしてアップロードします。

これで、リソースの用意は完了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/9db196f5-55b9-504e-3985-fe4d4a0cce68.png)

それでは、実際にコードを記述して GS2-Inventory の機能を利用してみましょう。

コードベースには第一回で作成したログインプログラムを使用します。
のちの作業のためにログインしたユーザーのユーザーIDを出力します。

アクセストークンを指定してログイン中のユーザーの Bag という名前のインベントリ内の ItemSet の一覧を取得します。

```cs
using Cysharp.Threading.Tasks;
using Gs2.Core.Model;
using Gs2.Unity.Core;
using Gs2.Unity.Util;
using UnityEngine;

public class NewBehaviourScript : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        async UniTask Impl()
        {
            Debug.Log("Start");
            
            var profile = new Profile(
                "YourClientId",
                "YourClientSecret",
                new Gs2BasicReopener(),
                Region.ApNortheast1
            );
            await profile.InitializeAsync();
            
            Debug.Log("Initialized");

            var gs2 = new Gs2Domain(profile);
            
            var userId = PlayerPrefs.GetString("UserId");
            var password = PlayerPrefs.GetString("Password");

            if (userId == "" || password == "")
            {
                Debug.Log("Create Account");
                
                var namespaceObject = gs2.Account.Namespace(
                    namespaceName: "Account"
                );
                var createResult = await namespaceObject.CreateAsync(
                );
                var account = await createResult.ModelAsync();
                
                Debug.Log("Save Account");
                
                PlayerPrefs.SetString("UserId", account.UserId);
                PlayerPrefs.SetString("Password", account.Password);
                PlayerPrefs.Save();

                userId = account.UserId;
                password = account.Password;
            }
            
            Debug.Log("Authentication");
            
            var accountObject = gs2.Account.Namespace(
                namespaceName: "Account"
            ).Account(
                userId: userId
            );
            var authenticationResult = await accountObject.AuthenticationAsync(
                keyId: "grn:gs2:ap-northeast-1:xlSzJ6ni-tutorial:key:Key:key:AuthenticationKey",
                password: password
            );
            var body = authenticationResult.Body;
            var signature = authenticationResult.Signature;
            
            Debug.Log("Login");
            
            var accessTokenObject = gs2.Auth.AccessToken(
            );
            var result = await accessTokenObject.LoginAsync(
                keyId: "grn:gs2:ap-northeast-1:xlSzJ6ni-tutorial:key:Key:key:AuthenticationKey",
                body: body,
                signature: signature
            );

            var accessToken = await result.ModelAsync();
            
            Debug.Log(accessToken.AccessToken.UserId);

            var it = gs2.Inventory.Namespace(
                "Inventory"
            ).Me(
                accessToken
            ).Inventory(
                "Bag"
            ).ItemSetsAsync();

            await foreach (var item in it)
            {
                Debug.Log(item.ItemName + ": " + item.Count);
            }
        }

        StartCoroutine(Impl().ToCoroutine());
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}

```

実装ができましたので、作成したプログラムを実行してみましょう。

ユーザーIDが出力された後、特に何も表示されません。何もアイテムを所有していないので当然ですね。
続けて、マネージメントコンソールでアイテムを付与してみましょう。
出力されたユーザーIDをコピーして、マネージメントコンソールのユーザーデータ管理機能のターゲットユーザーID欄に指定します。
Bagインベントリが表示されますので、詳細を選択してアイテムの入手処理を行います。

Potion を 5個、Herb を 3個 付与します。
これで再実行してみましょう。
先ほどはユーザーIDが表示された後、何も出力されませんでしたが、今回はアイテムの名前と所持数量が表示されました。
ちゃんと付与した分が反映されています。

続けて、アイテムを消費させてみましょう。

アイテム一覧を表示した後で、Potion を1つ消費してみます。
ItemSet は複数スタック存在する可能性があるため、”Potion” の指定だけでは不十分です。
スタックごとに割り当てられる Item Set ID を指定する必要がありますが、null を指定しておくと最も量が少ないスタックから消費してくれます。

消費した後の状態を知りたいため、もう一度アイテムの一覧を表示しましょう。

```cs
using Cysharp.Threading.Tasks;
using Gs2.Core.Model;
using Gs2.Unity.Core;
using Gs2.Unity.Util;
using UnityEngine;

public class NewBehaviourScript : MonoBehaviour
{
    // Start is called before the first frame update
    void Start()
    {
        async UniTask Impl()
        {
            Debug.Log("Start");
            
            var profile = new Profile(
                "YourClientId",
                "YourClientSecret",
                new Gs2BasicReopener(),
                Region.ApNortheast1
            );
            await profile.InitializeAsync();
            
            Debug.Log("Initialized");

            var gs2 = new Gs2Domain(profile);
            
            var userId = PlayerPrefs.GetString("UserId");
            var password = PlayerPrefs.GetString("Password");

            if (userId == "" || password == "")
            {
                Debug.Log("Create Account");
                
                var namespaceObject = gs2.Account.Namespace(
                    namespaceName: "Account"
                );
                var createResult = await namespaceObject.CreateAsync(
                );
                var account = await createResult.ModelAsync();
                
                Debug.Log("Save Account");
                
                PlayerPrefs.SetString("UserId", account.UserId);
                PlayerPrefs.SetString("Password", account.Password);
                PlayerPrefs.Save();

                userId = account.UserId;
                password = account.Password;
            }
            
            Debug.Log("Authentication");
            
            var accountObject = gs2.Account.Namespace(
                namespaceName: "Account"
            ).Account(
                userId: userId
            );
            var authenticationResult = await accountObject.AuthenticationAsync(
                keyId: "grn:gs2:ap-northeast-1:xlSzJ6ni-tutorial:key:Key:key:AuthenticationKey",
                password: password
            );
            var body = authenticationResult.Body;
            var signature = authenticationResult.Signature;
            
            Debug.Log("Login");
            
            var accessTokenObject = gs2.Auth.AccessToken(
            );
            var result = await accessTokenObject.LoginAsync(
                keyId: "grn:gs2:ap-northeast-1:xlSzJ6ni-tutorial:key:Key:key:AuthenticationKey",
                body: body,
                signature: signature
            );

            var accessToken = await result.ModelAsync();
            
            Debug.Log(accessToken.AccessToken.UserId);

            var it = gs2.Inventory.Namespace(
                "Inventory"
            ).Me(
                accessToken
            ).Inventory(
                "Bag"
            ).ItemSetsAsync();

            await foreach (var item in it)
            {
                Debug.Log(item.ItemName + ": " + item.Count);
            }

            await gs2.Inventory.Namespace(
                "Inventory"
            ).Me(
                accessToken
            ).Inventory(
                "Bag"
            ).ItemSet(
                "Potion",
                null
            ).ConsumeAsync(
                1
            );
            
            it = gs2.Inventory.Namespace(
                "Inventory"
            ).Me(
                accessToken
            ).Inventory(
                "Bag"
            ).ItemSetsAsync();

            await foreach (var item in it)
            {
                Debug.Log(item.ItemName + ": " + item.Count);
            }
        }

        StartCoroutine(Impl().ToCoroutine());
    }

    // Update is called once per frame
    void Update()
    {
        
    }
}
```

Unity に戻って実行してみます。
Potion が 5個 から 4個 に減っています。
マネージメントコンソールに戻り、サーバー上でも減っていることを確認します。
確かにサーバー上でも4個に減っています。完璧ですね。

これでコーディングと動作確認は完了です。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/f2e2d35e-c045-977d-3119-7086793856a8.png)

マネージメントコンソールで作成したリソースを GS2-Deploy のテンプレートにして、再現性のある環境構築を行いましょう。

まずは Inventory 用のテンプレートファイルを作成しましょう。
1ファイルにすべてのリソースを詰め込んでもいいですが、役割ごとにファイルを分割するとわかりやすくなることもあります。
たとえば、特定のイベント用のクエストやミッションの場合、イベント期間が終了したらスタックを削除することでイベント関連のリソースをすべて削除できたりします。
分けないメリット、分けるメリットそれぞれありますので、自分に合ったテンプレート運用を探ってみるといいでしょう。

GS2-Inventory に “Inventory” という名前でネームスペースを作成します。
ネームスペースを作成したら、マスターデータを設定します。マスターデータは CurrentItemModelMaster リソースで指定できます。
Settings には JSON の内容を指定します。ちょうど先ほどエクスポートしたマスターデータがありますので、それを貼り付けて利用しましょう。

```yaml
GS2TemplateFormatVersion: "2019-05-01"
Description: GS2 SDK identifier template Version 2019-07-10

Globals:
  Alias:
    InventoryNamespaceName: Inventory

Resources:
  InventoryNamespace:
    Type: GS2::Inventory::Namespace
    Properties:
      Name: ${InventoryNamespaceName}
  
  InventoryMasterData:
    Type: GS2::Inventory::CurrentItemModelMaster
    Properties:
      NamespaceName: ${InventoryNamespaceName}
      Settings: |
        {
          "version": "2019-02-05",
          "inventoryModels": [
            {
              "name": "Bag",
              "initialCapacity": 10,
              "maxCapacity": 10,
              "protectReferencedItem": false,
              "itemModels": [
                {
                  "name": "Harb",
                  "stackingLimit": 99,
                  "allowMultipleStacks": false,
                  "sortValue": 1
                },
                {
                  "name": "Potion",
                  "stackingLimit": 99,
                  "allowMultipleStacks": false,
                  "sortValue": 0
                }
              ]
            }
          ]
        }
    DependsOn:
      - InventoryNamespace
```

これでマスターデータの作成は完了です。デプロイしてみましょう。

まずは手動で作成した GS2-Inventory のネームスペースを削除します。
GS2-Deploy に Inventory という名前でテンプレートを作成し、テンプレートファイルをアップロードします。
デプロイが完了したら、GS2-Inventory のネームスペースを選択し、ユーザーに再び Potion と Herb を付与します。

Unity に戻り、実行してみます。
ちゃんと動作しました。これで再現性がある環境も作れました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/497fb326-81a5-52f4-a6b4-59fa9b5f4d8a.png)

次回予告！

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/100723/4c59f197-7c0f-2910-f3c1-db5c684e7c34.png)

たったこれだけの作業でアイテムの管理機能が使えるのは、驚きだったのではないですか？
しかし、アイテムの入手をマネージメントコンソールから実行しましたが、それをリリースするゲームで行うのは現実的ではありませんね。
ゲーム内の操作で一日一回だけアイテムがもらえる仕組みを用意してみましょう。

次回もお楽しみに。