C# State Machine Definition Reference
You define the data a game holds and how it handles player operations in a C# class. This page is the reference for that format and its APIs.
You use LocalStateMachineKit to generate definitions. It generates the GSL to register with GS2-StateMachine from the C# definition. When you use speculative execution, it also generates C# classes for calling operations from Unity.
- Steps to actually build and run one: Tutorial
- How to use the generated C# classes from your game: StateMachine Speculative Execution Session API
- Specifying output locations and SDK references: Generation Settings and Registration
You choose where the generated GSL runs with hostProfile in the generation settings. This page assumes the default, LocalReplayV1.
hostProfile | Execution model | Differences from this page |
|---|---|---|
LocalReplayV1 (default) | StateMachine speculative execution. The client runs operations, and the server replays the operation history to verify it | None |
RealtimeRoomV1 | Runs inside a Realtime room | Classes, records, Start, and Step are written the same way. Uses the Room API instead of GameRewards and GameRandom |
For RealtimeRoomV1 inputs, the Room API, sync fields and control, and execution rules inside a room, see the Room Definition Reference.
Definition Components
A definition consists of the following elements.
| Element | Meaning | Examples |
|---|---|---|
| State | Game data carried over to the next operation | Item counts, HP, the selected route |
| Operation | A method describing an input accepted from outside and how it is handled | Use an item, choose a route |
| Result | Data kept when the game finishes | Final score, whether it was cleared |
| Step | A value returning the state after processing and what to do next | Wait for an operation, continue processing, finish |
“State” here means the data that is stored. “Which operations are accepted now” is specified separately with Step.
Keep the three types of Step<state type, result type, type of reasons for rejecting input> consistent throughout the definition. The examples below abbreviate this long type name as Step with using Step = ....
Complete Definition Example
This is the definition of Counter, which holds a number Count. Add adds the specified number, and Finish finishes. Add and Finish are operation names defined in this example.
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 runs once at the beginning. In this example, it sets Count to 0 and accepts Add and Finish. After Add is processed, the same two operations are still accepted; after Finish is processed, the machine finishes.
Declaration Rules
Rules Class
public static class Counter
{
// Declare Start, operations, automatic steps, and helper functions
}The class name becomes the state machine name. Put one rules class in one source folder. The state and result records and enums may be split into separate files within the same folder.
Only static methods can be declared in the class. Fields, properties, constructors, and type parameters cannot be used.
Methods
Step below refers to Step<TState, TResult, TCode> with the same state, result, and reason types.
| Kind | Declaration form | When it is called |
|---|---|---|
| Start | public static Step Start() | When the game starts. Required |
| Operation | public static Step Add(TState state, int amount) | When an Add input is received from outside |
| Automatic step | private static Step Resolve(TState state) | When specified by Step.Next |
| Helper function | private static int Calculate(int value) | When called as an ordinary C# function from a method in the definition |
The difference between operations and automatic steps is whether they wait for external input. Automatic steps run without waiting for input. To proceed to a method that returns Step, use WaitFor or Next; don’t call it directly like Add(...).
Methods must be synchronous static methods. The following declarations cannot be used.
- Overloads of methods with the same name
- async or generic methods
- ref / out / in parameters, params, optional parameters
For names of types, methods, parameters, and so on, use ASCII letters, digits, and _, starting with a letter or _. Reserved names in the generated code and name collisions are checked at generation time.
Who Passes Arguments
Start takes no arguments. In other methods that return Step, the first parameter is the state type.
public static Step Add(CounterState state, int amount, GameRewards rewards)With this declaration, the caller passes only amount. The runtime passes the current state and rewards for rewards. In the generated Unity class, you call game.Add(3).
| Parameter | Who passes the value | Where it can be used |
|---|---|---|
| State as the first parameter | Runtime | Required in operations and automatic steps |
GameRewards | Runtime | Operations and automatic steps. At most one each |
GameRandom<TStream> | Runtime | Operations and automatic steps. At most one each |
| Any other data | The caller of the operation | Public operations only |
You cannot pass new external input to an automatic step. To carry values from an operation to an automatic step, include them in the state. The input name revision is reserved and cannot be used.
To use random numbers or rewards during initialization, create the initial state in Start and proceed with Next to an automatic step that has the required parameters.
Data Types
State, Result, and Operation Input
| Type | Handling |
|---|---|
int | Signed 32-bit integer |
long | Signed 64-bit integer. Handled without narrowing to int32 or floating point |
float / double | Handled at their respective floating-point precision. NaN and Infinity cannot be stored or sent |
bool | Boolean |
string | A non-null string |
| enum | An enum declared in the source with an underlying type of int |
| record | A data type with properties of the types above. Use the declaration form below |
Make states and results records that declare properties as parenthesized parameters, as follows. This declaration form is called a positional record. For operation inputs, you can specify the basic types in the table, enums, and records.
public record CounterState(int Count);
public record CounterResult(int Count);
public record MoveInput(int X, int Y);Create the updated state with new or with and pass it to Step.
var next = state with { Count = state.Count + amount };
return Step.Stay(next);Record properties cannot contain other records, arrays, collections, or null. Inheritance, type parameters, additional members, property default values, and record struct are also not supported.
Kinds of Enums
| Use | Declaration example | Numeric rules |
|---|---|---|
| Ordinary enums for state, input, etc. | enum Choice { None, Small, Large } | Don’t specify numeric values explicitly |
TCode: Ignore reasons | enum Error { InvalidAmount = 1001 } | Explicitly assign a positive int32 value to each member. No duplicates |
TStream: random number uses | enum RandomSource { Drop = 1 } | Explicitly assign an int32 value of 0 or more to each member. No duplicates |
Manage error codes and random stream IDs as values whose meaning does not change after operation begins. A single rules class uses only one enum type for random numbers.
SDK Data in Reward Requests
SDK objects used to build rewards are handled separately from the states and operation inputs above. You can use the nullable types, arrays, and nested Models that the SDK declares. You cannot store SDK objects in TState or operation inputs. For details, see GameRewards.
Supported C# Syntax
The C# syntax you can use in a definition is as follows.
| Category | Supported |
|---|---|
| Declarations and variables | Local variables of supported types, assignment, constants |
| Creating data | new and with for positional records; creating SDK requests, Models, and arrays |
| Branching | if, conditional expressions, switch expressions with constants, when, and a final _ |
| Loops | for, while, break, continue |
| Functions | Private helper functions in the same class, named arguments, return |
| Logical operations | bool negation and comparison, short-circuit evaluation of && / ` |
| Numeric operations | checked int32 / int64 addition, subtraction, multiplication, division, and remainder; float / double arithmetic; supported numeric conversions; compound assignment to local variables |
| Numeric functions | int32 Math.Min / Math.Max |
| SDK nullable values | HasValue, Value, GetValueOrDefault, ?? |
General collections, foreach, nested records, async, and calls to arbitrary external APIs are not supported. Integer arithmetic is generated with overflow checking. Loops and function calls have execution limits.
Processing Scope of a Single Operation
When an operation is received from outside, its method runs. If it returns Next, the specified automatic step also runs right after. This whole sequence is treated as a single input.
- When it reaches
WaitFororStay, it waits for the next input with the updated state. - When it reaches
End, it keeps the final state and result and finishes. - If it reaches
Ignorealong the way, this input’s state changes, random number advances, and reward requests are rolled back, and it waits in the same state as before the input. The input is not added to the operation history either.
Processing completion here is the result of local execution. Server verification happens separately when you send the operation history with Session Confirm.
Step API
Step is a return value that specifies what happens next. Below, TState is the state type, TResult is the result type, and TCode is the enum of reasons for rejecting input.
| API | What happens next | State kept |
|---|---|---|
WaitFor | Waits for the specified operations | The state passed as the argument |
Stay | Waits for the same combination of operations as before the input | The state passed as the argument |
Next | Immediately runs the specified automatic step | Passes the argument’s state to the subsequent step |
End | Finishes | The argument’s state. If omitted, the current state |
Ignore | Rolls back this input and waits again | The state before the input |
Specify the targets of WaitFor, Or, and Next with nameof(...) pointing to a method in the same rules class. String literals, variables, and Start cannot be specified.
WaitFor / Or
Specifies the operations accepted next.
static Step WaitFor(string target, TState state);
Step Or(string target);Makes state the next state and waits for input of the public operation specified by target. To accept multiple operations, chain them as WaitFor(...).Or(...).Or(...).
return Step.WaitFor(nameof(Add), nextState).Or(nameof(Finish));Or is chained to a WaitFor call. The same operation cannot be specified more than once.
Stay
Updates the state and keeps accepting the same operations.
static Step Stay(TState state);Makes state the next state and waits with the same combination of operations as just before this input was accepted. It does not mean calling the running function again.
return Step.Stay(nextState);For example, if Add and Finish were being accepted, returning Stay from Add keeps accepting both. It cannot be used in Start or its automatic steps before the first wait for input is reached.
Next
Runs the next step without waiting for input.
static Step Next(string target, TState state);Passes state and immediately runs the private automatic step specified by target.
return Step.Next(nameof(Resolve), nextState);An example declaration of the target is private static Step Resolve(CounterState state). You can also specify random number and reward parameters if needed. Public operations, Start, and methods that require new player input cannot be targets.
Whether processing is adopted or rolled back follows Processing Scope of a Single Operation. Cycles of Next are rejected at generation time. To wait for input repeatedly, use WaitFor or Stay.
End
Finishes the game and keeps a result.
static Step End(TResult result);
static Step End(TResult result, TState state);Keeps the result and finishes the state machine. After finishing, no new operations are accepted.
| Arguments | Behavior |
|---|---|
result only | Keeps the state at that point and sets the result |
result, state | Makes the specified state the final state and sets the result |
return Step.End(new CounterResult(nextState.Count), nextState);Merely creating a local variable nextState does not update the kept state. To keep the updated state, pass it as the second argument. When finishing immediately in Start, also specify the initial state as the second argument.
Finishing and requesting rewards are independent. You don’t need to return End to request rewards.
Ignore
Rolls back an input that the game rules cannot accept, with a reason.
static Step Ignore(TCode code);Does not adopt this input and returns a reason. Specify a member of the error enum directly as code. It cannot be specified from a variable.
return Step.Ignore(CounterError.InvalidAmount);The scope of the rollback follows Processing Scope of a Single Operation. It cannot be used in Start or in automatic steps before the first wait for input.
GameRewards: Requesting Rewards
Specify rewards determined in the game as GS2 SDK grant requests. Add GameRewards to the parameters of an operation or automatic step. You can use it in the middle of the game as well.
Grant(request)
void Grant<TRequest>(TRequest request) where TRequest : class;You can pass to request only the grant requests that the SDK registers in AcquireAction. Requests for read APIs or consume APIs are rejected at generation time.
You can find grant operations in each service’s transaction actions under Acquire Action. For example, check what to grant in Inventory’s acquire actions and use the corresponding SDK Request type. Whether the SDK you use supports that type is checked at generation time.
In the following example, each time Add increases Count, the same number of parts is requested. Write the usings at the top of the definition file and Add inside the rules class.
using Gs2.Gs2Inventory.Request;
using Gs2.Gs2Inventory.Model;
// Operation inside the Counter class
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, and parts are the names of the resources to grant to. When using a real service, specify registered names. The caller still calls game.Add(3), and the runtime passes GameRewards.
Values You Can Specify
Use the SDK’s Request and Model types as they are.
| SDK value | Handling |
|---|---|
int? / long? | Keeps the respective integer range |
float? / double? | Keeps the respective precision. NaN and Infinity cannot be sent |
| bool, strings, null | Specify according to the SDK’s declarations |
| Arrays and nested Models | Build them with the SDK types |
You can use new, object initializers, With, property reads and writes, and array creation and indexing. You can also build them with variables and private helper functions.
Grant saves the content at the time it is called. Changing the original object afterwards does not change the already requested reward. The recipient, TimeOffsetToken, and DuplicationAvoider are set by the runtime, so don’t specify them in the definition. Whether 0, negative numbers, or null are allowed follows the specification of the grant API.
For reference settings when using SDK types, see SDK DLL References. bindings.json is not required.
Requesting vs. Paying Out
Grant requests a reward. It does not increase local inventory on the spot. Requests from inputs that end in Ignore are rolled back.
The actual payout happens after the operation history is sent to the server and verified. Accepting input locally, server verification, and applying rewards are separate stages. For how to confirm, see Session Confirm.
Grant(id, amount): Specifying with an Enum
void Grant<T>(T id, int amount) where T : Enum;For Inventory SimpleItems only, you can also specify a reward enum and a count. For example, define enum Reward { Parts } and call rewards.Grant(Reward.Parts, amount). Configure where to grant separately in bindings.json.
In this form, amount is int32. If it is 0, no request is issued; a negative value is a runtime error. The numeric ranges and constraints differ from the form that passes an SDK request.
GameRandom: Random Numbers
Generates random numbers whose results the server can reproduce. Add GameRandom<TStream> to the parameters of an operation or automatic step. TStream is an enum representing the use of the random numbers.
RandomStream Stream(TStream stream);
int RangeInclusive(int min, int max);Choose the use with Stream, and get an int32 in a range that includes both ends with RangeInclusive. For example, RangeInclusive(2, 4) returns 2, 3, or 4. min > max is a runtime error.
In the following example, each Add increases Count by 2 to 4. Write the enum outside the rules class and Add inside the class.
public enum RandomSource { Count = 1 }
// Operation inside the Counter class
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);
}In this case, the caller calls game.Add(). Random number arguments are not passed. Use a separate enum member for each use, and keep IDs whose meaning does not change after operation begins. For the numeric rules, see Kinds of Enums.
System.Random and UnityEngine.Random cannot be used. Generation and replay use the runtime’s seed and usage state. In the case of Ignore, the common rollback rules apply.
Validation and Failure Behavior
| Situation | Behavior |
|---|---|
| Unsupported syntax or types, inconsistent transition targets | Diagnostics are shown at generation time. Source diagnostics include the file name, line, and column |
| Violation of game-specific acceptance conditions | Check inside the operation and return a reason with Step.Ignore |
| Mismatches in input types, numeric ranges, enum candidates, required record fields, or revision | Also checked in the generated runtime code. The input is not adopted, as an internal Retry, which is distinguished from game-specific Ignore reasons |
| Calling an operation from the Session that is not accepted while waiting | Exception |
| Runtime errors such as division by zero | Treated as errors. Unlike Ignore, returning to the pre-input state and continuing is not guaranteed |
| Generation failure | The previously successful output is kept |
Ignore and runtime errors are recovered differently. Don’t represent ordinary input rejection with exceptions; use Ignore.