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


# GDScript로 룰 작성하기

stateMachine 룸의 룰을 GDScript로 작성하고, Godot에서 생성·등록하는 방법



`stateMachine` 룸의 룰은 C# 외에 GDScript로도 작성할 수 있습니다. Godot만으로 게임을 만드는 경우에는 C# 환경을 준비하지 않아도 룰을 작성할 수 있습니다.

GDScript 룰은 Godot에서 실행되지 않습니다. GS2의 리얼타임 룰 컴파일러(`gs2-realtime-compiler`)가 C# 룰과 같은 GSL과, Godot에서 사용하는 클래스로 변환합니다. 따라서 할 수 있는 일과 실행 규칙은 C# 룰과 같습니다. Room API의 의미, 실행 규칙, 상한값은 [룸 정의 레퍼런스]()를 참조해 주세요. 이 페이지에서는 GDScript에서의 작성 방법과 생성·등록 절차를 설명합니다.

룰 스크립트에서는 SDK의 스텁(`Gs2RoomRules`, `Gs2Record`, `Gs2Step`, `Gs2Room`, `Gs2RoomSync`)을 사용합니다. 따라서 에디터의 자동 완성과 정적 형식 검사가 동작합니다.

## 예 {#example}

```gdscript
class_name TickRoom
extends Gs2RoomRules

const STATE := TickState
const RESULT := TickResult
const ERROR := TickError


class TickState extends Gs2Record:
	var steps: int


class TickResult extends Gs2Record:
	var finished: bool


enum TickError { INVALID_OPERATION = 5 }

## @room_schema(5)
enum Character { X = 0, Y = 1, LAST_TICK = 2 }

## @room_input
enum TickInput {
	## @input_range(-1, 1)
	MOVE_X = 0,
}


static func start() -> Gs2Step:
	Gs2Room.define_tick_rate(30)
	Gs2Room.define_simulated_prefab(Character)
	return Gs2Step.wait_for(on_tick, TickState.new({steps = 0}))


static func on_tick(state: TickState, tick: int) -> Gs2Step:
	for i in range(Gs2Room.object_count(Character)):
		_move(Gs2Room.object_at(Character, i), tick)
	return Gs2Step.stay(state.with({steps = state.steps + 1}))


## @predict(Character)
static func _move(id: int, step: int) -> void:
	Gs2Room.set_field(id, Character.X, Gs2RoomMath.clamp(Gs2Room.get_field(id, Character.X) + Gs2Room.input(id, TickInput.MOVE_X) * 10, -1000, 1000))
	Gs2Room.set_field(id, Character.LAST_TICK, step)
```

## 룰의 구성 {#structure}

| 요소 | 작성 방법 | C#에서의 대응 |
| --- | --- | --- |
| 룰 | `class_name <이름>`과 `extends Gs2RoomRules`. 하나의 스크립트에 하나의 룰 | 룰 클래스 |
| 상태·결과·오류 | `const STATE := <레코드>`, `const RESULT := <레코드>`, `const ERROR := <enum>` | `Step`의 형식 인수 |
| 레코드 | `class <이름> extends Gs2Record:`에 형식이 지정된 필드만 작성한다. `<이름>.new({field = value, ...})`로 모든 필드를 지정하여 만들고, `record.with({field = value})`로 복사한다 | record |
| 시작 | `static func start() -> Gs2Step` | `Start` |
| 핸들러 | `on_interact(state, object_id: int, method: int)`, `on_timer(state, object_id: int, event_id: int)`, `on_tick(state, tick: int)`, `on_spawned(state, object_id: int)` | `Interact`·`Timer`·`Tick`·`Spawned` |
| 헬퍼 | `static func _name(...)`. 값을 반환하거나 `-> void` | private 메서드 |
| 상수 | `const NAME := 3` | `const int` |

함수는 모두 `static`으로 하고, 인수와 반환값에 형식을 붙입니다. 핸들러의 인수 이름은 룸이 전달하는 이름이므로 바꿀 수 없습니다(사용하지 않는 인수는 `_object_id`처럼 `_`를 붙일 수 있습니다).

사용할 수 있는 형식은 `int`(룸에서는 int32로 계산하며, 범위를 넘으면 그 처리 전체가 실패한다), `bool`, `String`, 레코드, enum입니다. `float`는 float 필드에 쓰는 상수(`Gs2Room.set_float`)와 태그의 인수에만 사용할 수 있습니다.

오류 enum에 `Error`라는 이름은 사용할 수 없습니다(GDScript의 내장 `Error`와 겹칩니다). `TickError`처럼 룰별 이름을 붙여 주세요.

## Step {#step}

| GDScript | C# | 의미 |
| --- | --- | --- |
| `Gs2Step.wait_for(on_tick, state)` | `Step.WaitFor(nameof(Tick), state)` | 새 태스크에서 핸들러의 이벤트를 기다린다 |
| `.or_wait_for(on_timer)` | `.Or(nameof(Timer))` | 동시에 다른 핸들러도 기다린다 |
| `Gs2Step.next(_claim, state)` | `Step.Next` | 같은 이벤트 안에서 이어서 함수를 실행한다 |
| `Gs2Step.stay(state)` | `Step.Stay(state)` | 현재 태스크 그대로 상태를 갱신한다 |
| `Gs2Step.end(result[, state])` | `Step.End` | 룰을 종료한다 |
| `Gs2Step.ignore(TickError.MEMBER)` | `Step.Ignore` | 이벤트를 거부한다(수행한 변경은 모두 취소된다) |

핸들러는 이름 문자열이 아니라 함수 자체(`on_tick`)로 지정합니다.

## 태그 {#tags}

C#의 속성 대신 `##` 주석에 태그를 한 줄에 하나씩 작성합니다. 알 수 없는 태그나 놓는 위치가 잘못된 태그는 오류가 됩니다.

| 태그 | 작성 위치 | C#에서의 대응 |
| --- | --- | --- |
| `## @room_schema(7, standard=POSITION, position_precision=0.5, rotation_precision=...)` | enum | `[RoomSchema]`. `standard=`는 `POSITION` / `ROTATION` / `TRANSFORM` |
| `## @field(int\|bool\|float, precision=0.5, default=1.5)` | 스키마의 멤버 | `[RoomField]` |
| `## @room_input` | enum | `[RoomInput]` |
| `## @input_range(-1, 1)` | 입력의 멤버 | `[InputRange]` |
| `## @room_action` | enum | `[RoomAction]` |
| `## @room_timer` | enum | `[RoomTimer]` |
| `## @predict(Character)` | 함수 | `[Predict]`. `static func _name(id: int[, tick: int]) -> void` |

## Gs2Room {#room-api}

C#의 `Room`과 같은 API를 snake_case 이름으로 사용할 수 있습니다. GDScript에는 제네릭과 오버로드가 없으므로 다음과 같이 구분하여 작성합니다.

| 작성 방법 | 예 | C# |
| --- | --- | --- |
| 스키마를 첫 번째 인수로 전달한다 | `Gs2Room.define_simulated_prefab(Character)`, `Gs2Room.spawn(Crate, x, y)` | `Room.DefineSimulatedPrefab<Character>()`, `Room.Spawn<Crate>(x, y)` |
| 숫자 Prefab ID를 전달할 때는 `_raw` | `Gs2Room.define_server_prefab_raw(30, 1991)`, `Gs2Room.spawn_raw(30, 0, 0, 0)` | `Room.DefineServerPrefab(30, ...)` |
| 필드 읽기·쓰기 | `get_field` / `set_field`, bool은 `get_bool` / `set_bool`, float 상수는 `set_float` | `Room.Get` / `Room.Set` |
| 표준 필드 | `get_standard` / `set_standard(id, Gs2RoomSync.StandardField.ROTATION_Y, v)` | `StandardField` |
| 2D | `spawn(S, x, y)`, `raycast_2d`, `overlap_2d` | z를 생략한 형태 |

그 밖의 함수(`input` / `input_clamped`, `object_count` / `object_at`, `is_prefab` / `prefab_of` / `owner_of` / `exists` / `find_owned`, `despawn`, `after` / `cancel_timer`, `sender_connection_id` / `sender_user_id`, `define_hitbox` / `define_relevance` 등)는 C#과 같은 의미입니다. 정수 계산에는 `Gs2RoomMath`(`clamp`·`abs`·`sign`·`sqrt`·`length2`·`length3`·`mul_div`, 상수 `INT32_MIN` / `INT32_MAX`)와 `mini` / `maxi`를 사용할 수 있습니다.

## 이름의 대응 {#naming}

GDScript의 이름은 정해진 규칙에 따라 GSL의 이름으로 변환됩니다. 규칙에 맞지 않는 이름은 오류가 됩니다.

| GDScript | GSL에서의 이름 |
| --- | --- |
| `start`, `on_tick`, `_move` | `Start`, `Tick`, `Move` |
| 인수와 로컬 변수 `object_id`(`_object_id`) | `objectId` |
| 레코드의 필드 `box_id` | `BoxId` |
| enum의 멤버 `LAST_TICK` | `LastTick` |
| 클래스와 enum | 그대로(PascalCase) |

오류 메시지는 GDScript의 작성 방법과 이름으로 표시됩니다.

## 작성할 수 있는 구문 {#syntax}

- `var x: int = ...` / `var x := ...`, `const`
- `if` / `elif` / `else`, `while`, `for i in range(...)`(끝 값은 한 번만 평가된다)
- `match`(상수 패턴과 `_`)
- `return`, `pass`, `break`, `continue`
- 산술(`+ - * / %`), 비교, `and` / `or` / `not`, `a if c else b`
- `@warning_ignore("...")`

배열, 딕셔너리(레코드의 `new` / `with` 인수 제외), 첨자, `self`, `await`, 람다, 시그널, `in` / `is` / `as`, 비트 연산, `**`, float 계산은 작성할 수 없습니다. 작성한 경우에는 위치가 포함된 오류가 됩니다.

정수 나눗셈에는 GDScript의 `integer_division` 경고가 나옵니다. 룰 디렉터리에서 이 경고를 비활성화하거나, 문 앞에 `@warning_ignore("integer_division")`을 작성해 주세요.

## 생성과 등록 {#deployment}

### 컴파일러 준비하기

SDK와 같은 릴리스에서 OS에 맞는 `gs2-realtime-compiler`를 받고([받는 방법]()), Godot 에디터 설정의 `gs2/realtime/compiler_path`에 그 위치를 설정합니다.

### 생성하기

**Project > Tools > GS2 > Build Realtime Rules**를 실행합니다. 룰이 하나라면 다음 프로젝트 설정을 사용합니다. 기본 위치로 괜찮다면 설정은 필요하지 않습니다.

| 설정 | 의미 | 기본값 |
| --- | --- | --- |
| `gs2/realtime/rules_directory` | 룰 스크립트를 두는 위치 | `res://rules` |
| `gs2/realtime/generated_directory` | 게임이 사용하는 클래스의 출력 위치 | `res://generated/realtime` |
| `gs2/realtime/package_directory` | 등록할 `package.zip`의 출력 위치 | `res://../build/realtime` |

하나의 위치에 둘 수 있는 룰 스크립트는 하나입니다. 룰이 여러 개인 경우에는 룰별로 위치를 나누고, 프로젝트 설정의 `gs2/realtime/rule_sets`에 `{"rules": ..., "generated": ..., "package": ...}` 조합을 나열합니다. `rule_sets`가 비어 있지 않으면 위의 3가지 설정은 사용하지 않습니다.

명령줄에서 생성할 수도 있습니다.

```sh
gs2-realtime-compiler --build path/to/rules --output path/to/build
```

`generated_directory`에는 다음 클래스가 출력됩니다. 더 이상 생성되지 않는 파일은 삭제되고, 변경되지 않은 파일의 `.uid`는 남으므로 씬에서의 참조는 깨지지 않습니다.

| 파일 | 용도 |
| --- | --- |
| `<스키마 이름>.realtimeschema` | `Gs2NetworkEntity.schema`에 지정하는 동기화 스키마 |
| `<스키마 이름>_schema.gd`(`class_name <스키마 이름>Schema`) | 필드의 슬롯과 형식 지정 접근자 |
| 입력·액션 enum별 스크립트 | `world.set_input(TickInput.MOVE_X, 1)`, `door.interact(DoorAction.OPEN)`처럼 사용하는 상수 |
| 오류 enum의 스크립트 | 거부 결과 코드의 상수 |
| `<룰 이름>_prediction.gd` | 예측 코드. `<룰 이름>Prediction.register(world)`로 활성화한다 |

생성되는 클래스는 `class_name`을 가지며, `class_name`은 프로젝트 전체에서 공통입니다. 다른 룰의 생성물이나 프로젝트의 다른 스크립트와 이름이 겹치면, 그 룰은 출력되지 않고 오류가 됩니다(대소문자만 다른 이름도 겹친 것으로 간주합니다). 예를 들어 2개의 룰 모두에 `enum PlayerState`가 있으면 둘 다 `PlayerStateSchema`를 생성하므로, 어느 한쪽의 이름을 바꿔 주세요.

룰 스크립트는 게임 실행에는 사용하지 않습니다. 프로젝트 안에 두면 에디터의 형식 검사가 동작하지만, 내보내기 필터로 제외해 주세요.

### 등록하기

등록 절차는 C# 룰과 같습니다. `package.zip` 안의 `main.gsl`을 GS2-StateMachine의 [UpdateStateMachineMaster](#updatestatemachinemaster)의 `payload`에 지정하고, `mainStateMachineName`에는 룰의 `class_name`을 지정합니다. Realtime 네임스페이스 설정은 [룸 정의 레퍼런스의 생성과 등록](#deployment)을 참조해 주세요.

GDScript에서 생성한 GSL은 같은 내용의 C# 룰에서 생성한 GSL과, 원본 파일의 위치를 나타내는 주석을 제외하고 동일합니다.



