C# State Machine Definition Tutorial

A tutorial that builds a small game in the Unity Editor, edits C# rules, and operates them from the game screen

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 Progress starts at 0.
  • Choosing Small increases it by 1, and choosing Large increases it by 3.
  • It finishes when Progress reaches 9. Even if an operation would exceed 9, the value is capped at 9.

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.

  1. From the Unity menu, select GS2 > Local State Machine > Generate from C#. The GS2 C# Authoring window opens.
  2. Enter MyGame in Name under New definition.
  3. Press Create and preview.
  4. 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 choice selector 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 choiceResult of pressing Advance
SmallProgress increases by 1
LargeProgress increases by 3, but does not exceed 9
NoneProgress 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.

  1. Start creates a state with Progress 0 and waits for Advance input.
  2. Advance receives the current state state and the choice the player selected.
  3. If it is None, it returns Ignore and does not accept the input.
  4. If the choice is valid, it creates the updated state with state with { ... }.
  5. When it reaches 9, it finishes with End; otherwise, it waits for the next Advance.

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 writeRole
Game definitionConditions for accepting operations, state changes, and finish conditions
Unity screenCalls operations from buttons and displays the current state

Here, you write the Unity screen side.

  1. Create MyGamePreview.cs under Assets and save the following content.
  2. Create an empty GameObject in the scene and attach this script.
  3. Press Unity’s Play button.
  4. 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);
    }
}
#endif

game.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.

  1. Prepare a GS2-StateMachine namespace and enable speculative execution.
  2. Read Assets/Generated/MyGame/machine.gsl and specify it as the payload of the master registration. mainStateMachineName is MyGame. See the master registration description.
  3. 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

If Things Don’t Go as Described

SituationWhat to check
Create and generate is shown instead of Create and previewRemove 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 nameSelect the existing project.json and press Generate and preview
Pressing Open rules does not open a code editorIt is a button that opens the folder; open MyGame.cs inside it in your editor
Behavior does not change after editingSave the rules and press Generate and preview. Check the Console for generation errors
You cannot operate after reaching 9The 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.