GS2-StateMachine
GS2-Quest provided a mechanism to manage the start and end of quests and to receive rewards on completion according to the quest started. However, GS2-Quest had a problem handling game specifications where in-game randomness is strong and rewards are difficult to determine in advance.
GS2-StateMachine was developed to manage in-game state at a finer granularity.
State Machine
The state machine is what is used to manage in-game 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 manages which state the player is currently in and the values of the variables usable inside the state machine. The state machine will eventually transition to an end state, and rewards can be determined by the state variables the state machine holds at that time.
Events and Transitions
What connects the states of a state machine are transitions. A transition sets the conditions under which one state transitions to the next state.
The condition can be the receipt of an event, and the next state to transition to can vary by event type. Events can be emitted from scripts running inside the state machine, but can also be accepted from the player. This allows processing to branch according to the choices the player made or the game result.
Events can carry parameters, so even without preparing one event per choice, you can keep the state machine simple by passing an event meaning “selected from the choices” together with a parameter representing “what was selected.”
State Machine Version Management
State machines are versioned, and even if you update the state machine contents, those started before continue to operate with the state machine definition that was in use at startup time. This allows you to apply a new state machine definition without affecting running state machines.
If you make a change that breaks compatibility, please use the namespace setting mechanism that treats state machines started with a version below the specified version as deleted.
Note that only the state machine definition is versioned. The GS2-Script referenced by the state machine is not versioned, so caution is required.
A started state machine can have an expiration set in minutes; when the expiration is reached, it is automatically deleted.
State Machine Definition Language
The state machine is defined using GS2 States Language (GSL), which was developed by GS2. GSL is written using 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 specifications, please refer to About the GS2 States Language specification.
Speculative Execution of State Machines
The state machine provided by GS2-StateMachine can use speculative execution in Unity. This feature allows server programs with complex logic to be realized without making players feel communication time, while still preventing 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" : Render game screen based on state variables
group Report Events [Every 3 sec.]
"Event Stream" -> "GS2-StateMachine" : Report Events
note over "GS2-StateMachine" : The server replays the events\nto verify that the state variable hash values match
group Hash Mismatch
"GS2-SDK" <- "GS2-StateMachine" : Notify State Mismatch
"Game" <- "GS2-SDK" : OnDetectStateMismatch
note over "Game" : Reload the state machine
"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
endAs shown in the figure, the local state machine has the ability to execute GSL received from the server. However, it does not have the authority to rewrite user data on the server, and user data rewrites that occur inside the state machine only rewrite the SDK’s local cache.
The local state machine performs state transitions based on messages received from the game, while recording the hash values of received messages and the state machine’s state variables in the event stream. The event stream sends a report to GS2-StateMachine every 3 seconds if any events have occurred.
When GS2-StateMachine receives the report, it executes the events recorded in it against the state machine maintained on the server. When state transitions occur, it verifies that they match the hash values of state variables included in the report; if there are no problems up to the last event, it actually executes the user data rewrites performed by the state machine and saves the state of the state machine in the database.
If the destination state differs or there is a mismatch in state variable hash values, a “state mismatch” error is returned.
This can be handled from the game program via the local state machine’s OnDetectStateMismatch callback.
Once a mismatch occurs, the event stream stops reporting to GS2-StateMachine, so the local state machine must be re-created.
At this time, a rollback may occur up to the last state in which consistency was confirmed (the state saved to the database).
Random numbers in state machines
There will be times when a state machine wants to branch processing based on random numbers. For such purposes, 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 per use, you can obtain random numbers based on different random number sequences.
By leveraging this mechanism, you can achieve a high level of resistance against cherry-picking of random number values.
Random numbers generated by this method are guaranteed to yield exactly the same values on the server, and state mismatches based on random numbers do not occur.
Transaction processing in state machines
When rewriting user data during the execution of the state machine, do not use GS2-SDK for Lua; use the dedicated syntax instead. By doing so, even during speculative execution, the SDK can rewrite its cached data without communication, allowing user data rewrites to also benefit from speculative execution.
For details on the API, please refer to the transaction action documentation of 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 processing in state machines
In a state machine that uses speculative execution, it is not recommended to communicate with the outside world. The reason is that if external factors cause differences in the result of state machine execution, the likelihood of state mismatch increases dramatically.
Local state machine runtime environment
To be able to use a local state machine, you must install LocalStateMachineKit separately from GS2-SDK. LocalStateMachineKit can be installed from the GS2-SDK Installer.
However, since this introduces additional open source libraries, you should pay attention to license notices. For details, see GS2 LocalStateMachineKit for Unity.
Script Integration
When a Task state is executed inside the state machine, GS2-Script can be invoked. By returning an “event” as the result of the script execution, you can transition the state machine based on the script’s logic.
Master Data Management
Registering master data allows you to configure data and behaviors available to the microservice.
Master data types include the following:
StateMachineMaster: State machine definition (GSL). It is versioned; if you want to make a breaking change, you can uselowestStateMachineVersionin the namespace settings to treat state machines started with an older version as deleted.
Master data can be registered via the Management Console, by reflecting data from GitHub, or by setting up workflows to register via CI using GS2-Deploy.
Script Triggers
You can invoke GS2-Script when a state machine starts or when it reaches an end state.
The main configurable event triggers and script setting names are as follows:
startScript: When the state machine startspassScript: When the state machine reaches a PassTaskerrorScript: When the state machine reaches an ErrorTask
Note that GS2-Script invoked from a Task state inside the state machine is specified separately via the Script directive in GSL and is used to drive state transitions themselves.
Transaction Actions
GS2-StateMachine provides the following transaction actions:
- Acquire actions: start a state machine
By using “start a state machine” as an acquire action, you can safely execute, within a transaction, processing that directly starts a specific in-game session (such as a game loop involving complex state management, like roguelike dungeon exploration) as a reward when an item is purchased at a shop or a mission is achieved. This enables a smooth experience that connects purchase to play start.
For rewriting user data from within the state machine, you can issue transaction actions consistent with the speculative execution of the local state machine using the transaction.execute syntax. See Transaction processing in state machines for details.
Example Implementation
Starting the state machine
Starting a state machine cannot be processed by the game engine SDK. Please configure it as a reward in a microservice such as GS2-Quest.
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(
"status-0001" // 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: "status-0001"
).ModelAsync(); const auto Domain = Gs2->StateMachine->Namespace(
"namespace-0001" // namespaceName
)->Me(
AccessToken
)->Status(
"status-0001" // statusName
);
const auto Future = Domain->Model();
Future->StartSynchronousTask();
if (Future->GetTask().IsError()) return false;
const auto Item = Future->GetTask().Result();Local state machine (speculative execution of state machine)
Start a local state machine
var localStateMachine = await LocalStateMachineExecutor.StartAsync(
gs2: gs2,
gameSession: GameSession,
stateMachineNamespaceName: "namespace-0001",
statusName: "status-0001"
);Send an event to the 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, re-fetch the latest state machine state from the server and restart from the local state machine startup.
localStateMachineExecutor.OnDetectStateMismatch += (namespaceName, statusName) =>
{
Debug.LogWarning("detect state mismatch!");
};Dispatching the event stream
While using the local state machine, you must call the Dispatch function at a regular interval so that the event stream sends events to GS2-StateMachine.
private async UniTask Dispatch() {
while (true) {
await this._localStateMachineExecutor.DispatchAsync(
Login.Gs2,
Login.GameSession
);
await UniTask.Delay(TimeSpan.FromMilliseconds(100));
}
}