Game Server Services で魔法石を実装する

# Game Server Services(GS2) とは

[Game Server Services(GS2)](https://gs2.io/) はゲームサーバの mBaaS サービスです。
プレゼントボックスやスタミナ管理などゲームで使える様々なサーバ機能をマイクロサービス化して提供しています。
今回はその中でも　魔法石 を管理する **GS2-Money** を使ってみたいと思います。

魔法石をサーバサイドで実現するためにはアカウント管理も必要になるので、あわせて **GS2-Account** も使います。
自前でアカウント管理する場合や、別のアカウント認証サービスを利用する場合はそちらを利用することも出来ます。

## GS2-Money

> GS2-Money は 仮想通貨の管理機能を提供するサービスです。
> 
> 仮想通貨は資金決済法の前払式支払手段に該当します。
> GS2-Money は前払式支払手段のうち自家型と呼ばれる方式に対応します。
> そのため、発行した仮想通貨を自社提供アプリ以外のアプリから消費させる用途には使用できません。
> 
> GS2-Money は各単価ごとの仮想通貨の残高を管理します。
> たとえば、120円で5個の課金石を購入した場合は単価24円の課金石を5個所持している。
> 追加で1200円で60個の課金石を購入した場合は単価20円の課金石を60個所持している。と管理されます。
> 消費する際には単価の高い課金石から優先して消費されます。これは未使用残高をなるべく少なくするためです。
> ただし、無料で配布した石に関しては無料を優先するよう優先度が設定できます。
[全文はこちら](http://docs.gs2.io/services/enduser/money)

## GS2-Account

> GS2-Account は ユーザを識別するためのアカウント管理機能を提供するサービスです。
> 
> GS2-Account はデバイスの機種変更時にデータを引き継げるようにするために
> メールアドレス+パスワード や ソーシャルアカウントを利用したデータの引き継ぎ機能に対応しています。
[全文はこちら](http://docs.gs2.io/services/enduser/account)

## 利用料金

### GS2-Money

#### 仮想通貨のチャージ

| チャージ時点の仮想通貨の未使用残高 | 利用料金 |
|-----------------------------|----------:|
| 10万円未満 | 無料 |
| 10万円以上、1,000万円未満 | チャージ額の10% | 
| 1,000万円以上、5000万円未満 | チャージ額の8% | 
| 5000万円以上、1億円未満 | チャージ額の6% | 
| 1億円以上 | チャージ額の5% | 

#### API呼び出しに対する料金

> 3円 / 1,000 APIリクエスト の利用料金がかかります。
> ただし、仮想通貨のチャージによる利用料金と比較して API呼び出しに対する課金額のほうが少ない場合は、API呼び出しに対する課金は免除されます。

未使用残高が 10万円を超えたらチャージ額に対して課金されるけれど、10万円未満であればAPIリクエスト単価だけでいい。
『未使用残高』が10万円ということは、実際の取引額はもっと多くなる点に注意。
たとえば、購入された魔法石のうち9割がすぐに使われるのであれば月商100万円を超えたあたりからチャージ額に対する課金が行われるようになる。という感じです。

APIリクエストは残高の取得APIの呼び出しや購入後のレシート検証APIの呼び出しなどのリクエスト毎にカウントされます。

### GS2-Account

#### サービスクラス

| サービスクラス | 割り当てられるクオータ | 利用料金 |
|------------|-------------------:|--------:|
| account1.nano | 10/秒 | 3円/時 |
| account1.micro | 50/秒 | 15円/時  |
| account1.small |  100/秒 | 30円/時  |
| account1.medium | 300/秒 | 90円/時  |
| account1.large | 1,000/秒 | 300円/時  |
| account1.xlarge | 2,000/秒 | 600円/時  |
| account1.2xlarge | 4,000/秒 | 1200円/時  |

性能によって料金が異なる。
クオータというのが性能を表していて、APIを呼び出す毎にこのクオータを消費する。
APIの処理内容によって消費するクオータ量は異なる。

以下の消費クオータが例示されている。

| リクエスト | 消費クオータ |
|---------|-----------:|
| アカウントの作成 | 10 |
| アカウント一覧の取得 | 50件あたり5 |
| アカウント認証 | 3 |
| アカウント引き継ぎ情報の作成 | 10 |
| アカウント引き継ぎの実行 | 10 |

最小の account1.nano だと月額2,160円で秒間1〜3回。
次の account1.micro だと月額10,800円で秒間5〜16回。となる。

#### その他の料金

> 3円 / 1,000 APIリクエスト の利用料金がかかります。

GS2-Money と同様にAPI呼び出し回数に応じて課金される。

#### 無料枠

> アカウント登録から1年間 account1.nano が毎月 750時間 無料でご利用いただけます。
> 毎月APIリクエスト 100,000回 までは無料でご利用いただけます。100,000回を超過してからAPIリクエストの料金が発生します。

アカウント登録から1年間は一番安いものが1つタダで使えます。（1ヶ月は31日でも744時間）
APIリクエストも毎月10万リクエストまでタダで使えます。

# 初期設定

## GS2-Money を利用できる状態にする
![image.png](https://qiita-image-store.s3.amazonaws.com/0/100723/0a45b500-1557-9a2b-3e35-f2530e4ab4d1.png)

アカウント登録後、ログインしたら表示されるメニューから GS2-Formation を選択します。
GS2-Formation は GS2 を Unity から利用するために必要な設定をボタンポチで設定してくれるものです。

ここでボタンを押してしばらく待つと、Unity で使用する **クライアントID** と **クライアントシークレット** が発行されます。

## 販売する魔法石を登録する
![image.png](https://qiita-image-store.s3.amazonaws.com/0/100723/fc89bad5-d576-a8d0-6173-28a63f4c61b8.png)

GS2-Fromation 初期設定内容には GS2-Money は含まれていないので、ここは手動で設定する必要があります。
GS2-Money のページを開いて『仮想通貨の新規作成』を選択します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100723/31e88da5-9eb1-845e-a871-c35bd22a869e.png)

各設定項目に値を埋めていき、最後に『作成』を選択します。
このとき、レシート検証機能の利用 のチェックボックスにチェックをいれるのを忘れないようにします。

### AppStore や GooglePlay の設定をする。

細かい設定手順が [AppStore](http://docs.gs2.io/services/enduser/money/platform-settings/apple) と [GooglePlay](http://docs.gs2.io/services/enduser/money/platform-settings/google) それぞれに書かれているので、それに従って設定します。

### GS2-Money に魔法石の商品情報を登録する

魔法石を作ったら、次は 120円で10個。360円で40個。みたいな単価とそれで得られる魔法石の数を設定します。
まずは”その購入したらいくつの魔法石がもらえるのか”という商品を登録します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100723/3a78390d-f4cd-896d-a4f1-73a67172ee25.png)
作成した仮想通貨を選択し、下の『商品』というタブを選択し、『商品の新規作成』を選択します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100723/37e6fec7-440d-c49e-9c90-a8ae982b9edc.png)

適当な商品名と、その商品を購入したときに魔法石を付与する数を設定します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100723/4c42bde5-b352-72bf-a32a-3f388ba04a37.png)
次に、AppStoreやGooglePlayで購入した商品とGS2-Moneyの商品を紐付ける情報を登録します。

![image.png](https://qiita-image-store.s3.amazonaws.com/0/100723/58838921-7fa0-316d-f2e5-395fb71647d4.png)
商品IDには AppStore や GooglePlay で設定した商品のIDを設定します。
販売価格にはその商品を AppStore や GooglePlay でいくらで販売しているのかを設定します。

これで初期設定は完了です。

# 実装

## GS2-SDK のインストール

http://docs.gs2.io/sdk/gs2-sdk-for-unity/get-start
ここから GS2-SDK for Unity をダウンロードします。
Unity でプロジェクトを開いている状態でダウンロードしたファイルを開くとアセットのインストールが行えます。

## Unity IAP のインストール

GS2-Money は内部で Unity IAP を使用するので、Unity IAP もインストールします。
![image.png](https://qiita-image-store.s3.amazonaws.com/0/100723/4a74a5ef-9ff6-057f-c22c-2d262f403090.png)
右側にある In-App Purchasing を有効にして、アセットをインストールしてください。

## ユーザアカウントの作成

GS2-Account にユーザのアカウントを登録します。

```csharp
public class Test : MonoBehaviour
{
    private const string ClientId = "クライアントID";
    private const string ClientSecret = "クライアントシークレット";

    private const string GameName = "game-0001";

    private Client Gs2;

    private void Start()
    {
        Gs2 = new Client(new Profile()
            .WithClientId(ClientId)
            .WithClientSecret(ClientSecret));
        StartCoroutine (Action ());
    }

    private IEnumerator Action()
    {
        Account account = null;
        yield return client.Account.Create(
            r =>
            {
                if (r.Error != null)
                {
                    throw r.Error;
                }
                account = r.Result;
            },
            GameName);
        // account に登録したアカウント情報が入っている
    }

}
```

`Account` には発行されたユーザIDとパスワードが格納されており、それを使用して認証を行います。
次回ログイン時にも使用するので、ローカルストレージに保存しておきます。

## ログイン

ログインには GS2-Account を使用してユーザ認証をし、GS2-Auth でアクセストークンを発行します。
独自のアカウントシステムを持つ場合は、独自のアカウントシステムで認証後にサーバサイドSDKを使用して GS2-Auth でアクセストークンを取得し、クライアントに返すことになります。

```csharp
public class Test : MonoBehaviour
{
    private const string ClientId = "クライアントID";
    private const string ClientSecret = "クライアントシークレット";

    private const string GameName = "game-0001";
    private const string KeyName = "account";
    
    private const string UserId = "ユーザID";
    private const string Password = "パスワード";

    private Client Gs2;

    private void Start()
    {
        Gs2 = new Client(new Profile()
            .WithClientId(ClientId)
            .WithClientSecret(ClientSecret));
        StartCoroutine (Action ());
    }

    private IEnumerator Login(UnityAction<AsyncResult<GameSession>> callback, string userId, string password)
    {
        string authenticationToken = null;
        yield return Gs2.Account.Authentication(
            result =>
            {
                if (result.Error != null)
                {
                    callback.Invoke(
                        new AsyncResult<GameSession>(
                            null,
                            result.Error));
                }
                else
                {
                    authenticationToken = result.Result;
                }
            },
            GameName,
            KeyName,
            userId,
            password);
        yield return Gs2.Auth.Login(
            callback,
            userId,
            KeyName,
            authenticationToken);
    }

    private IEnumerator Action()
    {
        GameSession session = null;
        yield return Login(
            r =>
            {
                if (r.Error != null)
                {
                    throw r.Error;
                }
                session = r.Result;
            },
            UserId,
            Password);
        // session にログインセッションが返ります
        // ログインセッションは今後様々なAPI呼び出しに必要となります
    }

}
```

GS2-Account の認証リクエストにはユーザID/パスワードの他に KeyName をというパラメータを渡していますが、
これは GS2-Key という暗号化サービスで登録した暗号鍵の名前です。
GS2-Formation で初期設定した場合は 「account」 という名前の暗号鍵が用意されていますので、それを使います。
GS2-Account の認証結果は GS2-Key の暗号鍵で暗号化され、クライアントには改ざん不可能な状態で GS2-Auth のログインリクエストに引き渡されます。

無事ログインが出来たら GameSession インスタンスが手に入ります。
GameSession インスタンスはログイン情報を格納したインスタンスで、認証が必要な様々なAPIを呼び出す際に必要となります。

## 残高の取得

```csharp
public class Test : MonoBehaviour
{
    private const string ClientId = "クライアントID";
    private const string ClientSecret = "クライアントシークレット";

    private const string MoneyName = "money-0001";

    private Client Gs2;

    private void Start()
    {
        Gs2 = new Client(new Profile()
            .WithClientId(ClientId)
            .WithClientSecret(ClientSecret));
        StartCoroutine (Action ());
    }

    private IEnumerator Action()
    {
        Wallet wallet = null;
        yield return Gs2.Money.GetWallet(
            r =>
            {
                if (r.Error != null)
                {
                    throw r.Error;
                }
                wallet = r.Result;
            },
            0); // 財布のスロット番号
        // wallet に所持している魔法石の情報が返ります
    }
}
```

財布のスロット番号とは iOS は 0 / Android は 1 のように区別することで、
各プラットフォームで購入した魔法石が混ざらないようにすることが出来ます。
また、無償付与の魔法石は全てのスロットで共有する。というような設定も出来ます。

Wallet には Paid と Free というプロパティがあり、有償付与魔法石と無償付与魔法石 それぞれの残高が取得できます。

## 魔法石の購入

GS2-Money で魔法石を購入するには事前準備が必要です。

```
Gs2.Money.Dispatch(
	e =>
	{
		// e に発生したイベントが渡ります
	},
	Session,
	MoneyName);
```

このディスパッチ関数を毎フレーム呼び出す必要があり、
`Gs2.Money.Status` が返す値が `Initialized` になるまで待つ必要があります。

これは毎フレーム `Gs2.Money.Status` の値を確認するのもいいですが、
`// e に発生したイベントが渡ります` の部分で EventType.InitializeComplete が返るのを待つ方法もあります。

初期化が終了したら

```
Product[] products = Gs2.Money.ListItem();
```

で販売中の商品が取得できます。
Product は Unity IAP のモデルで以下にリファレンスがあります。
https://docs.unity3d.com/ja/540/ScriptReference/Purchasing.Product.html

```
Product[] products = Gs2.Money.ListItem();
Gs2.Money.Buy(
	0,
	products[0]);
```

消費するスロット番号と購入する商品を指定して Buy を呼び出します。
すると、`Gs2.Money.Status` が `Purchasing` になり、しばらくすると `Verifing` になります。
購入処理が完了すると `Initialized` に戻ります。

イベントに `EventType.PurchaseComplete` または `EventType.PurchaseFailed` が返れば購入処理は完了です。

## 魔法石の消費

魔法石の購入処理が実装できたら、次は消費処理です。

```csharp
public class Test : MonoBehaviour
{
    private const string ClientId = "クライアントID";
    private const string ClientSecret = "クライアントシークレット";

    private const string MoneyName = "money-0001";

    private Client Gs2;

    private void Start()
    {
        Gs2 = new Client(new Profile()
            .WithClientId(ClientId)
            .WithClientSecret(ClientSecret));
        StartCoroutine (Action ());
    }

    private IEnumerator Action()
    {
        Wallet wallet = null;
        yield return Gs2.Money.ConsumeWallet(
            r =>
            {
                if (r.Error != null)
                {
                    throw r.Error;
                }
                wallet = r.Result;
            },
            0,      // 魔法石を消費するスロット番号
            100,    // 消費する魔法石の数量
            0,      // 消費用途
            false); // 有償付与の魔法石のみ消費対象とするか
        // wallet に消費後の魔法石の情報が返ります
    }
}
```

これで魔法石の消費が行えます。

# 最後に

小規模なゲームアプリでは広告を使ったマネタイズが主流だと思います。
GS2-Money を使用すれば、少ないコストで魔法石の機能を実装できます。

また、魔法石を取り扱うのであれば、機種変更時などのアカウントの引き継ぎ機能を実装したいところですが、
GS2-Account にはアカウントの引き継ぎ機能もありますので、引き継ぎも簡単に実装できます。