🏃

[Godot] ターミナルで GDScript を実行する。

に公開

きっかけ

テストを実行したり、ファイルを生成したり、をコマンドラインでできたらうれしいな。
と思って、とりあえずターミナルで GDScript を実行する方法を調べたら、

公式ドキュメント に、以下のようにしたらいいよと書いてありました。

sayhello.gd
#!/usr/bin/env -S godot -s
extends SceneTree

func _init():
    print("Hello!")
    quit()
$ chmod +x sayhello.gd
$ ./sayhello.gd

確かに実行できた!

で、めでたしめでたし。
とはならなかったので、書きます。

先に結論を書くと、GDScript を直接呼ばずにシーンを実行するシェルスクリプトを書きました

前提: godot コマンドへの PATH を通す

macOS の場合

brew 経由で入れる方法もあるみたいですが、自分はシンボリックリンクを張りました。

$ ln -sf /Applications/Godot.app/Contents/MacOS/Godot /usr/local/bin/godot

Windows (WSL) の場合

自分は D:\Apps\Godot_v4.5-stable_win64.exe に保存したので、それを呼ぶスクリプトを用意しました。

~/.local/bin/godot
#!/bin/sh

/mnt/d/Apps/Godot_v4.5-stable_win64.exe/Godot_v4.5-stable_win64_console.exe $@

GDScript の直接実行でうまくいかなかったこと

  1. Godot エディタ上で保存すると、chmod +x で変えたパーミッションが元に戻る。
  2. 標準出力にいろいろ出てくる。
  3. autoloads で設定した変数が読み込まれない。

1. Godot エディタ上で保存すると、chmod +x で変えたパーミッションが元に戻る。

よし、動くの確認したし、やりたいこと書いてくぞ。
と Godot エディタ上でスクリプトを修正すると以下のエラー。
(※macOSの場合。Windows(WSL)だとファイルシステムの違いでパーミッションは常に777なので無関係)

$ ./sayhello.gd
zsh: permission denied: ./sayhello.gd

そういう仕様? Issue は無さそう。(とりあえず Issue に上げました。 一瞬、自己解決したかと思って思わずクローズしてしまうミスをしました。慣れないことをした。 )

まあ、直接実行せずにシェルスクリプト経由で実行すればいいだけなので、あまり気にせず進めました。

#!/bin/sh
godot -s sayhello.gd

2. 標準出力にいろいろ出てくる。

$ ./sayhello.gd
Godot Engine v4.5.stable.official.876b29033 - https://godotengine.org
Metal 3.2 - Forward+ - Using Device #0: Apple - Apple M1 (Apple7)

Hello!
(略)

埋もれる。。。
--quiet オプションを渡すと静かになるよ。

$ godot --quiet -s ./sayhello.gd

と書いてありましたが標準出力に何も出なくなる(一応printerrなら出てくるが、それを使うのも違う)。

単純に、自分が書いたもの以外を消したいが素直にはできなそう。

3. autoloads で設定した変数が読み込まれない。

例えば、以下のようなファイルをプロジェクト設定からグローバル(autoloads)に設定します。

foo_bar.gd
extends Node
#class_name FooBar

static func do_something() -> void:
    print_debug('FooBar do_something')

func _ready() -> void:
    print_debug('FooBar ready')

これを実行するスクリプトを書きます。

xxx.gd
#!/usr/bin/env -S godot -s
extends SceneTree

func _init():
    print("Hello!")
    FooBar.do_something()
    quit()

問題なさそうですが、実行するとエラーになります。

$ godot -s ./xxx.gd
SCRIPT ERROR: Compile Error: Identifier not found: FooBar

ただし、FooBar.do_something() のところをコメントアウトし、以下のように書き換えると、_ready() は呼ばれてました。

xxx.gd
#!/usr/bin/env -S godot -s
extends SceneTree

func _init():
    print("Hello!")
    #FooBar.do_something()
    quit()
$ godot -s ./xxx.gd
Hello!
FooBar ready

どうも実行したスクリプトが autoloads よりも先に実行される? っぽい挙動でした。

GDScript を直接実行せず、シーンを実行する

$ godot --headless xxx.tscn

でシーンを実行できるので、先ほどの GDScript をアタッチしただけの Node のシーンを作成して試してみましたが、autoloads で設定した変数も参照エラーは出ずに呼べました。

なので、シーンを実行するように書き換えたのですが、
今度はコマンドライン引数が受け取れなくなりました。(悲しい)

GDScript を直接 $ godot -s ./xxx.gd で実行していた際は、OS.get_cmdline_args() でコマンドライン引数を受け取れていましたが、シーンだとだめっぽい? しょうがないので環境変数で渡すようにしました。

できたもの

3つファイルを用意します。

dev
├── runner
├── runner.gd
└── runner.tscn

dev/runner

シェルスクリプトを用意します。

/dev/runner
#!/bin/sh

GODOT_RUNNER="$@" godot --headless ./dev/runner.tscn \
  | grep GODOT_RUNNER_LOG \
  | sed 's/^GODOT_RUNNER_LOG | //'

コマンドライン引数は渡せないので、環境変数 GODOT_RUNNER に突っ込むようにしました。
また、標準出力に出したいものは、GODOT_RUNNER_LOG | xxx という形式のものを grep することにしました。

dev/runner.gd

dev/runner.gd
extends Node
class_name Runner

static func log(txt) -> void:
    print("GODOT_RUNNER_LOG | %s" % txt)

# = Runner
#
# 	$ ./dev/runner do_something
#
# will execute:
#
# 	$ GODOT_RUNNER='do_something' godot --headless ./dev/runner.tscn
#
func _ready() -> void:
    var env = OS.get_environment("GODOT_RUNNER")
    if not env: printerr("GODOT_RUNNER is not set.")
    var args = env.split(' ')
    match args[0]:
        'do_something': FooBar.do_something()
    get_tree().quit()

ログに出したいものは

Runner.log("Something happened.")

という形で明示的に書くようにしました。

dev/runner.tscn

単純に Node のシーンに上記のスクリプトをアタッチしているだけです。

dev/runner.tscn
[gd_scene load_steps=2 format=3 uid="uid://bm57ljnlmq0hi"]

[ext_resource type="Script" uid="uid://beuy8y175u38h" path="res://dev/runner.gd" id="1_6yldl"]

[node name="Runner" type="Node"]
script = ExtResource("1_6yldl")

使い方

上記3ファイルを用意することで、

$ ./dev/runner do_something

という形で、GDScript を実行できるようになりました。 やったー。

以上です。

Discussion