SwiftGodotTemplateのプラグインの中身を理解しよう
概要
SwiftGodotTemplate 内の swift_godot_editor_pluginプラグインの中身がどんな作りになっているのか紐解きたい。
前提
Godot のエディタプラグインの作り方と、SwiftGodotTemplate の使い方を知っている状態。
環境
- Godot 4.4.stable
- macOS 15.6
ディレクトリ構造
res://addons/swift_godot_editor_plugin
の中身は次のとおり。
addons/swift_godot_editor_plugin
├── plugin.cfg # プラグインの設定ファイル
├── swift_godot_editor_plugin.gd # プラグインのメインスクリプト
├── swift_godot_editor_plugin.gd.uid # 自動生成
├── swift_godot_logo.svg # SwiftGodot のロゴ
├── swift_godot_logo.svg.import # 自動生成
├── swift_godot_plugin_panel.gd # UIに紐づくスクリプト
├── swift_godot_plugin_panel.gd.uid # 自動生成
└── swift_godot_plugin_panel.tscn # エディタプラグインのUI
基本ファイル
プラグインに必須なのは plugin.cfg
と メインスクリプト。
plugin.cfg
の中身
- name: プラグイン名
- description: プラグインの説明
- author: 製作者
- version: プラグインバージョン
- script: プラグインのメインスクリプト
[plugin]
name="SwiftGodot"
description="A plugin for Godot Editor to integrate SwiftGodot and recompile it"
author="Elijah Semyonov"
version="0.1"
script="swift_godot_editor_plugin.gd"
このファイルより、プラグインのメインスクリプトは swift_godot_editor_plugin.gd
であることがわかる。
swift_godot_editor_plugin.gd
の中身
中身は次のようになっている。関数にいくつかコメントを追加。
@tool
extends EditorPlugin
# @tool により、このスクリプトはエディタ上で実行される。
# EditorPlugin を継承しているので、Godot エディタの機能拡張プラグインとして動作する。
const MainPanel = preload("res://addons/swift_godot_editor_plugin/swift_godot_plugin_panel.tscn")
var main_panel_instance
# プラグインが有効化され、エディタにロードされた瞬間に呼ばれる初期化処理。
func _enter_tree() -> void:
main_panel_instance = MainPanel.instantiate()
EditorInterface.get_editor_main_screen().add_child(main_panel_instance)
_make_visible(false)
# プラグインが無効化され、エディタからアンロードされるときに呼ばれる後始末処理。
func _exit_tree() -> void:
if main_panel_instance:
main_panel_instance.queue_free()
# プラグインが「エディタの上部タブ」として表示されるかどうかを返す。
func _has_main_screen() -> bool:
return true
# プラグインのタブが選択・非選択になったときに呼ばれる。
func _make_visible(visible: bool) -> void:
if main_panel_instance:
main_panel_instance.visible = visible
# プラグインタブに表示する名前を返す。
func _get_plugin_name() -> String:
return "SwiftGodot"
# プラグインタブに表示するアイコンを返す。
func _get_plugin_icon() -> Texture2D:
return load("res://addons/swift_godot_editor_plugin/swift_godot_logo.svg")
まず、プラグインの基本の処理は次の部分
@tool
をつけ、EditorPlugin
を継承させる_enter_tree
内でプラグイン有効化後の初期化処理を行う。_exit_tree
内でプラグイン無効化後のクリーンアップ処理を行う。
@tool
extends EditorPlugin
const MainPanel = preload("res://addons/swift_godot_editor_plugin/swift_godot_plugin_panel.tscn")
var main_panel_instance
func _enter_tree() -> void:
# シーンを初期化してエディタのメインスクリーンに追加
main_panel_instance = MainPanel.instantiate()
EditorInterface.get_editor_main_screen().add_child(main_panel_instance)
_make_visible(false)
func _exit_tree() -> void:
if main_panel_instance:
main_panel_instance.queue_free()
EditorInterface.get_editor_main_screen()
は、スクリプトや2D/3Dエディタなどが配置される部分のこと。
┌───────────────────────────────────────┐
│ メニュー / ツールバー │
├───────────────────────────────────────┤
│ 2D | Script | AssetLib | SwiftGodot | ← タブバー
├───────────────────────────────────────┤
│ ← get_editor_main_screen() が返す領域 → |
│ ┌───────────────────────┐ |
│ │ 作業エリア │ |← ここに add_child() で追加
│ └───────────────────────┘ |
├───────────────────────────────────────┤
│ その他のドック / インスペクタ等 │
└───────────────────────────────────────┘
例えば、この EditorInterface.get_editor_main_screen()
の代わりにadd_control_to_dock(DOCK_SLOT_LEFT_UL, main_panel_instance)
を追加してみる。
EditorInterface.get_editor_main_screen()
の場合
add_control_to_dock(DOCK_SLOT_LEFT_UL, main_panel_instance)
の場合はドックでの配置になる。
次に下部の処理を見てみる。これらは、EditorInterface.get_editor_main_screen()
に追加するために必要な関数群である。
# プラグインが「エディタの上部タブ」として表示されるかどうかを返す。
func _has_main_screen() -> bool:
return true
# プラグインのタブが選択・非選択になったときに呼ばれる。
func _make_visible(visible: bool) -> void:
if main_panel_instance:
main_panel_instance.visible = visible
# プラグインタブに表示する名前を返す。
func _get_plugin_name() -> String:
return "SwiftGodot"
# プラグインタブに表示するアイコンを返す。
func _get_plugin_icon() -> Texture2D:
return load("res://addons/swift_godot_editor_plugin/swift_godot_logo.svg")
これらの設定のおかげで、先ほどのスクリーンショットのようにアイコン付きで上部のタブバーに表示されている。
_get_plugin_name
や _get_plugin_icon()
の設定はあくまでタブバーで利用する場合で機能し、ドックに配置される場合はこれらは使われない。
シーンファイル
MainPanel として設定されている swift_godot_plugin_panel.tscn
がシーンファイル。
const MainPanel = preload("res://addons/swift_godot_editor_plugin/swift_godot_plugin_panel.tscn")
シーンファイルを開いてみる。
このルートノードは MarginContainer
になっている。
このルートノードの名前を変えて、プラグインをリロードする。
するとタブの名前が SwiftGodot
となっている。つまり、ルートノードの名前がタブの名前になることがわかる。
Godot の UI構築のノードツリーはかなり好き。
シーンファイルに紐づくスクリプトファイル
先ほどのシーンに紐づくスクリプト。swift_godot_plugin_panel.gd
このファイルが最後のファイルとなる。つまり、ここに SwiftGodot の Rebuildなどの処理が含まれている。
スクリプト全体を見てみる。
@tool
extends MarginContainer
const MARGIN = 16
@onready var rebuild_button := $VBoxContainer/ButtonsContainer/RebuildButton
@onready var clean_build_check_button := $VBoxContainer/ButtonsContainer/CleanBuildCheckButton
@onready var log := $VBoxContainer/Log
@onready var swift_path = ProjectSettings.globalize_path("res://swift_godot_game")
const PROGRESS_MARKERS := ["˥", "˦", "˧", "˨", "˩", "˨", "˧", "˦"]
signal state_changed(working: bool)
func _ready() -> void:
rebuild_button.pressed.connect(recompile_swift)
var on_state_changed = func(is_working: bool):
rebuild_button.disabled = is_working
clean_build_check_button.disabled = is_working
state_changed.connect(on_state_changed)
func append_log(string: String) -> void:
log.append_text(string + "\n")
func wait_process_finished(pid: int, progress_text: String):
var start_time = Time.get_ticks_msec()
var i = 0
var initial_log_text = log.get_parsed_text()
while OS.is_process_running(pid):
await get_tree().create_timer(0.1).timeout
var time_passed = (Time.get_ticks_msec() - start_time) / 1000
log.text = "%s%s %s %s s " % [initial_log_text, progress_text, PROGRESS_MARKERS[i], time_passed]
i = (i + 1) % PROGRESS_MARKERS.size()
log.text = initial_log_text
func recompile_swift() -> void:
state_changed.emit(true)
log.clear()
if OS.is_sandboxed():
state_changed.emit(false)
append_log("Impossible to launch OS processed. Sandboxed :()")
return
var target_dir = ProjectSettings.globalize_path("res://addons/swift_godot_extension/bin")
if not DirAccess.dir_exists_absolute(target_dir):
var err = DirAccess.make_dir_recursive_absolute(target_dir)
if err != OK:
append_log("Error creating directory '" + target_dir + "'")
state_changed.emit(false)
return
append_log("Building from the scratch. Can take some time. Consequent builds will be much faster")
var i = 0
if clean_build_check_button.button_pressed:
append_log("Running `swift package clean`")
var pid := OS.create_process(
"swift", [
"package", "clean",
"--package-path", swift_path,
"--build-path", target_dir
], false
)
if pid == -1:
append_log("Couldn't execute `swift package clean` command")
state_changed.emit(false)
return
await wait_process_finished(pid, "Cleaning")
append_log("Cleaned")
var gdignore_path: String = target_dir.path_join(".gdignore")
append_log("Writing %s" % gdignore_path)
var file = FileAccess.open(gdignore_path, FileAccess.WRITE)
if file == null:
var open_error = FileAccess.get_open_error()
append_log("Error creating/opening '" + gdignore_path + "'")
state_changed.emit(false)
return
else:
file.close()
append_log("Building into %s" % target_dir)
var pid := OS.create_process(
"swift", [
"build",
"--package-path", swift_path,
"--build-path", target_dir
], false)
if pid == -1:
append_log("Couldn't execute `swift build` command")
state_changed.emit(false)
return
append_log("Running `swift build`")
await wait_process_finished(pid, "Building")
append_log("Done! Restarting editor")
await get_tree().create_timer(1.0).timeout
EditorInterface.restart_editor(false)
関数は4つ。プラグイン内のスクリプトなので @tool
をつけている。
func _ready() -> void
- シグナルのセットアップ
func append_log(string: String) -> void
- ログを表示するための関数 (UI)
func wait_process_finished(pid: int, progress_text: String)
- 外部プロセスの終了を待ちながら進行状況を表示(UI)
func recompile_swift() -> void
- Rebuild ボタンを押した時に呼び出す関数
シグナルは1つ。このシグナルを使って、ボタンの有効化/無効化を設定している。
signal state_changed(working: bool)
- ビルド状態の変化を通知するシグナル。
Rebuild ボタンを押したら recompile_swift()
が呼ばれて、swift_path
に設定された Swift Package のビルドを始める。
recompile_swift
では次の処理を順次行っている。
- サンドボックス環境のチェック
- 出力先ディレクトリの確認と作成
- 必要に応じてクリーンビルドを実行
- .gdignore ファイルを生成(Godotにビルド対象外として認識させる)
- Swiftビルド実行
- ビルド完了後、エディタ再起動
swift package clean
や swift build
などのコマンドは OS.create_process から行われている。
EditorInterface.restart_editor(false)
でエディタをリスタートさせる。
ディレクトリ操作では次の関数が使われている。
DirAccess.dir_exists_absolute(path: String) -> bool
- 指定した絶対パスにディレクトリが存在するか確認
DirAccess.make_dir_recursive_absolute(path: String) -> int
- 指定した絶対パスにディレクトリを再帰的に作成(親ディレクトリがない場合も作成)
ファイル操作では次の関数が使われている。
FileAccess.open(path: String, mode: int) -> FileAccess
- 指定パスのファイルを指定モード(例:
FileAccess.WRITE
)で開く
- 指定パスのファイルを指定モード(例:
FileAccess.get_open_error() -> int
open()
が失敗した場合のエラーコードを取得
file.close()
- 開いたファイルを閉じる(
FileAccess.open()
の戻り値インスタンスで呼び出す)
- 開いたファイルを閉じる(
パスの操作
ProjectSettings.globalize_path(path: String) -> String
- Godot の
res://
などの仮想パスを OS の絶対パスに変換
- Godot の
String.path_join(subpath: String) -> String
- ファイルパスを結合し、OS依存の区切り文字で返す
おわりに
とてもシンプルな設計であることがわかった。
また外部にshellスクリプトなどを配置して叩くのではなく、OS.create_process
を使う方式でやるのが良さそう。
