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)을 사용합니다. 따라서 에디터의 자동 완성과 정적 형식 검사가 동작합니다.

예

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() -> Gs2StepStart
핸들러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(...). 값을 반환하거나 -> voidprivate 메서드
상수const NAME := 3const int

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

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

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

Step

GDScriptC#의미
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_inputenum[RoomInput]
## @input_range(-1, 1)입력의 멤버[InputRange]
## @room_actionenum[RoomAction]
## @room_timerenum[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를 전달할 때는 _rawGs2Room.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_floatRoom.Get / Room.Set
표준 필드get_standard / set_standard(id, Gs2RoomSync.StandardField.ROTATION_Y, v)StandardField
2Dspawn(S, x, y), raycast_2d, overlap_2dz를 생략한 형태

그 밖의 함수(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의 이름으로 변환됩니다. 규칙에 맞지 않는 이름은 오류가 됩니다.

GDScriptGSL에서의 이름
start, on_tick, _moveStart, Tick, Move
인수와 로컬 변수 object_id(_object_id)objectId
레코드의 필드 box_idBoxId
enum의 멤버 LAST_TICKLastTick
클래스와 enum그대로(PascalCase)

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

작성할 수 있는 구문

  • 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")을 작성해 주세요.

생성과 등록

컴파일러 준비하기

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/build

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

파일용도
<스키마 이름>.realtimeschemaGs2NetworkEntity.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과, 원본 파일의 위치를 나타내는 주석을 제외하고 동일합니다.