C# ステートマシン定義リファレンス

C# で記述する状態、操作、処理の継続、報酬、乱数の仕様

C# のクラスで、ゲームが保持するデータと、プレイヤーの操作に対する処理を定義します。このページは、その書式と API のリファレンスです。

定義の生成には LocalStateMachineKit を使用します。C# の定義から、GS2-StateMachine へ登録する GSL を生成します。投機的実行を使う場合は、Unity から操作を呼ぶための C# クラスも生成します。

生成する GSL の実行先は、生成設定の hostProfile で選びます。このページは、既定値の LocalReplayV1 を前提に説明します。

hostProfile実行方式このページとの違い
LocalReplayV1(既定値)StateMachine の投機的実行。クライアントで操作を実行し、サーバーが操作履歴を再生して検証するなし
RealtimeRoomV1Realtime のルーム内で実行するクラス・record・Start・Step の書き方は同じ。GameRewards と GameRandom の代わりに Room API を使う

RealtimeRoomV1 の入力、Room API、同期項目と操作権、ルーム内での実行規則は ルーム定義リファレンスを参照してください。

定義の構成要素

一つの定義は、次の要素で構成します。

要素意味例
状態次の操作へ引き継ぐゲームデータ所持数、HP、選択済みのルート
操作外部から受け付ける入力と、その処理を記述したメソッドアイテムを使う、ルートを選ぶ
結果ゲーム終了時に残すデータ最終スコア、クリアしたかどうか
Step処理後の状態と、次に何をするかを返す値操作を待つ、処理を続ける、終了する

ここでいう「状態」は保存するデータです。「現在どの操作を受け付けるか」は、別に Step で指定します。

Step<状態の型, 結果の型, 入力を拒否する理由の型> の三つの型を定義全体で揃えます。以下の例は using Step = ... でこの長い型名を Step と略記しています。

定義の全体例

数値 Count を保持する Counter の定義です。Add で指定した数を加え、Finish で終了します。Add と Finish は、この例で定義した操作名です。

using Gs2.LocalStateMachine.Authoring;
using Step = Gs2.LocalStateMachine.Authoring.Step<
    GameRules.CounterState, GameRules.CounterResult, GameRules.CounterError>;

namespace GameRules;

public record CounterState(int Count);
public record CounterResult(int Count);
public enum CounterError { InvalidAmount = 1001 }

public static class Counter
{
    public static Step Start() =>
        Step.WaitFor(nameof(Add), new CounterState(0)).Or(nameof(Finish));

    public static Step Add(CounterState state, int amount)
    {
        if (amount < 1 || amount > 10)
            return Step.Ignore(CounterError.InvalidAmount);

        var next = state with { Count = state.Count + amount };
        return Step.Stay(next);
    }

    public static Step Finish(CounterState state) =>
        Step.End(new CounterResult(state.Count), state);
}

Start は開始時に一度実行します。この例では Count を 0 にし、Add と Finish を受け付けます。Add の処理後も同じ二つの操作を受け付け、Finish の処理後は終了します。

宣言規則

ルールクラス

public static class Counter
{
    // Start、操作、自動処理、補助関数を宣言する
}

クラス名がステートマシン名になります。一つのソースフォルダーにルールクラスを一つ置きます。状態・結果の record と enum は、同じフォルダー内でファイルを分けても構いません。

クラス内に宣言できるのは static メソッドです。フィールド、プロパティ、コンストラクター、型パラメーターは使用できません。

メソッド

以下の Step は、同じ状態・結果・理由の型を指定した Step<TState, TResult, TCode> を指します。

種類宣言の形呼び出されるタイミング
開始処理public static Step Start()ゲームの開始時。必須
操作public static Step Add(TState state, int amount)外部から Add の入力を受け取ったとき
自動処理private static Step Resolve(TState state)Step.Next で指定されたとき
補助関数private static int Calculate(int value)定義内のメソッドから通常の C# 関数として呼んだとき

操作と自動処理の違いは、外部からの入力を待つかどうかです。自動処理は入力を待たずに実行します。Step を返すメソッドへ進む場合は WaitFor または Next を使い、Add(...) のように直接呼び出しません。

メソッドは同期的な static メソッドにします。次の宣言は使用できません。

  • 同名メソッドのオーバーロード
  • async、ジェネリックメソッド
  • ref / out / in 引数、params、省略可能な引数

型・メソッド・引数などの名前には ASCII の英字または _ で始まる英数字と _ を使用します。生成コードの予約名や名前の衝突は生成時に検査します。

引数を渡す側

Start は引数を取りません。それ以外の Step を返すメソッドでは、第 1 引数を状態型にします。

public static Step Add(CounterState state, int amount, GameRewards rewards)

この宣言では、呼び出し元が渡すのは amount だけです。現在の state と報酬用の rewards は実行環境が渡します。生成される Unity 用クラスでは game.Add(3) と呼びます。

引数値を渡す側使用できる場所
第 1 引数の状態実行環境操作・自動処理で必須
GameRewards実行環境操作・自動処理。それぞれ最大一つ
GameRandom<TStream>実行環境操作・自動処理。それぞれ最大一つ
上記以外のデータ操作の呼び出し元public な操作のみ

自動処理へ新しい外部入力を渡すことはできません。操作から自動処理へ値を引き継ぐ場合は状態に含めます。入力名 revision は予約されているため使えません。

初期化で乱数や報酬を使う場合は、Start で初期状態を作り、Next から必要な引数を持つ自動処理へ進みます。

データ型

状態・結果・操作入力

型扱い
int符号付き 32 ビット整数
long符号付き 64 ビット整数。int32 や浮動小数点へ狭めずに扱う
float / doubleそれぞれの浮動小数点精度で扱う。NaN・Infinity は保存・送信できない
bool真偽値
stringnull でない文字列
enumソース内で宣言した、基底型が int の列挙型
record上記の型をプロパティに持つデータ型。下記の宣言形式を使用する

状態と結果は、次のように丸括弧の引数でプロパティを宣言する record にします。この宣言形式を positional record と呼びます。操作入力には、表の基本型・enum・record を指定できます。

public record CounterState(int Count);
public record CounterResult(int Count);
public record MoveInput(int X, int Y);

更新後の状態は new または with で作り、Step に渡します。

var next = state with { Count = state.Count + amount };
return Step.Stay(next);

record のプロパティに、別の record、配列、コレクション、null を含めることはできません。また、継承、型パラメーター、追加メンバー、プロパティの既定値、record struct は対象外です。

enum の種類

用途宣言例数値の規則
状態・入力などの通常の enumenum Choice { None, Small, Large }数値を明示指定しない
TCode:Ignore の理由enum Error { InvalidAmount = 1001 }正の int32 値を各メンバーに明示。重複不可
TStream:乱数の用途enum RandomSource { Drop = 1 }0 以上の int32 値を各メンバーに明示。重複不可

エラーコードと乱数ストリーム ID は、運用開始後も意味を変えない値として管理します。一つのルールクラスで使用する乱数用 enum は一種類です。

報酬リクエスト内の SDK データ

報酬の組み立てに使用する SDK オブジェクトは、上記の状態・操作入力とは別の扱いです。SDK が宣言する nullable、配列、入れ子 Model を使用できます。SDK オブジェクトを TState や操作入力へ保存することはできません。詳細は GameRewards を参照してください。

対応する C# 構文

定義の中で使用できる C# の構文は次のとおりです。

分類対応範囲
宣言・変数対応型のローカル変数、代入、定数
データの作成positional record の new と with、SDK リクエスト・Model・配列の作成
分岐if、条件式、定数・when・最後の _ を使う switch 式
繰り返しfor、while、break、continue
関数同じクラス内の private 補助関数、名前付き引数、return
論理演算bool の否定・比較、&& / `
数値演算checked int32 / int64 の加減乗除・剰余、float / double の演算、対応する数値変換、ローカル変数への複合代入
数値関数int32 の Math.Min / Math.Max
SDK の nullable 値HasValue、Value、GetValueOrDefault、??

一般的なコレクション、foreach、入れ子 record、async、任意の外部 API 呼び出しは対象外です。整数演算はオーバーフローを検査する設定で生成します。ループと関数呼び出しには実行量の上限があります。

一回の操作の処理範囲

外部から一つの操作を受け取ると、そのメソッドを実行します。Next が返った場合は、指定した自動処理も続けて実行します。この一連の処理を、一回の入力として扱います。

  • WaitFor または Stay に到達すると、更新後の状態で次の入力を待ちます。
  • End に到達すると、最終状態と結果を保持して終了します。
  • 途中で Ignore に到達すると、今回の状態変更・乱数の進行・報酬要求を取り消し、入力前と同じ状態で待ちます。操作履歴にも加えません。

ここでの処理完了はローカル実行の結果です。サーバーの検証は、別途 Session の Confirm で操作履歴を送って受けます。

Step API

Step は次の動作を指定する戻り値です。以下の TState は状態型、TResult は結果型、TCode は入力を拒否する理由の enum を指します。

API次の動作保持する状態
WaitFor指定した操作を待つ引数の状態
Stay入力前と同じ操作の組み合わせを待つ引数の状態
Next指定した自動処理を直ちに実行する引数の状態を後続処理へ渡す
End終了する引数の状態。省略した場合は現在の状態
Ignore今回の入力を取り消し、再び待つ入力前の状態

WaitFor、Or、Next の処理先は、同じルールクラスのメソッドを指す nameof(...) で指定します。文字列リテラルや変数、Start は指定できません。

WaitFor / Or

次に受け付ける操作を指定します。

static Step WaitFor(string target, TState state);
Step Or(string target);

state を次の状態とし、target で指定した public な操作の入力を待ちます。複数の操作を受け付ける場合は、WaitFor(...).Or(...).Or(...) と連結します。

return Step.WaitFor(nameof(Add), nextState).Or(nameof(Finish));

Or は WaitFor の呼び出しに連結します。同じ操作を重複指定できません。

Stay

状態を更新し、同じ操作を引き続き受け付けます。

static Step Stay(TState state);

state を次の状態とし、今回の入力を受け付ける直前と同じ操作の組み合わせで待機します。実行中の関数をもう一度呼ぶ意味ではありません。

return Step.Stay(nextState);

たとえば Add と Finish を受け付けていた場合、Add から Stay を返すと両方を引き続き受け付けます。最初の入力待ちに到達する前の Start とその自動処理では使用できません。

Next

入力を待たずに、次の処理を実行します。

static Step Next(string target, TState state);

state を渡して、target で指定した private な自動処理を直ちに実行します。

return Step.Next(nameof(Resolve), nextState);

呼び出し先の宣言例は private static Step Resolve(CounterState state) です。必要なら乱数・報酬の引数も指定できます。public な操作、Start、新しいプレイヤー入力を必要とするメソッドは対象にできません。

処理の採用・取消は一回の操作の処理範囲に従います。Next の循環は生成時に拒否します。入力を繰り返し待つ場合は WaitFor または Stay を使用します。

End

結果を残してゲームを終了します。

static Step End(TResult result);
static Step End(TResult result, TState state);

結果を保持してステートマシンを終了します。終了後は新しい操作を受け付けません。

引数動作
result のみその時点の状態を保持し、結果を設定する
result, state指定した状態を最終状態とし、結果を設定する
return Step.End(new CounterResult(nextState.Count), nextState);

ローカル変数 nextState を作っただけでは、保持される状態は更新されません。更新後の状態を残す場合は第 2 引数へ渡します。Start で即時終了する場合も、初期状態を第 2 引数に指定します。

終了と報酬の要求は独立しています。報酬を要求するために End を返す必要はありません。

Ignore

ゲームのルール上受け付けられない入力を、理由付きで取り消します。

static Step Ignore(TCode code);

今回の入力を採用せず、理由を返します。code にはエラー enum のメンバーを直接指定します。変数からの指定はできません。

return Step.Ignore(CounterError.InvalidAmount);

取り消す範囲は一回の操作の処理範囲に従います。Start と、最初の入力待ちより前の自動処理では使用できません。

GameRewards:報酬の要求

ゲーム内で決まった報酬を、GS2 SDK の付与リクエストとして指定します。操作または自動処理の引数に GameRewards を追加します。ゲームの途中でも使用できます。

Grant(request)

void Grant<TRequest>(TRequest request) where TRequest : class;

request に渡せるのは、SDK が AcquireAction に登録している付与リクエストです。参照 API や消費 API のリクエストは生成時に拒否します。

付与する操作は、各サービスのトランザクションアクションの Acquire Action から探せます。たとえば Inventory の入手アクションで付与内容を確認し、対応する SDK の Request 型を使用します。使用中の SDK がその型に対応しているかは生成時に検査します。

次の例では Add で Count を増やすたびに、同じ数の parts を要求します。using は定義ファイルの先頭、Add はルールクラス内に記述します。

using Gs2.Gs2Inventory.Request;
using Gs2.Gs2Inventory.Model;

// Counter クラス内の操作
public static Step Add(CounterState state, int amount, GameRewards rewards)
{
    if (amount < 1 || amount > 10)
        return Step.Ignore(CounterError.InvalidAmount);

    var next = state with { Count = state.Count + amount };
    rewards.Grant(new AcquireSimpleItemsByUserIdRequest()
        .WithNamespaceName("rewards")
        .WithInventoryName("bag")
        .WithAcquireCounts(new[] {
            new AcquireCount().WithItemName("parts").WithCount(amount)
        }));
    return Step.Stay(next);
}

rewards、bag、parts は付与先のリソース名です。実サービスで使用する場合は、登録済みの名前を指定します。呼び出し元は引き続き game.Add(3) と呼び、GameRewards は実行環境が渡します。

指定できる値

SDK のリクエスト型と Model 型をそのまま使用します。

SDK の値扱い
int? / long?それぞれの整数範囲を保持する
float? / double?それぞれの精度を保持する。NaN・Infinity は送信不可
bool・文字列・nullSDK の宣言に従って指定する
配列・入れ子 ModelSDK の型で組み立てる

new、オブジェクト初期化子、With、プロパティの読み書き、配列の生成・添字アクセスを使用できます。変数や private 補助関数を使って組み立てることもできます。

Grant は呼び出した時点の内容を保存します。その後に元のオブジェクトを変更しても、要求済みの報酬は変わりません。受取人、TimeOffsetToken、DuplicationAvoider は実行環境が設定するため、定義では指定しません。0・負数・null の可否などは、付与先 API の仕様に従います。

SDK 型を使う場合の参照設定はSDK DLL の参照を参照してください。bindings.json は不要です。

要求と払い出しの違い

Grant は報酬を要求する処理です。ローカルの在庫をその場で増やす処理ではありません。Ignore になった入力の要求は取り消します。

実際の払い出しは、操作履歴をサーバーへ送り、検証を受けた後に行われます。ローカルでの入力受付、サーバー検証、報酬反映は別の段階です。 確認方法は Session の Confirmを参照してください。

Grant(id, amount):enum による指定

void Grant<T>(T id, int amount) where T : Enum;

Inventory の SimpleItem に限り、報酬 enum と個数でも指定できます。たとえば enum Reward { Parts } を定義し、rewards.Grant(Reward.Parts, amount) と呼びます。付与先は別途 bindings.json に設定します。

この形式の amount は int32 です。0 なら要求を発行せず、負数は実行エラーです。SDK リクエストを渡す形式の数値範囲・制約とは異なります。

GameRandom:乱数

サーバーが同じ結果を再現できる乱数を生成します。操作または自動処理の引数に GameRandom<TStream> を追加します。TStream は乱数の用途を表す enum です。

RandomStream Stream(TStream stream);
int RangeInclusive(int min, int max);

Stream で用途を選び、RangeInclusive で両端を含む範囲の int32 を得ます。たとえば RangeInclusive(2, 4) の戻り値は 2・3・4 です。min > max は実行エラーです。

次の例では、Add 一回につき Count を 2〜4 増やします。enum はルールクラスの外、Add はクラス内に記述します。

public enum RandomSource { Count = 1 }

// Counter クラス内の操作
public static Step Add(CounterState state, GameRandom<RandomSource> random)
{
    var amount = random.Stream(RandomSource.Count).RangeInclusive(2, 4);
    var next = state with { Count = state.Count + amount };
    return Step.Stay(next);
}

この場合、呼び出し元は game.Add() と呼びます。乱数の引数は渡しません。用途ごとに enum メンバーを分け、ID は運用開始後も意味を変えない値にします。数値の規則は enum の種類を参照してください。

System.Random や UnityEngine.Random は使用できません。生成・再生には実行環境のシードと使用状況を使います。Ignore の場合は共通の取消規則に従います。

検証と失敗時の動作

場面動作
未対応の構文・型、遷移先の不整合生成時に診断を表示する。ソースの診断にはファイル名・行・列が付く
ゲーム固有の受付条件に違反操作内で検査し、Step.Ignore で理由を返す
入力型・数値範囲・enum 候補・record 必須フィールド・リビジョンの不一致生成した実行コードでも検査する。内部の Retry として入力を採用せず、ゲーム固有の Ignore 理由と区別する
待機中に受け付けていない操作を Session から呼ぶ例外
ゼロ除算などの実行エラーエラーとして扱う。Ignore と異なり、入力前へ戻ってそのまま続行することは保証しない
生成の失敗前回成功した生成物を残す

Ignore と実行エラーは復帰方法が異なります。通常の入力拒否は例外で表現せず Ignore を使用してください。