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。1 つのスクリプトに 1 つのルールルールのクラス
状態・結果・エラー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# の属性の代わりに、## のコメントにタグを 1 行に 1 つずつ書きます。知らないタグや、置く場所を間違えたタグはエラーになります。

タグ書く場所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(...)(終わりの値は 1 回だけ評価される)
  • 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 にその場所を設定します。

生成する

プロジェクト > ツール > GS2 > Build Realtime Rules を実行します。ルールが 1 つなら、次のプロジェクト設定を使います。既定の場所でよければ、設定は要りません。

設定意味既定
gs2/realtime/rules_directoryルールのスクリプトを置く場所res://rules
gs2/realtime/generated_directoryゲームが使うクラスの出力先res://generated/realtime
gs2/realtime/package_directory登録する package.zip の出力先res://../build/realtime

1 つの場所に置けるルールのスクリプトは 1 つです。ルールが複数ある場合は、ルールごとに場所を分け、プロジェクト設定の 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 と、元のファイルの位置を示すコメントを除いて同じです。