GS2 Blog

Game Server Services(https://gs2.io/) の最新情報をお届けします

GS2-Inventory のシンプルインベントリに入手・消費時のスクリプトを設定できるようになりました

はじめに

GS2-Inventory はプレイヤーの所持品を管理するマイクロサービスです。
なかでも、シンプルインベントリはアイテム所持数の上限やインベントリの容量といった機能は持たない代わりに複数のアイテムをまとめて増減できる特徴があります。

機能の詳細は以下の記事を参照ください。

gs2.hatenablog.com

スクリプトは、GS2 内のマイクロサービスで何かイベントが生じた時に追加の処理をする仕組みです。
GS2-Script に Lua で定義したスクリプト、もしくは Amazon Event Bridge を使用して AWS 上で処理を実行できます。

追加された機能の詳細

これまでスタンダードインベントリの増減時のスクリプトは定義可能でしたが、シンプルインベントリに関しては未対応でした。
今回シンプルインベントリもスクリプトの実行に対応しました。

入手時に実行されるスクリプト

namespace = args.namespace
inventoryName = args.inventoryName
simpleItems = args.simpleItems
userId = args.userId
acquireCounts = args.acquireCounts

result = {
  permit=permit,
  overrideAcquireCounts=overrideAcquireCounts
}

消費時に実行されるスクリプト

namespace = args.namespace
inventoryName = args.inventoryName
simpleItems = args.simpleItems
userId = args.userId
consumeCounts = args.consumeCounts

result = {
  permit=permit,
  overrideConsumeCounts=overrideConsumeCounts
}

限界突破(凸)機能が追加されました

はじめに

限界突破とは、同一キャラクターを合成したり、特定の進化素材を使用することでキャラクターを強化する仕組みを指します。
限界突破を行うと、キャラクターのレベル上限が引き上げられ、より強力に育成することが可能となります。

機能追加の背景

GS2-Experience はレベルキャップの引き上げはサポートしていましたが、レアリティごとに初期値が異なったりするケースの取り回しは必ずしも扱いやすい状態とは言えませんでした。

今回 GS2-Grade というマイクロサービスを追加することで、レベル上限の管理をより直感的に行えるようにしました。

追加された機能の詳細

GS2-Grade はグレードとレベルキャップの組み合わせのみを管理し、グレードの上昇方法についてはスコープ外としています。
初期グレードはプロパティIDの正規表現にマッチするかどうかで設定が可能となっています。

具体的な例を示しましょう。
以下のような条件でグレードが定義されていたと仮定します。

GradeEntryModel

グレード ゲーム上の表現 レベルキャップ
1 ☆☆★ 50
2 ☆★★ 60
3 ★★★ 70
4 ★★★+1 75
5 ★★★+2 80

キャラクターには以下の種類があるとします。

ItemModel

キャラクターID 初期グレード
N-0001 ☆☆★
N-0002 ☆☆★
N-0003 ☆☆★
R-0001 ☆★★
R-0002 ☆★★
R-0003 ☆★★
SR-0001 ★★★
SR-0002 ★★★
SR-0003 ★★★

この場合、初期グレード用の正規表現

DefaultGrade

グレード ItemModel に対して適用する正規表現
2 ^R-.*$
3 ^SR-.*$

となります。(いずれにも該当しない場合初期グレードは1になります)

このような情報を組み合わせた型として

GradeModel

名前
defaultGrades DefaultGrade[]
experienceModelId レベルキャップを反映するGS2-Experience の経験値モデルID
gradeEntries GradeEntryModel[]

このようなデータをマスターデータとして GS2-Grade に登録します。

GS2-Grade にはグレードの更新API(加算・減算・上書き)が用意されており、グレードの値を書き換えると同一プロパティIDの GS2-Experience のランクキャップが自動的に更新されます。

GS2-Enhance の更新内容

GS2-Grade をより効率的に扱うための機能追加が GS2-Enhance に対して行われました。
GS2-Enhance は素材となるアイテムやキャラクターを消費して経験値を得る仕組みでしたが、新しく限界突破(凸)をするための専用のインターフェースが追加されました。

GS2-Enhance がサポートする限界突破は同一キャラクターや同一属性を合成するようなケースで利用できるもので、素材を使用した限界突破であれば、GS2-Exchangeなどを使用して素材を消費してグレードを加算する交換レートを用意するだけで実装できます。

GS2-Enhance は素材が同種のキャラクターであることを検証した上でグレードを上昇させるトランザクションを発行する仕組みをより簡便に実装できるようにすることを目的に機能追加されています。

そのため、GS2-Grade の GradeEntryModel には素材が同種であることを判定するための正規表現を定義することが可能です。
グレードごとに条件が設定できますので、グレード1 の時は同属性であればどんなキャラクターでもいいが、グレード3以降は完全に同種のキャラクターでなければ限界突破できないような設定も可能です。

GS2-Enhance で利用する同種判定のための正規表現は以下の構造を持ちます。

GradeEntryModel

名前
propertyIdRegex 限界突破対象のプロパティIDから変数を取り出すための正規表現
gradeUpPropertyIdRegex グレードアップに利用可能な素材を判定するための正規表現

propertyIdRegex の入力例

grn:gs2:{region}:{ownerId}:inventory:namespace-0001:user:(.*):inventory:character:item:(.*):.*

gradeUpPropertyIdRegex の入力例

grn:gs2:{region}:{ownerId}:inventory:namespace-0001:user:$1:inventory:character:item:$2:.*

プロパティIDから、propertyIdRegex でユーザーIDとアイテム名をキャプチャし、gradeUpPropertyIdRegex の $1, $2 に値を反映して正規表現を作成しています。
この正規表現にマッチするプロパティIDのリソースのみが限界突破に利用できる素材となります。

GS2-StateMachine に投機的実行機能が追加されました

はじめに

GS2-StateMachine はステートマシンをサーバーで管理することで、ゲーム開発者が定義した独自のゲームロジックを、チート耐性が高い状態で実行する仕組みです。

GS2-StateMachine の詳細は以下の記事で解説しています。

gs2.hatenablog.com

機能追加の背景

ステートマシン管理機能は柔軟な処理を、安全に実行できる仕組みです。
しかし、欠点としてステートマシンにメッセージを送信するたびに通信をしなければならないため、通信頻度の高いゲーム性ではプレイ体験の面でもGS2のAPI呼び出しコストの面でも課題がありました。

追加された機能の詳細

ステートマシン定義をサーバーとゲームで共有し、ゲームはステートマシン定義に基づいてステートマシンを実行することで通信待ちせずに投擲に処理を実行し
ステートマシンに送信したメッセージや、状態遷移した際の状態変数の変化を一定間隔でまとめて GS2-StateMachine に送信し、GS2-StateMachine は受け取ったイベントを元に同じステートマシン定義を使用したステートマシンで状態を再現しつつ、状態変数の変化が正しいかを検証することができるようになりました。

以下のメカニズムで処理が実行されます。

これにより、ステートマシンの実行にあたって通信待ちをすることなく、通信回数を減らすことが可能となり、GS2-StateMachine がより扱いやすいものになりました。

詳細なドキュメントは以下をご確認ください。

docs.gs2.io

トランザクションをより細かい粒度で待てるようになりました

はじめに

GS2 におけるトランザクションとは、GS2-Showcase の商品購入APIの戻り値 や GS2-Exchange の交換実行API の戻り値です。
このような複数のマイクロサービスの値を書き換える動作は非同期処理で実行されます。

この処理単位を 「トランザクション」 と呼んでいます。

gs2.hatenablog.com

そして、8月には非同期処理の完了を待てるように SDK を拡張しました。

機能追加の背景

ガチャを例に説明します。ガチャは一般的に以下のプロセスで実行されます。

消費「GS2-Money から課金通貨を300減らす」
入手「GS2-Lottery でガチャを実行する」

そして、ガチャを実行すると新しいトランザクションが発行されます。

消費 なし
入手「ガチャで排出されたキャラクターを GS2-Inventory に格納する」

8月に追加した内容は、トランザクション内で新しいトランザクションが発生した時にその結果も待つかどうかのオプションがあります。

var transaction = await gs2.Showcase(
  "namespace"
).Me(
  gameSession
).Showcase(
  "showcase"
).DisplayItem(
  "displayItem"
).BuyAsync(
  1
);
await transaction.WaitAsync();

このように記述すると

消費「GS2-Money から課金通貨を300減らす」
入手「GS2-Lottery でガチャを実行する」

の部分だけを待って、アイテムの入手処理は終わっていない状態で処理が返ります。
これによって、通信待ちの時間を最小にして抽選結果に応じた演出を開始し、抽選の演出している間にアイテムの付与が行えました。

var transaction = await gs2.Showcase(
  "namespace"
).Me(
  gameSession
).Showcase(
  "showcase"
).DisplayItem(
  "lottery1"
).BuyAsync(
  1
);
await transaction.WaitAsync(true);

このように記述すると

消費「GS2-Money から課金通貨を300減らす」
入手「GS2-Lottery でガチャを実行する」

消費 なし
入手「ガチャで排出されたキャラクターを GS2-Inventory に格納する」

の全てを待つことができました。
しかし、これではトランザクションがもう少し複雑な構造を持っている場合に、完全に処理を待つことしかできませんでした。

追加された機能の詳細

WaitAsync の第一引数に何も指定しない場合に続きのトランザクションオブジェクトを返すようになりました。

この機能の実例として複雑なトランザクションを追いながら解説します。
10連ガチャで9個と1個で異なる GS2-Lottery の抽選ロジックを適用する場合を考えてみましょう。

最初のトランザクション

消費「GS2-Money から課金通貨を3000減らす」
入手「GS2-Lottery で LotteryA を9回実行する」
入手「GS2-Lottery で LotteryB を1回実行する」

となりますが、このような入手処理が複数存在する場合は GS2 においてはジョブキューを経由するように変換されます。
つまり、以下のようになります。

消費「GS2-Money から課金通貨を3000減らす」
入手「GS2-JobQueue に《GS2-Lottery で LotteryA を9回実行する》と《GS2-Lottery で LotteryB を1回実行する》ジョブを登録」
JobQueue を実行し
《GS2-Lottery で LotteryA を9回実行する》と《GS2-Lottery で LotteryB を1回実行する》を実行し、最大10個のトランザクションを発行
(最大としているのは同一アイテムが排出された場合、トランザクション処理の最適化処理で数が減ることがあるため)
入手「ガチャで排出されたキャラクターを GS2-Inventory に格納する」× 最大10個

内部的には 最大11個のトランザクション、2個のジョブキューで構成されますが
SDK上は開発者がより直感的に扱えるように、上記の3段階のトランザクションステップで扱い、トランザクションステップ単位で処理を待つことができるようになりました。

つまり、改めてコードを示すと

var transaction = await gs2.Showcase(
  "namespace"
).Me(
  gameSession
).Showcase(
  "showcase"
).DisplayItem(
  "lottery10"
).BuyAsync(
  1
);
var transaction2 = await transaction.WaitAsync();
await transaction2.WaitAsync();

と記述することで、2つ目のトランザクションステップまで実行を待つことができます。
つまり

消費「GS2-Money から課金通貨を3000減らす」
入手「GS2-JobQueue に《GS2-Lottery で LotteryA を9回実行する》と《GS2-Lottery で LotteryB を1回実行する》ジョブを登録」

JobQueue を実行し
《GS2-Lottery で LotteryA を9回実行する》と《GS2-Lottery で LotteryB を1回実行する》を実行し、最大10個のトランザクションを発行

までということになります。

これで、ガチャの抽選結果が確定しつつ、アイテムをインベントリに追加する前までトランザクション処理を待つことができるようになりました。

SimpleInventory / BigInventory に任意の所持数量を設定できるようになりました

はじめに

SimpleInventory はアイテムの所持数量を管理する仕組みで、通常の Inventory が持つスタックサイズの制限や、Inventory の容量制限の管理の仕組みがない代わりに、複数のアイテムを同時に増減することが可能なインベントリです。

BigInventory はアイテムの所持数量を int64 の範囲を超えて保持できるインベントリです。

今回の更新の背景

アイテムの所持数量の操作は、これまで増減APIを通して実行することになっていました。
しかし、任意の数量を持っている状況を作り出したい時に、すでに所有している数量を考慮した上で増減量を調整して処理を実行するのはロジックが関わり不具合の原因となりかねない処理を組み込む必要がありました。

今回の更新の詳細

setSimpleItemsByUserId
GS2-Inventory SDK API リファレンス | Game Server Services | Docs

setBigItemByUserId
GS2-Inventory SDK API リファレンス | Game Server Services | Docs

APIが追加され、それぞれに対応したトランザクション入手アクションとして

Gs2Inventory:SetSimpleItemsByUserId
GS2-Inventory トランザクションアクション | Game Server Services | Docs

Gs2Inventory:SetBigItemByUserId
GS2-Inventory トランザクションアクション | Game Server Services | Docs

が追加されました。

サーバーから取得した時刻を基準とした現在時刻取得機能が追加されました

はじめに

イベントの開始時刻や終了時刻など運営型ゲームでは時間の取り扱いは重要です。

機能追加の背景

一方で、ゲームが動作するデバイスの時刻設定は必ずしも正確とは言えず、正しくUIを表現するのは難しいです。
サーバーから取得した時刻を使用して、現在時間を補正することが一般的に行われており、それによってより正確なUIを表現することができるようになります。

GS2 では元々 GS2-Realtime::Now というAPIで現在時刻を取得できました。
しかし、それはその瞬間の時刻の取得であり、刻々と変化する時刻やスリープ復帰後にも問題なく動作するような仕組みはゲーム開発者が構築する必要がありました。

追加された機能の詳細

    Gs2DateTime datetime = await application.Realtime.Gs2DateTimeAsync(
        GameSession
    );
    DateTimeOffset now = datetime.Current;

Gs2DateTimeAsync を呼び出した時に GS2-Realtime::Now を呼び出して現在時刻を取得し Gs2DateTime オブジェクトを返します。
Gs2DateTime の Current にアクセスすることで、補正が適用された現在時刻を取得できます。

QA目的で GS2-Account や GS2-Auth を使用して現在時刻のオフセットを設定している場合、Gs2DateTime オブジェクトが返す時刻にもオフセットが適用されます。

Nifty Cloud mobile backend をご利用の皆さんへ

本日、Nifty Cloud mobile backend(NCMB) のサービス終了がアナウンスされました。
多くのインディーデベロッパーさんを支えてきたサービスで、移行先を検討される方も多くいらっしゃると思います。

GS2 がその移行先として選んでいただくにあたって、必要となるであろう情報をこちらにまとめます。

料金(無料枠)

まず、重要なのは利用料金です。

API

無料で利用可能な範囲についてですが、NCMB は 100万回 ⁄ 月 が無料で利用できます。
GS2 も月2万円の無料枠があり、APIリクエスト 0.02円 が基本となっているため、同じく100万回/月 が無料で利用可能です。

ストレージ

NCMB ではストレージ機能が提供されており、容量制限があります。
しかし、GS2 が提供するストレージ機能である GS2-Datastore には容量制限はありません。
代わりに 「データの保存容量 1GB 10円」の料金が発生し、無料枠の消費で充当が可能です。

データベース

NCMB ではデータベース機能があり、データベースにインデックスを構築して検索速度を向上するのは無料プランでは利用できません。
GS2 では任意のデータベースを作成できない代わりに、ゲームの用途に最適化された30を超えるサービスが提供されており、常に最適化されたインデックスを使用してデータベースにアクセスします。
そのため、サービス利用者は検索速度などを考える必要がなく、同様の料金プランも存在しません。

スクリプト

NCMB ではスクリプト機能があり、無料プランでは「5万回 ⁄ 月」が利用可能です。
GS2 では、通常のAPIリクエストと同様にカウントされます。
追加で「スクリプトの実行時間 1秒 0.005円」の追加料金が発生しますが、こちらも無料枠の消費で充当が可能です。
NCMBにあるような累計時間の制限はありません。

インディー開発者向けプラン

GS2 には NCMB にない料金プランとしてインディー開発者向けの料金プランがあります。
この料金プランは過去12ヶ月の売り上げが 1000万円未満の個人又は法人が利用可能なプランで、一定の条件を満たす限りGS2のあらゆる機能が無料で利用できます。

一定の条件

月の平均リクエスト回数が秒間10回または、一定期間継続して秒間リクエスト回数が100回を超える場合は Individual プランの対象外となります。

という、通常であれば売り上げ1000万円を超える規模の売り上げになっていてもおかしくない規模のアクセスが発生しているにもかかわらず、そのような申告をいただいていないケース向けの制限です。

起動シーケンス もしくは タイトル画面 に GS2 のロゴを掲載する必要があります。 
ゲームのリリースの目処がたったタイミングで、チャットよりロゴデータと表示に伴うガイドラインを受け取ってください。

に従ってゲーム内にGS2のロゴを掲載する必要があります。
詳細は以下を参照してください。

GS2の利用料金 | Game Server Services | Docs

利用方法

GS2 は https://gs2.io でサインアップすることで、すぐにサービスを利用開始できます。

サンプル

github.com

こちらにサンプルコードを用意しました。
NCMB を利用して実現していた一般的な内容が含まれているはずです。

実行例

GS2 では環境のセットアップに管理画面からポチポチ登録するだけでなく、定義ファイルのアップロードでも対応できます。
今回は CloudSave プロジェクトの実行までの流れを説明します。

環境のセットアップ

サンプルに template.yaml というファイルがあります。

GS2TemplateFormatVersion: "2019-05-01"

Resources:
  AccountNamespace:
    Type: GS2::Account::Namespace
    Properties:
      Name: CloudSave-Account

  DatastoreNamespace:
    Type: GS2::Datastore::Namespace
    Properties:
      Name: CloudSave-Datastore

こちらは、GS2-Account と GS2-Datastore のセットアップをする内容が記述されています。

GS2 のマネージメントコンソール(管理画面)にアクセスします。

サイドメニューより「Deploy -> Stacks」を選択します。

「スタックの新規作成」を選択します。

スタックの名前を「CloudSave」として、template.yaml をアップロードします。

スタックの詳細画面で、実行状態が「作成完了」や、イベントリストに「CREATE_COMPLETE」のような表示が行われればセットアップは完了です。

実行

サンプルリポジトリをチェックアウトし、CloudSave.unity を Unity Editor で開きます。

シーン内に存在する GameObject を選択し、インスペクターを確認します。

ClientID / Client Secret にはあらかじめサンプルの動作確認用の値が設定されています。(この値は定期的に変更されるため、動作確認以外の利用は避けてください)
ここを自分のプロジェクトの ClientID / ClientSecret に書き換えましょう。

マネージメントコンソールの Home にプロジェクトで使用できる ClientID / ClientSecret が表示されますので、そちらを設定してください。

実行すると「ABC」という文字列がコンソールに書き出されます。

実装の確認
/*
 * Copyright 2016 Game Server Services, Inc. or its affiliates. All Rights
 * Reserved.
 *
 * Licensed under the Apache License, Version 2.0 (the "License").
 * You may not use this file except in compliance with the License.
 * A copy of the License is located at
 *
 *  http://www.apache.org/licenses/LICENSE-2.0
 *
 * or in the "license" file accompanying this file. This file is distributed
 * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
 * express or implied. See the License for the specific language governing
 * permissions and limitations under the License.
 */

#if GS2_ENABLE_UNITASK
using System.Collections.Generic;
using System.Text;
using Cysharp.Threading.Tasks.Linq;
using Gs2.Core.Model;
using Gs2.Unity.Core;
using Gs2.Unity.Util;
using UnityEngine;
using Cysharp.Threading.Tasks;
using UnityEditor;

namespace Gs2.Sample.Simple
{
    public class CloudSaveAsync : MonoBehaviour
    {
        public string clientId;
        public string clientSecret;

        public string accountNamespaceName;
        public string datastoreNamespaceName;
        
        private async UniTask ProcessAsync() {
            // Initialize GS2 SDK
            var gs2 = await Gs2Client.CreateAsync(
                new BasicGs2Credential(
                    clientId,
                    clientSecret
                ),
                Region.ApNortheast1
            );
            
            // define GS2-Account namespace
            var gs2Account = gs2.Account.Namespace(
                this.accountNamespaceName
            );
            
            // Create an anonymous account
            var account = await (
                await gs2Account.CreateAsync()
            ).ModelAsync();
            
            // login
            var gameSession = await gs2.LoginAsync(
                new Gs2AccountAuthenticator(
                    accountSetting: new AccountSetting {
                        accountNamespaceName = this.accountNamespaceName,
                    }
                ),
                account.UserId,
                account.Password
            );

            // define GS2-Datastore namespace
            var gs2Datastore = gs2.Datastore.Namespace(
                this.datastoreNamespaceName
            );

            // define save data
            var saveData = new byte[] {
                (byte) 'A', (byte) 'B', (byte) 'C'
            };
            
            // upload save data
            await gs2Datastore.Me(
                gameSession
            ).UploadAsync(
                "public",
                new List<string>(),
                saveData,
                "PublicData"
            );

            // download save data
            var downloadedSaveData = await gs2Datastore.Me(
                gameSession
            ).DataObject(
                "PublicData"
            ).DownloadAsync();

            Debug.Log(Encoding.ASCII.GetString(downloadedSaveData));
        }
        
        public void Start() {
            StartCoroutine(ProcessAsync().ToCoroutine());
        }
    }
}
#endif

実は ABC という文字列は GS2-Datastore に保存し、ダウンロードした内容を表示していました。

データの確認

上記プログラムでは GS2-Account でログインし、GS2-Datastore にデータをアップロードしていました。
マネージメントコンソールでデータを確認してみましょう。

まずは GS2-Account の管理メニューを開くためにサイドメニューから「Account -> Namespaces」を選択します。

CloudSave-Account がこのサンプルでアカウントを保存している領域です。

「bc708ed6-a319-4480-bd64-bd5727afec09」というユーザーIDのプレイヤーが作成されていることがわかります。

次に、GS2-Datastore の管理メニューを開きます「Datastore -> Namespaces」を選択し、「CloudSave-Datastore」を開きます。

ユーザーデータタブの Target User ID に「bc708ed6-a319-4480-bd64-bd5727afec09」を指定すると、このプレイヤーが GS2-Datastore にアップロードしたデータを参照できます。

「PublicData」 という名前のファイルがアップロードされていることがわかります。

3バイトのデータがアップロードされていることがわかります。

最後に

まずはぜひ触ってみてください!

(C) Game Server Services, Inc.