GS2 Blog

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

ステートマシン管理機能が追加されました

はじめに

今回のアップデートは GS2 のローンチ以来最もインパクトのあるアップデートになります。

これまで、GS2はさまざまなマイクロサービスを用意し、利用者の皆様に提供してきました。
しかし、これはGS2があらかじめ想定した利用用途の範囲で使う、ある意味限定された機能でした。

今回追加されたステートマシン管理機能は、最も柔軟で汎用性のある機能となります。

機能追加の背景

近年スマホゲームのインゲームの複雑化が進んでおり、GS2-Quest が提供しているように《開始したクエスト によって報酬の最大値が確定》し、《終了時に最大値までの範囲で実際に得られた報酬を報告する》という仕組みでは補きれない仕様が増えてきています。

そこで、GS2-Quest で開始されたクエストの中身をより細かく進捗管理する必要性がでてきました。

代表的なゲームの例は《ウマ娘》でしょう。
エストを開始すると、プレイ時間にして1時間にわたる育成フェーズが開始され、育成フェーズを終えたらその結果がキャラクターとして入手されます。

この《1時間にわたる育成フェーズ》で、今何ターン目を遊んでいるか、現在のステータスはどうなっているか といった情報を管理する仕組みが ステートマシン管理機能です。

追加された機能の詳細

《インゲームの状態管理をする》というのは簡単ですが、インゲームの仕様はアウトゲームと比べるとゲームによる仕様の差が大きいです。
そこで、インゲームの開始時に《ステートマシンを起動》し、ゲーム内での選択に応じて《ステートマシンにイベントを送信》し、ステートマシンは受け取ったイベントに応じて《ステートを遷移したり、状態変数を書き換える GS2-Script を実行》し、《ステートマシンが終了ステートに遷移したのをもってクエストの終了とする》という実装に至りました。

どのような《ステート》があるのか、どのような《状態変数》を持つのか、《状態変数》をもって《ゲーム内でどのような振る舞いをする》のかはゲーム開発者の実装に委ねられます。

あくまでGS2が提供するのは《ステートマシンの定義ファイル》のアップロードを受け付け、プレイヤーごとに起動された《ステートマシン》の状態を管理し、《ステートマシンに対するイベントを受け付ける》機能のみを持ちます。

インゲームの処理をデバイス内の計算資源やメモリ資源を使用して処理をすると、どうしてもチート行為からゲームを保護することが難しいですが、ステートマシンをサーバーで管理することで、改ざんが困難になりまし、よりサーバー側で詳細な分析データを集めることが可能となります。

追加された機能の詳細

ステートマシン管理機能を実装するにあたって《ステートマシンをどのように定義するか》は非常に大きな課題でした。
実は、世の中にはプログラミング言語や、RPC定義のための言語は多数ありますが、ステートマシン定義に最適化された言語はほとんど存在しません。

代表的なものとしては Amazon が Step Functions 向けに提供している ASL があります。

docs.aws.amazon.com

しかし、この言語は JSON ベースで定義する内容となっており、記述性が非常に悪いことから GS2 での採用を検討するにあたって、あまり前向きに考えることができませんでした。

ここにブレークスルーをもたらしたのが Chat-GPT でした。
詳細については、GS2のリードエンジニアリングを務めている丹羽のブログ記事をご確認ください。

kazutomo.hatenablog.com

StateMachine MainStateMachine {
  Variables {
    int turn;
    int choiceSkill;
    array skills;
  }

  EntryPoint Initialize;

  Task Initialize() {
    Event Pass();
    Event Error(string reason);
    Script grn:gs2:{region}:{ownerId}:script:statemachine-script:script:MainStateMachine_Initialize
  }

  SubStateMachineTask ChoiceSkill {
    using ChoiceSkill;
    in (turn <- turn);
    out (choiceSkill -> choiceSkill);
  }

  WaitTask InGame {
    Event Pass();
    Event Fail();
    Event Error(string reason);
  }

  Task NextTurn() {
    Event Next();
    Event Exit();
    Event Error(string reason);
    Script grn:gs2:{region}:{ownerId}:script:statemachine-script:script:MainStateMachine_NextTurn
  }

  PassTask Pass;

  ErrorTask Error(string reason);

  Transition Initialize handling Pass -> ChoiceSkill;
  Transition Initialize handling Error -> Error;
  Transition ChoiceSkill handling Pass -> InGame;
  Transition InGame handling Pass -> NextTurn;
  Transition InGame handling Fail -> Pass;
  Transition InGame handling Error -> Error;
  Transition NextTurn handling Next -> ChoiceSkill;
  Transition NextTurn handling Exit -> Pass;
  Transition NextTurn handling Error -> Error;
}

そして、作られたのがこちらのようなステートマシン定義言語 GS2 States Language(GSL) です。

ステートに入った時に実行する GS2-Script を指定し、GS2-Script 内で状態変数を書き換えたり、戻り値によって発行されるイベントを使ってステートを遷移させたり、あるいはプレイヤーからのイベント送信を待ち受けるステートを定義します。

ステートの種類

Task

最も一般的なステートです。
このステートに到達すると Script で指定された GS2-Script が実行されます。
GS2-Script の戻り値に発行するイベント返すようにしておくことで、スクリプトの実行後にイベントの種類に応じて Transition で定義した次のステートに遷移します。

WaitTask

クライアントからのイベントを待ち受けます。
プレイヤーに複数の選択肢を提示したり、インゲームの攻略を待ち受ける場合に使用します。
プレイヤーがとった選択や、インゲームをクリアしたのか失敗したのかをイベントとして送信します。
ステートマシンは受け取ったイベントの種類に応じて Transition で定義した次のステートに遷移します。

SubStateMachineTask

1つのステートマシン定義ファイル内で複数のステートマシンを定義できます。
このステートはサブステートマシンを in で指定したパラメーターを引数として起動します。
サブステートマシンが終了すると、サブステートマシンの状態変数を out で指定した内容に応じて起動元のステートマシンに反映できます。

PassTask

ステートマシンが正常終了した際に遷移するステートです。
サブステートマシンの場合は起動元のステートマシンに処理を返し、メインステートマシンの場合はステートマシン自体を終了します。

ErrorTask

ステートマシンが異常終了した際に遷移するステートです。
GS2-Script の実行に致命的なエラーが発生した場合や、意図しない状態に陥った時に Error イベントを発行し、このステートに遷移させます。

Unity からの使用例

ステートマシンを開始

ステートマシンを開始はゲームエンジン用の SDK では処理できません。
GS2-Quest などのマイクロサービスの報酬として設定してください。

ステートマシンにイベントを送信

    var result = await gs2.StateMachine.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        statusName: status1.Name
    ).EmitAsync(
        eventName: "event-0001",
        args: "{\"value1\": \"value1\", \"value2\": 2.0, \"value3\": 3}"
    );
    var item = await result.ModelAsync();

ステートマシンの状態を取得

    var item = await gs2.StateMachine.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        statusName: status1.Name
    ).ModelAsync();

終了したステートマシンを削除

    var result = await gs2.StateMachine.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        statusName: status1.Name
    ).ExitAsync(
    );
    var item = await result.ModelAsync();
(C) Game Server Services, Inc.