C# State Machine Definition Tutorial
On this page, you build a sample MyGame that increases a number with button presses and finishes when it reaches 9. There are three rules.
- The number
Progressstarts at0. - Choosing
Smallincreases it by1, and choosingLargeincreases it by3. - It finishes when
Progressreaches9. Even if an operation would exceed9, the value is capped at9.
In steps 1 and 2, you run the template in the Editor preview; in step 3, you change the rules; and in step 4, you operate it from a Unity scene. In step 5, you add verification by the server.
1. Create the Template
You work in a Unity project with the GS2 SDK for Unity, GS2-CSharp-SDK, and a version of LocalStateMachineKit that includes the C# generation feature. The generation feature supports Unity 2021.2 or later.
- From the Unity menu, select GS2 > Local State Machine > Generate from C#. The GS2 C# Authoring window opens.
- Enter
MyGamein Name under New definition. - Press Create and preview.
- When generation and compilation finish, the GS2 Local Preview window opens.
In this step, you don’t need to select a Project or expand or change Generation settings. The creation and output locations are set automatically. You don’t need to log in or configure a server yet.
If MyGame already exists, don’t recreate it; select GameRules/MyGame/project.json from the … of Existing definition > Project and press Generate and preview.
2. Operate It in the Preview
GS2 Local Preview has View at the top and Operations at the bottom.
- View > State > Progress: The number that operations increase. It starts at
0. - Operations: Has a
choiceselector and an Advance button.
Change choice to Small and press Advance. Progress becomes 1, and Accepted: Advance is displayed. Advance here is the operation button that moves the game forward.
| Value chosen in choice | Result of pressing Advance |
|---|---|
Small | Progress increases by 1 |
Large | Progress increases by 3, but does not exceed 9 |
None | Progress does not change, and Ignored: InvalidChoice (1001) is displayed |
If you press Reset and then run Large three times, Progress changes 0 → 3 → 6 → 9. When it reaches 9, 9 is also shown in Result > Progress, and the Advance button can no longer be pressed. To try again, press Reset.
You can use this preview without pressing Unity’s Play button.
3. Change the Amount Small Adds from 1 to 2
Go back to the GS2 C# Authoring window and press Open rules. The rules folder appears in Finder / Explorer; open MyGame.cs in a text editor or IDE.
The file is at GameRules/MyGame/Rules/MyGame.cs directly under the Unity project. Because it is outside Assets, it does not appear in Unity’s Project window.
The template contains the following rules (line breaks added for readability).
using System;
using Gs2.LocalStateMachine.Authoring;
using Step = Gs2.LocalStateMachine.Authoring.Step<
GameRules.MyGameState, GameRules.MyGameResult, GameRules.MyGameError>;
namespace GameRules;
public enum MyGameError { InvalidChoice = 1001 }
public enum MyGameChoice { None, Small, Large }
public record MyGameState(int Progress);
public record MyGameResult(int Progress);
public static class MyGame
{
public static Step Start() =>
Step.WaitFor(nameof(Advance), new MyGameState(0));
public static Step Advance(MyGameState state, MyGameChoice choice)
{
if (choice != MyGameChoice.Small && choice != MyGameChoice.Large)
return Step.Ignore(MyGameError.InvalidChoice);
var amount = 1;
if (choice == MyGameChoice.Large)
amount = 3;
var next = state with { Progress = Math.Min(9, state.Progress + amount) };
if (next.Progress == 9)
return Step.End(new MyGameResult(next.Progress), next);
return Step.WaitFor(nameof(Advance), next);
}
}It processes in the following order.
Startcreates a state with Progress 0 and waits forAdvanceinput.Advancereceives the current statestateand thechoicethe player selected.- If it is
None, it returnsIgnoreand does not accept the input. - If the choice is valid, it creates the updated state with
state with { ... }. - When it reaches 9, it finishes with
End; otherwise, it waits for the nextAdvance.
MyGameState is the in-progress state, and MyGameResult is the result you receive on finishing. using Step = ... is shorthand for specifying these two types together with the type for reasons to ignore input.
Change var amount = 1; to var amount = 2; and save. Return to Unity and press GS2 C# Authoring > Generate and preview. The preview starts over, and this time Small changes Progress 0 → 2.
Generate and preview generates GSL and a Session from the definition. GSL is the file that runs the rules, and the Session is a C# class for calling operations from Unity. From now on, you can try changes with “edit and save the rules → Generate and preview”. Don’t edit the generated GSL or Session directly.
4. Call It from the Unity Game Screen
Next, operate the same MyGame from buttons in a scene.
The C# that writes the game rules and the C# that writes the Unity screen have different roles.
| What you write | Role |
|---|---|
| Game definition | Conditions for accepting operations, state changes, and finish conditions |
| Unity screen | Calls operations from buttons and displays the current state |
Here, you write the Unity screen side.
- Create MyGamePreview.cs under
Assetsand save the following content. - Create an empty GameObject in the scene and attach this script.
- Press Unity’s Play button.
- Press Small / Large in the Game view and check how Progress changes. Since you changed the rules in the previous step, pressing Small increases Progress by 2.
#if UNITY_EDITOR && !GS2_DISABLE_LOCAL_STATE_MACHINE_KIT
using System.IO;
using Generated.MyGame;
using Gs2.Unity.Gs2StateMachine.Local;
using UnityEngine;
public sealed class MyGamePreview : MonoBehaviour
{
private MyGameSession game;
private void Start()
{
var path = Path.Combine(Application.dataPath, "Generated/MyGame/machine.gsl");
game = MyGameSession.FromExecutor(
LocalStateMachineExecutor.CreatePreview(File.ReadAllText(path), "MyGame"));
}
private void OnGUI()
{
if (game == null) return;
GUILayout.Label("Progress: " + game.View.State.Progress);
using (new UnityEditor.EditorGUI.DisabledScope(!game.View.Actions.Advance))
{
if (GUILayout.Button("Small")) game.Advance(MyGameChoice.Small);
if (GUILayout.Button("Large")) game.Advance(MyGameChoice.Large);
}
if (game.View.Result != null)
GUILayout.Label("Finished: " + game.View.Result.Progress);
}
}
#endifgame.Advance(...) calls the operation, and game.View.State is the current state used for display. The runtime passes the state argument of the definition’s Advance, so the screen side passes only choice.
game.View.Actions.Advance indicates “whether Advance can be called now”. In this example, it becomes false after finishing, so the buttons are disabled. If there are game-specific conditions such as insufficient materials, check them inside the operation function and return Ignore.
View.State is a read-only copy. Call operation functions instead of rewriting the state directly. If you want to be notified only on changes, you can add the following line right after creating the Session in Start.
game.ViewChanged += view => Debug.Log("Progress: " + view.State.Progress);You have now tried editing the definition, regenerating, and operating from the game screen. This MyGamePreview is code for checking inside the Editor. To build it into your app, proceed to the following connection steps.
5. Use Server Verification
Even when you connect to the server, the game.Advance(...) and game.View.State you call on the screen side stay the same. What changes is how the Session is created and how the operation history is sent.
First, prepare the GS2 side.
- Prepare a GS2-StateMachine namespace and enable speculative execution.
- Read
Assets/Generated/MyGame/machine.gsland specify it as thepayloadof the master registration.mainStateMachineNameisMyGame. See the master registration description. - From your game’s start logic, start the state machine for the target user. Use the start API or the start action.
The following is a UniTask example for use in an app that is already initialized and logged in. gs2 and gameSession are the initialized SDK connection and login information, namespaceName is the namespace name, and statusName is the name of the status you started in step 3.
var game = await Generated.MyGame.MyGameSession.OpenAsync(
gs2, gameSession, namespaceName, statusName);
var outcome = game.Advance(Generated.MyGame.MyGameChoice.Small);
if (outcome.Ignored)
Debug.Log("Input was not accepted: " + outcome.Code);
await game.ConfirmAsync(gs2, gameSession);OpenAsync connects to the status you started in step 3. If you don’t use UniTask, use OpenFuture / ConfirmFuture.
ConfirmAsync sends the operation history so far to the server and waits for the verification result. You can call it not only at the end but also in the middle of the game. The server runs the same rules and checks the results. Write important rules such as damage calculations and reward conditions in the game definition as well.
The Editor preview has no history to send to the server. Instead of playing the preview partway and then connecting, open the state started on the server with OpenAsync.
Look Up the Definition Specifications
- Add rewards: An example of specifying rewards with SDK request types.
- Switch accepted operations: When to use
WaitFor/Stay/Next/End. - Use random numbers: How to use random numbers that the server can reproduce.
- C# Definition Reference: Declaration rules, data types, and the subset of C# you can use.
- StateMachine Speculative Execution Session API: APIs for operations, state reference, and server verification.
- Generation Settings and Registration: project.json, SDK references, and handling generated files.
If Things Don’t Go as Described
| Situation | What to check |
|---|---|
| Create and generate is shown instead of Create and preview | Remove GS2_DISABLE_LOCAL_STATE_MACHINE_KIT and recompile. You don’t need to remove it if you use only the generation feature |
| You cannot create with the same name | Select the existing project.json and press Generate and preview |
| Pressing Open rules does not open a code editor | It is a button that opens the folder; open MyGame.cs inside it in your editor |
| Behavior does not change after editing | Save the rules and press Generate and preview. Check the Console for generation errors |
| You cannot operate after reaching 9 | The game has finished. In the preview window, press Reset; in a scene, stop Play and start again |
If generation fails, the previously successful output is kept. To avoid checking behavior with stale output while errors remain, resolve the errors in the Console before regenerating.