最終確認日

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 の中身

中身は次のようになっている。関数にいくつかコメントを追加。

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() の場合

SwiftGodotTemplateの中身を理解しよう-1755089827369

add_control_to_dock(DOCK_SLOT_LEFT_UL, main_panel_instance) の場合はドックでの配置になる。

SwiftGodotTemplateの中身を理解しよう-1755089836096

次に下部の処理を見てみる。これらは、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 がシーンファイル。

swift_godot_editor_plugin.gd
const MainPanel = preload("res://addons/swift_godot_editor_plugin/swift_godot_plugin_panel.tscn")

シーンファイルを開いてみる。

SwiftGodotTemplateの中身を理解しよう-1755155659563

このルートノードは MarginContainerになっている。 このルートノードの名前を変えて、プラグインをリロードする。

するとタブの名前が SwiftGodot となっている。つまり、ルートノードの名前がタブの名前になることがわかる。

SwiftGodotTemplateの中身を理解しよう-1755155815073

Godot の UI構築のノードツリーはかなり好き。

シーンファイルに紐づくスクリプトファイル

先ほどのシーンに紐づくスクリプト。swift_godot_plugin_panel.gd

このファイルが最後のファイルとなる。つまり、ここに SwiftGodot の Rebuildなどの処理が含まれている。

スクリプト全体を見てみる。

swift_godot_plugin_panel.gd
@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 では次の処理を順次行っている。

  1. サンドボックス環境のチェック
  2. 出力先ディレクトリの確認と作成
  3. 必要に応じてクリーンビルドを実行
  4. .gdignore ファイルを生成(Godotにビルド対象外として認識させる)
  5. Swiftビルド実行
  6. ビルド完了後、エディタ再起動

swift package cleanswift 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 の絶対パスに変換
  • String.path_join(subpath: String) -> String
    • ファイルパスを結合し、OS依存の区切り文字で返す

おわりに

とてもシンプルな設計であることがわかった。

また外部にshellスクリプトなどを配置して叩くのではなく、OS.create_process を使う方式でやるのが良さそう。

サイトアイコン
公開日
更新日