C# 스테이트 머신 정의 튜토리얼
이 페이지에서는 **수치를 버튼 조작으로 늘리고, 9가 되면 종료하는 샘플 MyGame**을 만듭니다. 룰은 다음 세 가지입니다.
- 수치
Progress는0에서 시작합니다. Small을 선택하면1,Large를 선택하면3증가합니다.Progress가9가 되면 종료합니다.9를 초과하는 조작이라도 값은9로 맞춥니다.
순서 1~2에서 템플릿을 Editor의 프리뷰로 동작시키고, 순서 3에서 룰을 변경, 순서 4에서 Unity 씬에서 조작합니다. 순서 5에서 서버에 의한 검증을 추가합니다.
1. 템플릿 만들기
GS2 SDK for Unity, GS2-CSharp-SDK, LocalStateMachineKit의 C# 생성 기능을 포함한 버전을 도입한 Unity 프로젝트에서 조작합니다. 생성 기능은 Unity 2021.2 이상이 대상입니다.
- Unity 메뉴에서 **GS2 > Local State Machine > Generate from C#**을 선택합니다. GS2 C# Authoring 창이 열립니다.
- New definition의 Name에
MyGame을 입력합니다. - Create and preview를 누릅니다.
- 생성과 컴파일이 끝나면 GS2 Local Preview 창이 열립니다.
이 순서에서는 Project 선택이나 Generation settings의 펼치기·변경은 필요하지 않습니다. 생성 위치와 출력 위치는 자동으로 설정됩니다. 로그인이나 서버 설정도 아직 필요하지 않습니다.
MyGame이 이미 존재하는 경우에는 다시 만들지 말고, Existing definition > Project의 **…**에서 GameRules/MyGame/project.json을 선택한 후 Generate and preview를 눌러 주세요.
2. 프리뷰에서 조작하기
GS2 Local Preview에는 위쪽의 View와 아래쪽의 Operations가 있습니다.
- View > State > Progress: 조작으로 늘리는 수치. 처음에는
0입니다. - Operations:
choice선택란과 Advance 버튼이 있습니다.
choice를 Small로 변경하고 Advance를 눌러 주세요. Progress가 1이 되고 Accepted: Advance라고 표시됩니다. 여기서 Advance는 게임을 진행시키는 조작 버튼입니다.
| choice에서 선택하는 값 | Advance를 누른 결과 |
|---|---|
Small | Progress가 1 증가한다 |
Large | Progress가 3 증가한다. 단, 9를 초과하지 않는다 |
None | Progress는 변하지 않고, Ignored: InvalidChoice (1001)이라고 표시된다 |
Reset을 누른 후 Large를 3회 실행하면 Progress가 0 → 3 → 6 → 9로 변화합니다. 9가 되면 Result > Progress에도 9가 표시되고, Advance 버튼은 누를 수 없게 됩니다. 다시 시험할 때는 Reset을 누릅니다.
이 프리뷰는 Unity의 Play 버튼을 누르지 않고 사용할 수 있습니다.
3. Small로 증가하는 양을 1에서 2로 바꾸기
GS2 C# Authoring 창으로 돌아가 Open rules를 누릅니다. Finder / 탐색기에 룰 폴더가 표시되므로, MyGame.cs를 텍스트 에디터나 IDE로 엽니다.
파일은 Unity 프로젝트 바로 아래의 GameRules/MyGame/Rules/MyGame.cs에 있습니다. Assets 바깥에 있으므로 Unity의 Project 창에는 표시되지 않습니다.
템플릿의 내용은 다음 룰입니다(읽기 쉽도록 줄바꿈하였습니다).
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);
}
}여기서는 다음 순서로 처리하고 있습니다.
Start에서 Progress가 0인 상태를 만들고,Advance입력을 기다립니다.Advance는 현재 상태state와 플레이어가 선택한choice를 받습니다.None이면Ignore를 반환하여 입력을 채택하지 않습니다.- 유효한 선택이면
state with { ... }로 갱신 후의 상태를 만듭니다. - 9에 도달하면
End로 종료하고, 그 외에는 다음Advance를 기다립니다.
MyGameState는 진행 중인 상태, MyGameResult는 종료 시에 받는 결과입니다. using Step = ...은 이 두 타입과 입력을 무시하는 이유의 타입을 한꺼번에 지정하기 위한 약기입니다.
var amount = 1;을 var amount = 2;로 변경하고 저장해 주세요. Unity로 돌아가 GS2 C# Authoring > Generate and preview를 누릅니다. 프리뷰는 처음부터 시작되며, 이번에는 Small로 Progress가 0 → 2가 됩니다.
Generate and preview는 정의로부터 GSL과 Session을 생성합니다. GSL은 룰을 실행하기 위한 파일, Session은 Unity에서 조작을 호출하기 위한 C# 클래스입니다. 이후에도 「룰을 편집하여 저장 → Generate and preview」로 시험할 수 있습니다. 생성된 GSL이나 Session은 직접 편집하지 않습니다.
4. Unity 게임 화면에서 호출하기
다음으로, 같은 MyGame을 씬 안의 버튼에서 조작합니다.
게임의 룰을 작성하는 C#과 Unity의 화면을 작성하는 C#은 역할이 다릅니다.
| 작성하는 것 | 역할 |
|---|---|
| 게임의 정의 | 조작을 받아들이는 조건, 상태의 변화, 종료 조건을 작성한다 |
| Unity의 화면 | 버튼에서 조작을 호출하고, 현재 상태를 표시한다 |
여기서는 Unity의 화면 측을 작성합니다.
Assets아래에 MyGamePreview.cs를 만들고, 다음 내용을 저장합니다.- 씬에 빈 GameObject를 만들고, 이 스크립트를 붙입니다.
- Unity의 Play 버튼을 누릅니다.
- Game 뷰의 Small / Large를 누르고 Progress의 변화를 확인합니다. 이전 순서에서 룰을 변경했으므로 Small을 누르면 Progress가 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(...)가 조작 호출, game.View.State가 표시에 사용하는 현재 상태입니다. 정의의 Advance에 있는 state 인수는 실행 환경이 전달하므로, 화면 측에서 전달하는 것은 choice뿐입니다.
game.View.Actions.Advance는 「지금 Advance를 호출할 수 있는지」를 나타냅니다. 이 예에서는 종료 후에 false가 되므로 버튼을 비활성화하고 있습니다. 소재 부족 등 게임 고유의 조건이 있는 경우에는 조작 함수 안에서 검사하여 Ignore를 반환합니다.
View.State는 읽기 전용 복사본입니다. 상태를 직접 변경하는 대신 조작 함수를 호출해 주세요. 변경 시에만 알림을 받고 싶은 경우에는 Start의 Session 생성 직후에 다음 행을 추가할 수 있습니다.
game.ViewChanged += view => Debug.Log("Progress: " + view.State.Progress);여기까지 정의 편집·재생성·게임 화면에서의 조작을 시험해 보았습니다. 이 MyGamePreview는 Editor 안에서 확인하기 위한 코드입니다. 앱에 통합하는 경우에는 다음 접속 절차로 진행합니다.
5. 서버 검증 사용하기
서버에 접속하더라도 화면 측에서 호출하는 game.Advance(...)와 game.View.State는 같습니다. 변경하는 것은 Session의 생성과 조작 이력의 전송입니다.
먼저 GS2 측을 준비합니다.
- GS2-StateMachine의 네임스페이스를 준비하고, 투기적 실행을 활성화합니다.
Assets/Generated/MyGame/machine.gsl을 읽어 마스터 등록의payload에 지정합니다.mainStateMachineName은MyGame입니다. 마스터 등록 설명을 참조해 주세요.- 게임의 시작 처리에서 대상 사용자의 스테이트 머신을 시작합니다. 시작 API나 시작용 액션을 이용합니다.
다음은 초기화·로그인을 마친 앱 안에서 사용하는 UniTask 예제입니다. gs2와 gameSession은 초기화가 끝난 SDK 접속과 로그인 정보, namespaceName은 네임스페이스 이름, statusName은 순서 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("입력을 채택하지 않았습니다: " + outcome.Code);
await game.ConfirmAsync(gs2, gameSession);OpenAsync는 순서 3에서 시작한 스테이터스에 접속합니다. UniTask를 사용하지 않는 경우에는 OpenFuture / ConfirmFuture를 사용합니다.
ConfirmAsync는 그때까지의 조작 이력을 서버에 전송하고 검증 결과를 기다립니다. 종료 시뿐만 아니라 게임 도중에도 호출할 수 있습니다. 서버는 같은 룰을 실행하여 결과를 대조합니다. 대미지 계산이나 보상 조건 등의 중요한 룰도 게임의 정의 측에 작성해 주세요.
Editor의 프리뷰에는 서버로 전송할 이력이 없습니다. 프리뷰를 도중까지 플레이한 후 접속하는 것이 아니라, 서버에서 시작한 상태를 OpenAsync로 엽니다.
정의 사양 알아보기
- 보상 추가하기: SDK의 리퀘스트 타입으로 보상을 지정하는 예.
- 받아들이는 조작 전환하기:
WaitFor/Stay/Next/End의 구분. - 난수 사용하기: 서버에서 재현할 수 있는 난수의 사용 방법.
- C# 정의 레퍼런스: 선언 규칙, 데이터 타입, 사용할 수 있는 C#의 범위.
- StateMachine의 투기적 실행 Session API: 조작·상태 참조·서버 검증 API.
- 생성 설정·등록: project.json, SDK 참조, 생성물의 취급.
순서대로 진행되지 않는 경우
| 상황 | 확인할 사항 |
|---|---|
| Create and preview가 아니라 Create and generate가 표시된다 | GS2_DISABLE_LOCAL_STATE_MACHINE_KIT를 제거하고 다시 컴파일한다. 생성 기능만 사용하는 경우에는 제거할 필요가 없다 |
| 같은 이름으로 생성할 수 없다 | 기존 project.json을 선택하고 Generate and preview를 누른다 |
| Open rules를 눌러도 코드 편집 화면이 나오지 않는다 | 폴더를 여는 버튼이므로, 그 안의 MyGame.cs를 에디터로 연다 |
| 편집 후에도 동작이 바뀌지 않는다 | 룰을 저장하고 Generate and preview를 누른다. Console에 생성 오류가 없는지 확인한다 |
| 9에 도달한 후 조작할 수 없다 | 게임이 종료된 상태. 프리뷰 창이라면 Reset, 씬이라면 Play를 정지하고 다시 시작한다 |
생성에 실패한 경우에는 이전에 성공한 생성물을 남겨 둡니다. 오류가 있는 상태에서 오래된 생성물로 동작을 확인하지 않도록, Console의 오류를 해소한 후 다시 생성해 주세요.