> For the complete documentation index, see [llms.txt](/ko/llms.txt)


# C# 스테이트 머신 정의 튜토리얼

Unity Editor에서 작은 게임을 만들고, 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 이상이 대상입니다.

1. Unity 메뉴에서 **GS2 > Local State Machine > Generate from C#**을 선택합니다. **GS2 C# Authoring** 창이 열립니다.
2. **New definition**의 **Name**에 `MyGame`을 입력합니다.
3. **Create and preview**를 누릅니다.
4. 생성과 컴파일이 끝나면 **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 창에는 표시되지 않습니다.

템플릿의 내용은 다음 룰입니다(읽기 쉽도록 줄바꿈하였습니다).

```csharp
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);
    }
}
```

여기서는 다음 순서로 처리하고 있습니다.

1. `Start`에서 Progress가 0인 상태를 만들고, `Advance` 입력을 기다립니다.
2. `Advance`는 현재 상태 `state`와 플레이어가 선택한 `choice`를 받습니다.
3. `None`이면 `Ignore`를 반환하여 입력을 채택하지 않습니다.
4. 유효한 선택이면 `state with { ... }`로 갱신 후의 상태를 만듭니다.
5. 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의 화면 측을 작성합니다.

1. `Assets` 아래에 **MyGamePreview.cs**를 만들고, 다음 내용을 저장합니다.
2. 씬에 빈 GameObject를 만들고, 이 스크립트를 붙입니다.
3. Unity의 **Play** 버튼을 누릅니다.
4. Game 뷰의 **Small** / **Large**를 누르고 Progress의 변화를 확인합니다. 이전 순서에서 룰을 변경했으므로 Small을 누르면 Progress가 2 증가합니다.

```csharp
#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(...)`가 조작 호출, `game.View.State`가 표시에 사용하는 현재 상태입니다. 정의의 `Advance`에 있는 `state` 인수는 실행 환경이 전달하므로, 화면 측에서 전달하는 것은 `choice`뿐입니다.

`game.View.Actions.Advance`는 「지금 Advance를 호출할 수 있는지」를 나타냅니다. 이 예에서는 종료 후에 `false`가 되므로 버튼을 비활성화하고 있습니다. 소재 부족 등 게임 고유의 조건이 있는 경우에는 조작 함수 안에서 검사하여 `Ignore`를 반환합니다.

`View.State`는 읽기 전용 복사본입니다. 상태를 직접 변경하는 대신 조작 함수를 호출해 주세요. 변경 시에만 알림을 받고 싶은 경우에는 `Start`의 Session 생성 직후에 다음 행을 추가할 수 있습니다.

```csharp
game.ViewChanged += view => Debug.Log("Progress: " + view.State.Progress);
```

여기까지 정의 편집·재생성·게임 화면에서의 조작을 시험해 보았습니다. 이 `MyGamePreview`는 Editor 안에서 확인하기 위한 코드입니다. 앱에 통합하는 경우에는 다음 접속 절차로 진행합니다.

## 5. 서버 검증 사용하기

서버에 접속하더라도 화면 측에서 호출하는 `game.Advance(...)`와 `game.View.State`는 같습니다. 변경하는 것은 Session의 생성과 조작 이력의 전송입니다.

먼저 GS2 측을 준비합니다.

1. GS2-StateMachine의 네임스페이스를 준비하고, 투기적 실행을 활성화합니다.
2. `Assets/Generated/MyGame/machine.gsl`을 읽어 마스터 등록의 `payload`에 지정합니다. `mainStateMachineName`은 `MyGame`입니다. [마스터 등록 설명]()을 참조해 주세요.
3. 게임의 시작 처리에서 대상 사용자의 스테이트 머신을 시작합니다. [시작 API](#startstatemachinebyuserid)나 [시작용 액션](#gs2statemachinestartstatemachinebyuserid)을 이용합니다.

다음은 초기화·로그인을 마친 앱 안에서 사용하는 **UniTask 예제**입니다. `gs2`와 `gameSession`은 초기화가 끝난 SDK 접속과 로그인 정보, `namespaceName`은 네임스페이스 이름, `statusName`은 순서 3에서 시작한 스테이터스의 이름입니다.

```csharp
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`로 엽니다.

## 정의 사양 알아보기

- [보상 추가하기](#rewards): SDK의 리퀘스트 타입으로 보상을 지정하는 예.
- [받아들이는 조작 전환하기](#step): `WaitFor` / `Stay` / `Next` / `End`의 구분.
- [난수 사용하기](#random): 서버에서 재현할 수 있는 난수의 사용 방법.
- [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의 오류를 해소한 후 다시 생성해 주세요.



