C# 스테이트 머신 정의 레퍼런스

C#으로 기술하는 상태, 조작, 처리의 계속, 보상, 난수의 사양

C# 클래스로 게임이 보유하는 데이터와 플레이어의 조작에 대한 처리를 정의합니다. 이 페이지는 그 서식과 API의 레퍼런스입니다.

정의 생성에는 LocalStateMachineKit를 사용합니다. C# 정의로부터 GS2-StateMachine에 등록할 GSL을 생성합니다. 투기적 실행을 사용하는 경우에는 Unity에서 조작을 호출하기 위한 C# 클래스도 생성합니다.

생성하는 GSL의 실행 대상은 생성 설정의 hostProfile에서 선택합니다. 이 페이지는 기본값인 LocalReplayV1을 전제로 설명합니다.

hostProfile실행 방식이 페이지와의 차이
LocalReplayV1(기본값)StateMachine의 투기적 실행. 클라이언트에서 조작을 실행하고, 서버가 조작 이력을 재생하여 검증한다없음
RealtimeRoomV1Realtime 룸 안에서 실행한다클래스·record·Start·Step의 작성 방법은 같다. GameRewards와 GameRandom 대신 Room API를 사용한다

RealtimeRoomV1의 입력, Room API, 동기화 항목과 조작 권한, 룸 안에서의 실행 규칙은 룸 정의 레퍼런스를 참조해 주세요.

정의의 구성 요소

하나의 정의는 다음 요소로 구성합니다.

요소의미예
상태다음 조작으로 인계하는 게임 데이터소지 수, HP, 선택한 루트
조작외부로부터 받아들이는 입력과 그 처리를 기술한 메서드아이템을 사용한다, 루트를 선택한다
결과게임 종료 시에 남기는 데이터최종 스코어, 클리어 여부
Step처리 후의 상태와 다음에 무엇을 할지를 반환하는 값조작을 기다린다, 처리를 계속한다, 종료한다

여기서 말하는 「상태」는 저장하는 데이터입니다. 「현재 어떤 조작을 받아들일지」는 별도로 Step에서 지정합니다.

Step<상태 타입, 결과 타입, 입력을 거부하는 이유의 타입>의 세 타입을 정의 전체에서 통일합니다. 다음 예는 using Step = ...으로 이 긴 타입명을 Step으로 약기하고 있습니다.

정의 전체 예

수치 Count를 보유하는 Counter의 정의입니다. Add로 지정한 수를 더하고, Finish로 종료합니다. Add와 Finish는 이 예에서 정의한 조작 이름입니다.

using Gs2.LocalStateMachine.Authoring;
using Step = Gs2.LocalStateMachine.Authoring.Step<
    GameRules.CounterState, GameRules.CounterResult, GameRules.CounterError>;

namespace GameRules;

public record CounterState(int Count);
public record CounterResult(int Count);
public enum CounterError { InvalidAmount = 1001 }

public static class Counter
{
    public static Step Start() =>
        Step.WaitFor(nameof(Add), new CounterState(0)).Or(nameof(Finish));

    public static Step Add(CounterState state, int amount)
    {
        if (amount < 1 || amount > 10)
            return Step.Ignore(CounterError.InvalidAmount);

        var next = state with { Count = state.Count + amount };
        return Step.Stay(next);
    }

    public static Step Finish(CounterState state) =>
        Step.End(new CounterResult(state.Count), state);
}

Start는 시작 시에 한 번 실행합니다. 이 예에서는 Count를 0으로 하고 Add와 Finish를 받아들입니다. Add 처리 후에도 같은 두 조작을 받아들이며, Finish 처리 후에는 종료합니다.

선언 규칙

룰 클래스

public static class Counter
{
    // Start, 조작, 자동 처리, 보조 함수를 선언한다
}

클래스 이름이 스테이트 머신 이름이 됩니다. 하나의 소스 폴더에 룰 클래스를 하나 둡니다. 상태·결과의 record와 enum은 같은 폴더 안에서 파일을 나누어도 상관없습니다.

클래스 안에 선언할 수 있는 것은 static 메서드입니다. 필드, 프로퍼티, 생성자, 타입 파라미터는 사용할 수 없습니다.

메서드

다음의 Step은 같은 상태·결과·이유 타입을 지정한 Step<TState, TResult, TCode>를 가리킵니다.

종류선언 형태호출되는 시점
시작 처리public static Step Start()게임 시작 시. 필수
조작public static Step Add(TState state, int amount)외부로부터 Add 입력을 받았을 때
자동 처리private static Step Resolve(TState state)Step.Next로 지정되었을 때
보조 함수private static int Calculate(int value)정의 안의 메서드에서 일반 C# 함수로 호출했을 때

조작과 자동 처리의 차이는 외부로부터의 입력을 기다리는지 여부입니다. 자동 처리는 입력을 기다리지 않고 실행합니다. Step을 반환하는 메서드로 진행하는 경우에는 WaitFor 또는 Next를 사용하며, Add(...)처럼 직접 호출하지 않습니다.

메서드는 동기적인 static 메서드로 합니다. 다음 선언은 사용할 수 없습니다.

  • 같은 이름의 메서드 오버로드
  • async, 제네릭 메서드
  • ref / out / in 인수, params, 생략 가능한 인수

타입·메서드·인수 등의 이름에는 ASCII 영문자 또는 _로 시작하는 영숫자와 _를 사용합니다. 생성 코드의 예약어나 이름 충돌은 생성 시에 검사합니다.

인수를 전달하는 측

Start는 인수를 받지 않습니다. 그 외의 Step을 반환하는 메서드에서는 첫 번째 인수를 상태 타입으로 합니다.

public static Step Add(CounterState state, int amount, GameRewards rewards)

이 선언에서 호출자가 전달하는 것은 amount뿐입니다. 현재의 state와 보상용 rewards는 실행 환경이 전달합니다. 생성되는 Unity용 클래스에서는 game.Add(3)으로 호출합니다.

인수값을 전달하는 측사용할 수 있는 곳
첫 번째 인수의 상태실행 환경조작·자동 처리에서 필수
GameRewards실행 환경조작·자동 처리. 각각 최대 하나
GameRandom<TStream>실행 환경조작·자동 처리. 각각 최대 하나
위 이외의 데이터조작의 호출자public 조작만

자동 처리에 새로운 외부 입력을 전달할 수는 없습니다. 조작에서 자동 처리로 값을 인계하는 경우에는 상태에 포함합니다. 입력 이름 revision은 예약되어 있으므로 사용할 수 없습니다.

초기화에서 난수나 보상을 사용하는 경우에는 Start에서 초기 상태를 만들고, Next로 필요한 인수를 가진 자동 처리로 진행합니다.

데이터 타입

상태·결과·조작 입력

타입취급
int부호 있는 32비트 정수
long부호 있는 64비트 정수. int32나 부동소수점으로 좁히지 않고 다룬다
float / double각각의 부동소수점 정밀도로 다룬다. NaN·Infinity는 저장·전송할 수 없다
bool진릿값
stringnull이 아닌 문자열
enum소스 안에서 선언한, 기반 타입이 int인 열거형
record위 타입을 프로퍼티로 가지는 데이터 타입. 아래의 선언 형식을 사용한다

상태와 결과는 다음과 같이 괄호 안의 인수로 프로퍼티를 선언하는 record로 합니다. 이 선언 형식을 positional record라고 부릅니다. 조작 입력에는 표의 기본 타입·enum·record를 지정할 수 있습니다.

public record CounterState(int Count);
public record CounterResult(int Count);
public record MoveInput(int X, int Y);

갱신 후의 상태는 new 또는 with로 만들어 Step에 전달합니다.

var next = state with { Count = state.Count + amount };
return Step.Stay(next);

record의 프로퍼티에 다른 record, 배열, 컬렉션, null을 포함할 수는 없습니다. 또한 상속, 타입 파라미터, 추가 멤버, 프로퍼티의 기본값, record struct는 대상 외입니다.

enum의 종류

용도선언 예수치 규칙
상태·입력 등의 일반 enumenum Choice { None, Small, Large }수치를 명시적으로 지정하지 않는다
TCode: Ignore의 이유enum Error { InvalidAmount = 1001 }양의 int32 값을 각 멤버에 명시. 중복 불가
TStream: 난수의 용도enum RandomSource { Drop = 1 }0 이상의 int32 값을 각 멤버에 명시. 중복 불가

오류 코드와 난수 스트림 ID는 운용 시작 후에도 의미를 바꾸지 않는 값으로 관리합니다. 하나의 룰 클래스에서 사용하는 난수용 enum은 한 종류입니다.

보상 리퀘스트 안의 SDK 데이터

보상 조립에 사용하는 SDK 오브젝트는 위의 상태·조작 입력과는 별도로 취급합니다. SDK가 선언하는 nullable, 배열, 중첩 Model을 사용할 수 있습니다. SDK 오브젝트를 TState나 조작 입력에 저장할 수는 없습니다. 자세한 내용은 GameRewards를 참조해 주세요.

지원하는 C# 구문

정의 안에서 사용할 수 있는 C# 구문은 다음과 같습니다.

분류지원 범위
선언·변수지원 타입의 로컬 변수, 대입, 상수
데이터 생성positional record의 new와 with, SDK 리퀘스트·Model·배열의 생성
분기if, 조건식, 상수·when·마지막 _를 사용하는 switch 식
반복for, while, break, continue
함수같은 클래스 안의 private 보조 함수, 이름 있는 인수, return
논리 연산bool의 부정·비교, && / `
수치 연산checked int32 / int64의 사칙연산·나머지, float / double 연산, 지원하는 수치 변환, 로컬 변수에 대한 복합 대입
수치 함수int32의 Math.Min / Math.Max
SDK의 nullable 값HasValue, Value, GetValueOrDefault, ??

일반적인 컬렉션, foreach, 중첩 record, async, 임의의 외부 API 호출은 대상 외입니다. 정수 연산은 오버플로를 검사하는 설정으로 생성합니다. 루프와 함수 호출에는 실행량의 상한이 있습니다.

1회 조작의 처리 범위

외부로부터 하나의 조작을 받으면 그 메서드를 실행합니다. Next가 반환된 경우에는 지정한 자동 처리도 이어서 실행합니다. 이 일련의 처리를 1회의 입력으로 취급합니다.

  • WaitFor 또는 Stay에 도달하면 갱신 후의 상태로 다음 입력을 기다립니다.
  • End에 도달하면 최종 상태와 결과를 보유하고 종료합니다.
  • 도중에 Ignore에 도달하면 이번 상태 변경·난수 진행·보상 요청을 취소하고, 입력 전과 같은 상태로 기다립니다. 조작 이력에도 추가하지 않습니다.

여기서의 처리 완료는 로컬 실행의 결과입니다. 서버의 검증은 별도로 Session의 Confirm으로 조작 이력을 전송하여 받습니다.

Step API

Step은 다음 동작을 지정하는 반환값입니다. 다음의 TState는 상태 타입, TResult는 결과 타입, TCode는 입력을 거부하는 이유의 enum을 가리킵니다.

API다음 동작보유하는 상태
WaitFor지정한 조작을 기다린다인수의 상태
Stay입력 전과 같은 조작의 조합을 기다린다인수의 상태
Next지정한 자동 처리를 즉시 실행한다인수의 상태를 후속 처리에 전달한다
End종료한다인수의 상태. 생략한 경우에는 현재 상태
Ignore이번 입력을 취소하고 다시 기다린다입력 전의 상태

WaitFor, Or, Next의 처리 대상은 같은 룰 클래스의 메서드를 가리키는 nameof(...)로 지정합니다. 문자열 리터럴이나 변수, Start는 지정할 수 없습니다.

WaitFor / Or

다음에 받아들일 조작을 지정합니다.

static Step WaitFor(string target, TState state);
Step Or(string target);

state를 다음 상태로 하고, target으로 지정한 public 조작의 입력을 기다립니다. 여러 조작을 받아들이는 경우에는 WaitFor(...).Or(...).Or(...)로 연결합니다.

return Step.WaitFor(nameof(Add), nextState).Or(nameof(Finish));

Or는 WaitFor 호출에 연결합니다. 같은 조작을 중복 지정할 수 없습니다.

Stay

상태를 갱신하고, 같은 조작을 계속 받아들입니다.

static Step Stay(TState state);

state를 다음 상태로 하고, 이번 입력을 받아들이기 직전과 같은 조작의 조합으로 대기합니다. 실행 중인 함수를 한 번 더 호출한다는 의미가 아닙니다.

return Step.Stay(nextState);

예를 들어 Add와 Finish를 받아들이고 있었던 경우, Add에서 Stay를 반환하면 양쪽을 계속 받아들입니다. 첫 입력 대기에 도달하기 전의 Start와 그 자동 처리에서는 사용할 수 없습니다.

Next

입력을 기다리지 않고 다음 처리를 실행합니다.

static Step Next(string target, TState state);

state를 전달하여 target으로 지정한 private 자동 처리를 즉시 실행합니다.

return Step.Next(nameof(Resolve), nextState);

호출 대상의 선언 예는 private static Step Resolve(CounterState state)입니다. 필요하다면 난수·보상 인수도 지정할 수 있습니다. public 조작, Start, 새로운 플레이어 입력을 필요로 하는 메서드는 대상으로 할 수 없습니다.

처리의 채택·취소는 1회 조작의 처리 범위를 따릅니다. Next의 순환은 생성 시에 거부합니다. 입력을 반복해서 기다리는 경우에는 WaitFor 또는 Stay를 사용합니다.

End

결과를 남기고 게임을 종료합니다.

static Step End(TResult result);
static Step End(TResult result, TState state);

결과를 보유하고 스테이트 머신을 종료합니다. 종료 후에는 새로운 조작을 받아들이지 않습니다.

인수동작
result만그 시점의 상태를 보유하고 결과를 설정한다
result, state지정한 상태를 최종 상태로 하고 결과를 설정한다
return Step.End(new CounterResult(nextState.Count), nextState);

로컬 변수 nextState를 만든 것만으로는 보유되는 상태가 갱신되지 않습니다. 갱신 후의 상태를 남기는 경우에는 두 번째 인수로 전달합니다. Start에서 즉시 종료하는 경우에도 초기 상태를 두 번째 인수로 지정합니다.

종료와 보상 요청은 독립적입니다. 보상을 요청하기 위해 End를 반환할 필요는 없습니다.

Ignore

게임 룰상 받아들일 수 없는 입력을 이유와 함께 취소합니다.

static Step Ignore(TCode code);

이번 입력을 채택하지 않고 이유를 반환합니다. code에는 오류 enum의 멤버를 직접 지정합니다. 변수로 지정할 수는 없습니다.

return Step.Ignore(CounterError.InvalidAmount);

취소하는 범위는 1회 조작의 처리 범위를 따릅니다. Start와 첫 입력 대기 이전의 자동 처리에서는 사용할 수 없습니다.

GameRewards: 보상 요청

게임 안에서 정해진 보상을 GS2 SDK의 입수 리퀘스트로 지정합니다. 조작 또는 자동 처리의 인수에 GameRewards를 추가합니다. 게임 도중에도 사용할 수 있습니다.

Grant(request)

void Grant<TRequest>(TRequest request) where TRequest : class;

request에 전달할 수 있는 것은 SDK가 AcquireAction에 등록하고 있는 입수 리퀘스트입니다. 참조 API나 소비 API의 리퀘스트는 생성 시에 거부합니다.

입수 조작은 각 서비스 트랜잭션 액션의 Acquire Action에서 찾을 수 있습니다. 예를 들어 Inventory의 입수 액션에서 입수 내용을 확인하고, 대응하는 SDK의 Request 타입을 사용합니다. 사용 중인 SDK가 그 타입에 대응하는지는 생성 시에 검사합니다.

다음 예에서는 Add로 Count를 늘릴 때마다 같은 수의 parts를 요청합니다. using은 정의 파일의 선두, Add는 룰 클래스 안에 기술합니다.

using Gs2.Gs2Inventory.Request;
using Gs2.Gs2Inventory.Model;

// Counter 클래스 안의 조작
public static Step Add(CounterState state, int amount, GameRewards rewards)
{
    if (amount < 1 || amount > 10)
        return Step.Ignore(CounterError.InvalidAmount);

    var next = state with { Count = state.Count + amount };
    rewards.Grant(new AcquireSimpleItemsByUserIdRequest()
        .WithNamespaceName("rewards")
        .WithInventoryName("bag")
        .WithAcquireCounts(new[] {
            new AcquireCount().WithItemName("parts").WithCount(amount)
        }));
    return Step.Stay(next);
}

rewards, bag, parts는 입수 대상의 리소스 이름입니다. 실제 서비스에서 사용하는 경우에는 등록된 이름을 지정합니다. 호출자는 계속 game.Add(3)으로 호출하며, GameRewards는 실행 환경이 전달합니다.

지정할 수 있는 값

SDK의 리퀘스트 타입과 Model 타입을 그대로 사용합니다.

SDK의 값취급
int? / long?각각의 정수 범위를 유지한다
float? / double?각각의 정밀도를 유지한다. NaN·Infinity는 전송 불가
bool·문자열·nullSDK의 선언에 따라 지정한다
배열·중첩 ModelSDK의 타입으로 조립한다

new, 오브젝트 이니셜라이저, With, 프로퍼티 읽기·쓰기, 배열 생성·인덱스 접근을 사용할 수 있습니다. 변수나 private 보조 함수를 사용하여 조립할 수도 있습니다.

Grant는 호출한 시점의 내용을 저장합니다. 그 후에 원래 오브젝트를 변경해도 요청한 보상은 바뀌지 않습니다. 수취인, TimeOffsetToken, DuplicationAvoider는 실행 환경이 설정하므로 정의에서는 지정하지 않습니다. 0·음수·null의 허용 여부 등은 입수 대상 API의 사양을 따릅니다.

SDK 타입을 사용하는 경우의 참조 설정은 SDK DLL 참조를 참조해 주세요. bindings.json은 필요하지 않습니다.

요청과 지급의 차이

Grant는 보상을 요청하는 처리입니다. 로컬 인벤토리를 그 자리에서 늘리는 처리가 아닙니다. Ignore가 된 입력의 요청은 취소합니다.

실제 지급은 조작 이력을 서버에 전송하고 검증을 받은 후에 이루어집니다. 로컬에서의 입력 수리, 서버 검증, 보상 반영은 별개의 단계입니다. 확인 방법은 Session의 Confirm을 참조해 주세요.

Grant(id, amount): enum에 의한 지정

void Grant<T>(T id, int amount) where T : Enum;

Inventory의 SimpleItem에 한해 보상 enum과 개수로도 지정할 수 있습니다. 예를 들어 enum Reward { Parts }를 정의하고 rewards.Grant(Reward.Parts, amount)로 호출합니다. 입수 대상은 별도로 bindings.json에 설정합니다.

이 형식의 amount는 int32입니다. 0이면 요청을 발행하지 않으며, 음수는 실행 오류입니다. SDK 리퀘스트를 전달하는 형식의 수치 범위·제약과는 다릅니다.

GameRandom: 난수

서버가 같은 결과를 재현할 수 있는 난수를 생성합니다. 조작 또는 자동 처리의 인수에 GameRandom<TStream>을 추가합니다. TStream은 난수의 용도를 나타내는 enum입니다.

RandomStream Stream(TStream stream);
int RangeInclusive(int min, int max);

Stream으로 용도를 선택하고, RangeInclusive로 양 끝을 포함하는 범위의 int32를 얻습니다. 예를 들어 RangeInclusive(2, 4)의 반환값은 2·3·4입니다. min > max는 실행 오류입니다.

다음 예에서는 Add 1회당 Count를 2~4 늘립니다. enum은 룰 클래스 바깥, Add는 클래스 안에 기술합니다.

public enum RandomSource { Count = 1 }

// Counter 클래스 안의 조작
public static Step Add(CounterState state, GameRandom<RandomSource> random)
{
    var amount = random.Stream(RandomSource.Count).RangeInclusive(2, 4);
    var next = state with { Count = state.Count + amount };
    return Step.Stay(next);
}

이 경우 호출자는 game.Add()로 호출합니다. 난수 인수는 전달하지 않습니다. 용도별로 enum 멤버를 나누고, ID는 운용 시작 후에도 의미를 바꾸지 않는 값으로 합니다. 수치 규칙은 enum의 종류를 참조해 주세요.

System.Random이나 UnityEngine.Random은 사용할 수 없습니다. 생성·재생에는 실행 환경의 시드와 사용 상황을 사용합니다. Ignore의 경우에는 공통 취소 규칙을 따릅니다.

검증과 실패 시의 동작

상황동작
지원하지 않는 구문·타입, 전이 대상의 불일치생성 시에 진단을 표시한다. 소스의 진단에는 파일명·행·열이 붙는다
게임 고유의 수리 조건 위반조작 안에서 검사하고 Step.Ignore로 이유를 반환한다
입력 타입·수치 범위·enum 후보·record 필수 필드·리비전의 불일치생성한 실행 코드에서도 검사한다. 내부 Retry로서 입력을 채택하지 않으며, 게임 고유의 Ignore 이유와 구별한다
대기 중에 받아들이지 않는 조작을 Session에서 호출한다예외
0으로 나누기 등의 실행 오류오류로 취급한다. Ignore와 달리 입력 전으로 돌아가 그대로 계속하는 것은 보장하지 않는다
생성 실패이전에 성공한 생성물을 남긴다

Ignore와 실행 오류는 복귀 방법이 다릅니다. 일반적인 입력 거부는 예외로 표현하지 말고 Ignore를 사용해 주세요.