GDScript로 룰 작성하기
stateMachine 룸의 룰은 C# 외에 GDScript로도 작성할 수 있습니다. Godot만으로 게임을 만드는 경우에는 C# 환경을 준비하지 않아도 룰을 작성할 수 있습니다.
GDScript 룰은 Godot에서 실행되지 않습니다. GS2의 리얼타임 룰 컴파일러(gs2-realtime-compiler)가 C# 룰과 같은 GSL과, Godot에서 사용하는 클래스로 변환합니다. 따라서 할 수 있는 일과 실행 규칙은 C# 룰과 같습니다. Room API의 의미, 실행 규칙, 상한값은 룸 정의 레퍼런스를 참조해 주세요. 이 페이지에서는 GDScript에서의 작성 방법과 생성·등록 절차를 설명합니다.
룰 스크립트에서는 SDK의 스텁(Gs2RoomRules, Gs2Record, Gs2Step, Gs2Room, Gs2RoomSync)을 사용합니다. 따라서 에디터의 자동 완성과 정적 형식 검사가 동작합니다.
예
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)룰의 구성
| 요소 | 작성 방법 | 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
| 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)로 지정합니다.
태그
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
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를 사용할 수 있습니다.
이름의 대응
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의 작성 방법과 이름으로 표시됩니다.
작성할 수 있는 구문
var x: int = .../var x := ...,constif/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")을 작성해 주세요.
생성과 등록
컴파일러 준비하기
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가지 설정은 사용하지 않습니다.
명령줄에서 생성할 수도 있습니다.
gs2-realtime-compiler --build path/to/rules --output path/to/buildgenerated_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의 payload에 지정하고, mainStateMachineName에는 룰의 class_name을 지정합니다. Realtime 네임스페이스 설정은 룸 정의 레퍼런스의 생성과 등록을 참조해 주세요.
GDScript에서 생성한 GSL은 같은 내용의 C# 룰에서 생성한 GSL과, 원본 파일의 위치를 나타내는 주석을 제외하고 동일합니다.