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


# StateMachine의 투기적 실행 Session API(Unity)

생성한 C# 클래스로 조작을 호출하고, 상태를 읽고, 서버 검증을 받는 API



**이 Session은 GS2-StateMachine의 투기적 실행을 Unity에서 사용하기 위한 클래스입니다.** 사용자의 진행 상태를 클라이언트에서 실행하고, 조작 이력을 StateMachine의 Report API로 전송하여 검증합니다. 게임 화면은 Session의 메서드로 조작하고, View에서 그 로컬 실행 결과를 읽습니다.

Realtime 룸 접속이나 다른 플레이어와의 동기화에는 사용하지 않습니다. 공유 오브젝트를 Unity에 반영하는 구조는 [Unity의 오브젝트 동기화]()를 참조해 주세요.

이 페이지는 호출 측의 API를 설명합니다. 게임 룰의 서식은 [C# 정의 레퍼런스](), 생성하는 절차는 [튜토리얼]()을 참조해 주세요. Session은 생성 설정의 [`hostProfile`](#project)이 `LocalReplayV1`(기본값)일 때 생성됩니다.

룰 클래스 `Counter`로부터는 `CounterSession`을 생성합니다. 다음에서는 그 인스턴스를 `game`으로 표기합니다. 상태·결과·입력의 타입은 생성 설정의 `namespace`에 생성합니다.

| 목적 | API |
| --- | --- |
| 게임을 조작한다 | [정의의 조작과 같은 이름의 메서드](#operations) |
| 현재 상태를 표시한다 | [View / ViewChanged](#view) |
| 서버에서 시작된 게임을 연다 | [OpenAsync / OpenFuture](#open) |
| 프리뷰 등의 실행 환경에 접속한다 | [FromExecutor](#executor) |
| 조작 이력을 서버에서 검증한다 | [ConfirmAsync / ConfirmFuture](#confirmation) |

## 조작 메서드 {#operations}

정의에 있는 public 조작과 같은 이름의 메서드를 생성합니다. 상태·난수·보상 인수는 실행 환경이 전달하므로 Session 측의 인수에는 포함하지 않습니다.

```csharp
// 정의 측: public static Step Add(CounterState state, int amount)
var outcome = game.Add(3);
```

반환값은 `InputResult<TCode>`입니다. `TCode`는 정의에서 지정한 Ignore 이유의 enum입니다.

| 프로퍼티 | 타입 | 내용 |
| --- | --- | --- |
| `Accepted` | `bool` | 로컬 실행에서 입력을 채택했는지 |
| `Ignored` | `bool` | Ignore에 의해 입력을 취소했는지 |
| `Code` | `TCode?` | Ignore의 이유. 이유가 없는 경우나 알 수 없는 코드는 null |
| `RawCode` | `int?` | Ignore 이유의 원래 수치. 이유가 없는 경우는 null |

Accepted는 서버 검증이 끝났다는 의미가 아닙니다. [Confirm](#confirmation)으로 별도의 검증을 받습니다.

```csharp
var outcome = game.Add(0);
if (outcome.Ignored)
    Debug.Log("입력을 받아들이지 않았습니다: " + outcome.Code);
```

현재 받아들이지 않는 조작을 호출하면 예외가 발생합니다. 0으로 나누기 등의 실행 오류도 Ignore와는 달리 정상적인 조작 결과로 반환되지 않습니다.

## View / ViewChanged {#view}

`game.View`는 현재 상태의 스냅샷입니다. 생성하는 상태·결과 클래스의 프로퍼티는 읽기 전용입니다. 상태를 변경할 때는 조작 메서드를 호출합니다.

```csharp
var count = game.View.State.Count;
var canAdd = game.View.Actions.Add;
```

| 멤버 | 내용 |
| --- | --- |
| `View.State` | 현재 상태. 타입은 정의의 TState에 대응하는 생성 클래스 |
| `View.Result` | 종료 결과. 타입은 정의의 TResult에 대응하는 생성 클래스. 결과가 없는 동안에는 null |
| `View.Actions.조작 이름` | 현재 대기 위치에서 그 조작을 받아들이는지를 나타내는 bool |
| `View.Revision` | 조작 이력의 리비전 |
| `View.Status` | 실행 상태 `ActiveStateMachineStatus` |
| `ViewChanged` | View 갱신 시의 `Action<SessionView>` 알림 |

`View.Actions`는 「그 조작을 받아들이는 위치에 있는지」를 나타냅니다. 소재 부족 등 조작 안에서 검사하는 게임 고유의 조건까지는 판정하지 않습니다.

Session을 생성한 후 다음과 같이 갱신 알림을 구독할 수 있습니다. Ignore에서는 ViewChanged가 발생하지 않습니다.

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

### View.Status의 값

| 값 | 의미 |
| --- | --- |
| `Initialized` | 실행 시작 전 |
| `Running` | 처리를 실행 중 |
| `Wait` | 다음 조작을 기다리고 있다 |
| `Pass` | 정상 종료. 정의의 End에 도달한 상태 |
| `Error` | 실행 오류로 정지한 상태 |

게임의 정상 종료는 `View.Status == ActiveStateMachineStatus.Pass`로 판정할 수 있습니다. 이는 로컬 실행 상태이며, 서버 검증의 완료를 나타내지는 않습니다.

## OpenAsync / OpenFuture {#open}

서버에서 시작된 스테이터스를 열어 Session을 생성합니다. 먼저 StateMachine의 [시작 API](#startstatemachinebyuserid) 등으로 대상 사용자의 스테이트 머신을 시작해 두어야 합니다.

```csharp
var game = await CounterSession.OpenAsync(
    gs2, gameSession, namespaceName, statusName);
```

| 인수 | 내용 |
| --- | --- |
| `gs2` | SDK의 접속 창구 `Gs2Domain` |
| `gameSession` | 로그인한 사용자의 정보 `IGameSession` |
| `namespaceName` | GS2-StateMachine의 네임스페이스 이름 |
| `statusName` | 시작된 스테이터스의 이름 |

Async 형식은 `UniTask<CounterSession>`을 반환하며, `GS2_ENABLE_UNITASK`가 필요합니다. UniTask를 사용하지 않는 경우에는 같은 인수의 OpenFuture를 사용합니다. 반환값은 `Gs2Future<CounterSession>`입니다.

접속 시에는 GSL의 해시를 대조합니다. Session과 등록된 정의가 일치하지 않으면 오류입니다. 정의를 갱신한 경우에는 Session도 다시 생성하고, 이전 정의로 진행 중인 게임에는 이전 정의에 대응하는 Session을 사용합니다.

## FromExecutor {#executor}

생성된 Executor에 Session을 접속합니다. Executor는 GSL의 실행과 조작 이력을 다루는 LocalStateMachineKit의 실행 클래스입니다.

```csharp
var game = CounterSession.FromExecutor(executor);
```

인수는 `LocalStateMachineExecutor`, 반환값은 `CounterSession`입니다. 이 API에서도 정의의 해시를 대조합니다.

예를 들어 `LocalStateMachineExecutor.CreatePreview(gsl, "Counter")`로 생성한 Executor를 전달하면 로그인 없이 프리뷰에 접속할 수 있습니다. 구체적인 생성 예는 [튜토리얼]()을 참조해 주세요. 프리뷰에는 서버 전송용 이력이 없으며, 도중에 실제 서비스로 인계할 수 없습니다.

## ConfirmAsync / ConfirmFuture {#confirmation}

그때까지의 조작 이력을 서버로 전송하고 검증 결과를 기다립니다. 게임 도중이든 종료 후든 호출할 수 있습니다.

```csharp
await game.ConfirmAsync(gs2, gameSession);
```

인수는 Open과 같은 `Gs2Domain`과 `IGameSession`입니다. Async 형식은 `UniTask`를 반환하며, `GS2_ENABLE_UNITASK`가 필요합니다. UniTask를 사용하지 않는 경우에는 ConfirmFuture를 사용합니다. 반환값은 `Gs2Future`입니다.

로컬에서의 입력 수리, 서버 검증, 보상 반영은 각각 다음 방법으로 확인합니다.

| 단계 | 확인 방법 | 확인할 수 있는 것 |
| --- | --- | --- |
| 로컬 실행 | 조작 결과의 `Accepted` | 로컬에서 입력이 채택되었다 |
| 서버 검증 | Confirm의 정상 완료 | 전송한 조작 이력이 서버에서 수리되었다 |
| 보상 반영 | 입수 대상 서비스의 상태 취득 | 비동기 보상 처리가 인벤토리 등에 반영되었다 |

Confirm의 성공만으로는 보상 반영까지 완료되었다고 판단할 수 없습니다.

### 전송 상태 참조

| 프로퍼티 | 내용 |
| --- | --- |
| `PendingEventCount` | 보류 중인 이벤트 수 |
| `IsConfirming` | 전송·확인 중인지. 이 동안에는 새로운 입력을 받아들이지 않는다 |
| `RequiresReconciliation` | 최신 서버 상태로부터 Session을 다시 만들어야 하는지 |

### Confirm이 완료·실패한 후

| 결과 | 이력의 취급 | 다음 조작 |
| --- | --- | --- |
| 정상 완료 | 전송한 보류 이력을 확인 완료로 제거한다 | 게임이 입력 대기 중이면 조작을 계속할 수 있다 |
| 실패하고 `RequiresReconciliation`이 false | 미확인 이력을 유지한다 | 원인을 해소하고 같은 Session의 Confirm을 다시 실행한다 |
| 실패하고 `RequiresReconciliation`이 true | 같은 Session으로부터의 재전송·새로운 입력을 받아들이지 않는다 | 최신 서버 상태를 Open으로 다시 열어 화면을 복원한다 |

재전송 시의 이력과 중복 방지 키는 SDK가 보유합니다. 조작 자체를 다시 호출하여 재현할 필요는 없습니다. 재전송 기한을 초과한 경우에는 다음 Confirm에서 RequiresReconciliation이 true가 되어 다시 열어야 합니다.

실패 후에도 IsConfirming과 RequiresReconciliation이 모두 false라면, 현재 받아들이고 있는 조작을 로컬에서 계속할 수 있습니다. 추가한 이력도 이후의 Confirm에서 전송합니다. 단, 미확인 결과를 서버에서 확정된 것으로 취급하지 마세요.

보류 이력은 메모리 안에 보유합니다. 다시 연 Session에는 서버에서 아직 검증되지 않은 로컬 조작이 인계되지 않습니다. 앱 종료를 넘어서는 저장은 별도의 설계가 필요합니다.

Session과 Executor는 Unity의 메인 스레드에서 사용해 주세요.



