GS2-StateMachine

state machine management function

GS2-Quest provided a mechanism to manage the start and end of quests and receive rewards upon completion according to the quests started. However, GS2-Quest had a problem handling game specifications where ingame randomness is strong and rewards are difficult to specify in advance.

GS2-StateMachine was developed to manage ingame state at a finer granularity.

State Machine

The state machine is used to manage ingame state.

flowchart TD
  Start ----> MainStateMachine_Initialize
  MainStateMachine_Pass ----> Exit
  subgraph MainStateMachine
    MainStateMachine_Initialize[[Initialize]] -->|Pass| MainStateMachine_ChoiceSkill

    MainStateMachine_ChoiceSkill[/ChoiceSkill/]

    MainStateMachine_InGame([InGame]) -->|Pass| MainStateMachine_NextTurn
    MainStateMachine_InGame([InGame]) -->|Fail| MainStateMachine_Pass

    MainStateMachine_NextTurn[[NextTurn]] -->|Next| MainStateMachine_ChoiceSkill
    MainStateMachine_NextTurn[[NextTurn]] -->|Exit| MainStateMachine_Pass

    MainStateMachine_Pass[\Pass/]


    subgraph ChoiceSkill
        ChoiceSkill_Initialize[[Initialize]] -->|Pass| ChoiceSkill_LotterySkills

        ChoiceSkill_LotterySkills[[LotterySkills]] -->|Pass| ChoiceSkill_WaitChoiceSkill

        ChoiceSkill_WaitChoiceSkill([WaitChoiceSkill]) -->|ChoiceSkill| ChoiceSkill_ChoiceSkill
        ChoiceSkill_WaitChoiceSkill([WaitChoiceSkill]) -->|ReLotterySkill| ChoiceSkill_ReLotterySkill

        ChoiceSkill_ReLotterySkill[[ReLotterySkill]] -->|Pass| ChoiceSkill_LotterySkills
        ChoiceSkill_ReLotterySkill[[ReLotterySkill]] -->|AlreadyReLottery| ChoiceSkill_WaitChoiceSkill

        ChoiceSkill_ChoiceSkill[[ChoiceSkill]] -->|Pass| ChoiceSkill_Pass
        ChoiceSkill_ChoiceSkill[[ChoiceSkill]] -->|InvalidSkillIndex| ChoiceSkill_WaitChoiceSkill

        ChoiceSkill_Pass[\Pass/]


    end
  end

  MainStateMachine_ChoiceSkill --> ChoiceSkill_Initialize
  ChoiceSkill_Pass -->|Pass| MainStateMachine_InGame

  Player ----->|Interaction| MainStateMachine_InGame
  Player ----->|Interaction| ChoiceSkill_WaitChoiceSkill

If you are a programmer, you have probably received a flowchart like the one above from your planner. Such a representation of state transitions is a state machine.

It keeps track of which state the player is currently in and what variables are available in the state machine. The state machine will eventually transition to the end state, and the rewards can be determined by the state variables that the state machine has at that time.

Events and transitions

Transitions are what connect the states of a state machine. Transitions set the conditions for moving from one state to the next.

The condition can be the receipt of an event, and each type of event can change the next state to transition to. Events can be issued by scripts running in the state machine, or they can be issued by the player. This allows the process to branch according to the choices made by the player or the outcome of the game.

Events can be parameterized, so there is no need to prepare an event for each choice. The state machine can be kept simple by passing an event that says “selected from the choices” and a parameter that says “what was selected”.

State Machine Definition Language

The GS2 States Language (GSL), originally developed by GS2, is used to define the state machine. GSL is written in the following notation.

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;
}

For detailed specification, please refer to About GS2 States Language Specification.

Speculative execution of state machines

The state machine provided by GS2-StateMachine can be used for speculative execution in Unity. This feature allows server programs with complex logic to be executed without players experiencing communication time and without cheating.

State Machine Speculative Execution Mechanism

actor Player
participant "Game"
participant "GS2-SDK"
participant "Local State Machine"
participant "Event Stream"
participant "GS2-StateMachine"
Player -> "Game" : Play
"Game" -> "GS2-SDK" : Load State Machine
"GS2-SDK" -> "GS2-StateMachine" : Load State Machine
"GS2-SDK" <- "GS2-StateMachine" : State Machine
"GS2-SDK" -> "Local State Machine" : Start Local State Machine
"Local State Machine" -> "Event Stream" : Create Stream
"GS2-SDK" <- "Local State Machine" : Ready
"Game" <- "GS2-SDK" : Load Complete
group Game Loop
    Player -> "Game" : Control
    "Game" -> "Local State Machine" : Emit Message
    "Local State Machine" -> "Event Stream" : Write Emit Event
    "Local State Machine" -> "Local State Machine" : Change State
    "Local State Machine" -> "Event Stream" : Write State Variables Hash
    "Game" <- "Local State Machine" : State Variables
    note over "Game" : Rendering of game screen \nwith reference to state machine variables
    group Report Events [Every 3 sec.]
        "Event Stream" -> "GS2-StateMachine" : Report Events
        note over "GS2-StateMachine" : Replay the received event on the server \nto verify that the hash value of the state variable matches
        group Hash Mismatch
            "GS2-SDK" <- "GS2-StateMachine" : State Mismatch
            "Game" <- "GS2-SDK" : OnDetectStateMismatch
            note over "Game" : Reload when callback is received
            "Game" -> "GS2-SDK" : Load State Machine
            "GS2-SDK" -> "GS2-StateMachine" : Load State Machine
            "GS2-SDK" <- "GS2-StateMachine" : State Machine
            "GS2-SDK" -> "Local State Machine" : Restart Local State Machine(State rolls back for up to 3 seconds)
        end
    end
end

As shown in the figure, the local state machine has the ability to execute GSLs received from the server. However, it does not have the authority to rewrite user data on the server and only rewrites the local cache of the SDK for user data generated in the state machine.

The local state machine performs state transitions based on messages received from the game, and records hash values of received messages and state variables of the state machine in the event stream. The event stream sends a report to GS2-StateMachine every 3 seconds with any events that have occurred.

When GS2-StateMachine receives the report, it executes the events recorded in it against the state machine maintained by the server. When the state transitions occur, GS2-StateMachine verifies that they match the hash values of the state variables in the report, and if there are no problems until the last event, it actually rewrites the user data performed by the state machine and saves the state of the state machine in the database.

If the transition destination state is different or there is a mismatch in the hash value of the state variable, a “state mismatch” error is returned. This can be handled from the game program via the “OnDetectStateMismatch” callback in the local state machine. When a mismatch occurs, the event stream stops reporting to GS2-StateMachine and the local state machine must be re-created.

At this time, it is possible to roll back to the state that was last checked for consistency (the state stored in the database).

Random Numbers in State Machines

There will be times when a state machine will want to branch out processes based on random numbers. For such applications, a random number generator that shares a random number seed with the server can be used.

category = 1
result = util.shared_random(category)
if result.isError then
  fail(result['statusCode'], result['errorMessage'])
end
random_value = result["result"]

By specifying different values for category for each application, random numbers can be obtained based on different random number sequences. By utilizing this mechanism, it is possible to obtain a high level of resistance against the act of careful selection of random number values.

Random numbers generated by this method are guaranteed to get exactly the same random values on the server, and no state mismatch will occur based on the random numbers.

Transaction Processing in the State Machine

When rewriting user data in the process of state machine execution, use the dedicated syntax instead of using GS2-SDK for Lua. By doing so, speculative execution is possible for user data rewriting in the form of rewriting cache data held by the SDK without communication processing even during speculative execution.

For more details on the API, please refer to the transaction action documentation for each microservice.

transaction.execute({
consumeActions={},
acquireActions={
  transaction.service("inventory").acquire.acquire_simple_items_by_user_id({
    namespaceName="namespace",
    inventoryName="inventory",
    acquireCounts={
      {
        itemName="item",
        count=1,
      },
    },
  })
}
})

Communication process in a state machine

It is not recommended to communicate with the outside world in a state machine that uses speculative execution. This is because the possibility of a state mismatch increases dramatically if external factors cause a difference in the results of the state machine execution.

Environment for running the local state machine

To be able to use a local state machine, LocalStateMachineKit must be installed separately from GS2-SDK. You can install LocalStateMachineKit from the GS2-SDK Installer.

However, since you will be installing an additional open source library, you should be careful about the license description. Check GS2 LocalStateMachineKit for Unity for details.

Example implementation

Starting the state machine

Starting a state machine cannot be handled by the SDK for game engines. It should be set up as a reward for GS2-Quest or other microservices.

Send an event to the state machine.

    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();
    const auto Domain = Gs2->StateMachine->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Status(
        "$status1.name" // statusName
    );
    const auto Future = Domain->Emit(
        "event-0001",
        "{\"value1\": \"value1\", \"value2\": 2.0, \"value3\": 3}" // args
    );
    Future->StartSynchronousTask();
    if (Future->GetTask().IsError())
    {
        return false;
    }

    // obtain changed values / result values
    const auto Future2 = Future->GetTask().Result()->Model();
    Future2->StartSynchronousTask();
    if (Future2->GetTask().IsError()) return false;
    const auto Result = Future2->GetTask().Result();

Get state machine state.

    var item = await gs2.StateMachine.Namespace(
        namespaceName: "namespace-0001"
    ).Me(
        gameSession: GameSession
    ).Status(
        statusName: status1.Name
    ).ModelAsync();
    const auto item = Gs2->StateMachine->Namespace(
        "namespace-0001" // namespaceName
    )->Me(
        AccessToken
    )->Status(
        "$status1.name" // statusName
    ).Model();

Local state machine (speculative execution of state machine)

Start local state machine

    var localStateMachine = await LocalStateMachineExecutor.StartAsync(
        gs2: gs2,
        gameSession: GameSession,
        stateMachineNamespaceName: "namespace-0001,
        statusName: status1.Name
    );

Send event to local state machine

    localStateMachineExecutor.Emit(
        "Select",
        new MapVariableValue(new Dictionary<string, IVariableValue> {
            ["x"] = new IntVariableValue(x),
            ["y"] = new IntVariableValue(y),
        }
    ));

Handling state mismatch

When you receive a state mismatch callback, you should re-fetch the latest state machine state from the server and start over from the local state machine startup.

    localStateMachineExecutor.OnDetectStateMismatch += (namespaceName, statusName) =>
    {
        Debug.LogWarning("detect state mismatch!");
    };

Dispatching event streams

While using the local state machine, the Dispatch function must be called at regular intervals to ensure that the event stream sends events to the GS2-StateMachine.

    private async UniTask Dispatch() {
        while (true) {
            await this._localStateMachineExecutor.DispatchAsync(
                Login.Gs2,
                Login.GameSession
            );
            await UniTask.Delay(TimeSpan.FromMilliseconds(100));
        }
    }

Detailed Reference