【Godot】EditorPluginでGodotエディタを拡張する(@tool・カスタムDock・インスペクタ)

作成: 2026-02-08

Godotの@toolアノテーションとEditorPluginクラスを使って、カスタムDock・ツールバーボタン・インスペクタプラグインを追加する方法。plugin.cfgの設定からベストプラクティスまで実践的に解説します。

概要

動作確認環境: Godot 4.3+

Godotエディタは拡張性が高く、EditorPluginクラスを使えばカスタムDockやインスペクタの追加が可能です。しかし、@toolアノテーションの挙動やプラグインのライフサイクルを正しく理解していないと、エディタがクラッシュしたり想定外の動作を起こします。

この記事では、プラグインの基本構成からカスタムDock・インスペクタプラグインの実装まで、実務で使える手順を解説します。

@toolアノテーションの基本

Godotでエディタ拡張を実装する際、最初に理解すべきなのが @tool アノテーションです。たとえば「エディタ上でAttack Rangeの範囲を円で描画して確認したい」といった場面で活躍します。

@tool をスクリプト先頭に記述すると、そのスクリプトはエディタ内でも実行されるようになります。ただし、エディタとゲーム実行時で処理を分けることが重要です。

@tool
extends Node2D

func _process(delta):
    if Engine.is_editor_hint():
        # エディタ内でのみ実行される処理
        queue_redraw()
    else:
        # ゲーム実行時の処理
        move_character(delta)

重要: Engine.is_editor_hint() で分岐しないと、エディタ内でもゲームロジックが動作してしまいます。

次に、@toolのsetterと_draw()を組み合わせた実践的な例を見てみましょう。インスペクタでradiusを変更すると、エディタ上の円がリアルタイムで更新されます。

@tool の主な用途:

用途説明
カスタム描画_draw()でエディタ上にガイド線やプレビューを表示
プロパティ連動@export変更時にリアルタイムでプレビュー更新
EditorPluginプラグイン本体スクリプトに必須
@tool
extends Sprite2D

@export var radius: float = 100.0:
    set(value):
        radius = value
        queue_redraw()  # 値変更時に再描画

func _draw():
    if Engine.is_editor_hint():
        draw_circle(Vector2.ZERO, radius, Color(0, 1, 0, 0.3))

plugin.cfgとEditorPluginの構成

@toolの基本がわかったら、いよいよプラグインの作成に進みましょう。プラグインを作成するには、決められたディレクトリ構造に従ってファイルを配置する必要があります。Godotはこの構造を自動検出してプラグイン設定画面に表示してくれます。

ディレクトリ構成

addons/
└── my_plugin/
    ├── plugin.cfg          # プラグイン設定ファイル
    ├── my_plugin.gd        # EditorPluginスクリプト
    └── dock/
        └── my_dock.tscn    # カスタムDockシーン(任意)

plugin.cfg

[plugin]

name="My Plugin"
description="カスタムDockを追加するプラグイン"
author="Your Name"
version="1.0.0"
script="my_plugin.gd"

EditorPluginの基本

@tool
extends EditorPlugin

func _enter_tree():
    # プラグイン有効化時に呼ばれる
    print("Plugin activated")

func _exit_tree():
    # プラグイン無効化時に呼ばれる
    # ここで追加したUI要素を必ず削除する
    print("Plugin deactivated")

プラグインの有効化手順:

  1. 「プロジェクト」→「プロジェクト設定」→「プラグイン」タブ
  2. 一覧からプラグインを見つけ、チェックボックスをONにする

カスタムDockの追加

プラグインの骨組みができたら、実際にエディタに機能を追加してみましょう。最も一般的な活用例が、エディタにカスタムパネル(Dock)を追加する方法です。たとえばシーン内のオブジェクト一覧を表示するデバッグツールや、テクスチャのプレビューUIなどを実装できます。

以下のコードで、左パネル上部にカスタムDockを追加します。

@tool
extends EditorPlugin

var dock: Control

func _enter_tree():
    dock = preload("res://addons/my_plugin/dock/my_dock.tscn").instantiate()
    add_control_to_dock(DOCK_SLOT_LEFT_UL, dock)

func _exit_tree():
    if dock:
        remove_control_from_docks(dock)
        dock.queue_free()

Dock配置スロット

スロット定数位置
DOCK_SLOT_LEFT_UL左パネル上部
DOCK_SLOT_LEFT_BL左パネル下部
DOCK_SLOT_RIGHT_UL右パネル上部
DOCK_SLOT_RIGHT_BL右パネル下部

Dockシーンの作成例

Dockで表示する内容は、通常のシーンとして作成できます。

Controlノードをルートに、必要なUI要素を配置します。

# dock/my_dock.gd
@tool
extends VBoxContainer

@onready var label = $StatusLabel
@onready var button = $RunButton

func _ready():
    button.pressed.connect(_on_run_pressed)

func _on_run_pressed():
    label.text = "実行中..."
    # プラグイン固有の処理
    # EditorInterfaceはEditorPluginスクリプト内で直接アクセス可能
    # Dock内スクリプトからはEditorPlugin経由でアクセスするのが安全
    label.text = "完了"

ツールバーボタンの追加

Dockは常時表示のパネルでしたが、「ワンクリックで特定の処理を実行する」だけなら、ツールバーにボタンを配置するほうが手軽です。たとえばシーン内の全ノードにバリデーションを走らせるボタンなどに向いています。

@tool
extends EditorPlugin

var button: Button

func _enter_tree():
    button = Button.new()
    button.text = "My Tool"
    button.pressed.connect(_on_button_pressed)
    add_control_to_container(CONTAINER_TOOLBAR, button)

func _exit_tree():
    if button:
        remove_control_from_container(CONTAINER_TOOLBAR, button)
        button.queue_free()

func _on_button_pressed():
    print("ツールバーボタンがクリックされました")

主なコンテナ定数

定数配置先
CONTAINER_TOOLBARメインツールバー
CONTAINER_SPATIAL_EDITOR_MENU3Dエディタメニュー
CONTAINER_CANVAS_EDITOR_MENU2Dエディタメニュー
CONTAINER_INSPECTOR_BOTTOMインスペクタ下部

カスタムインスペクタプラグイン

Dockやツールバーに続いて、もう一つ強力な拡張ポイントがインスペクタです。インスペクタは通常、ノードのプロパティを自動表示しますが、特定のノードを選択したときに専用のUIを追加することもできます。たとえば、CharacterBody2Dを選択すると速度値をわかりやすく表示するウィジェットを追加する、といったことが可能です。

まずはEditorPluginでインスペクタプラグインを登録し、その後で実際のプラグインクラスを定義します。

# my_plugin.gd
@tool
extends EditorPlugin

var inspector_plugin: MyInspectorPlugin

func _enter_tree():
    inspector_plugin = MyInspectorPlugin.new()
    add_inspector_plugin(inspector_plugin)

func _exit_tree():
    if inspector_plugin:
        remove_inspector_plugin(inspector_plugin)
# my_inspector_plugin.gd
@tool
class_name MyInspectorPlugin
extends EditorInspectorPlugin

func _can_handle(object: Object) -> bool:
    # このプラグインが処理する対象を判定
    return object is CharacterBody2D

func _parse_begin(object: Object):
    # インスペクタ先頭にUI要素を追加
    var label = Label.new()
    label.text = "== Character Info =="
    add_custom_control(label)

func _parse_property(object, type, name, hint_type, hint_string, usage_flags, wide):
    # 特定プロパティにカスタムコントロールを追加
    if name == "speed":
        var label = Label.new()
        label.text = "速度: %s" % str(object.get(name))
        add_custom_control(label)
        return true  # デフォルト表示を上書き
    return false

ベストプラクティス

ここまでの機能を一通り理解したところで、プラグイン開発で陥りやすい落とし穴をまとめておきましょう。通常のGDScript開発とは異なる注意点があり、特にリソース管理を怠るとエディタがクラッシュしたりメモリリークが発生します。

項目推奨事項
nullチェック_exit_tree() で削除する前に必ずnullチェックする
editor/runtime分離Engine.is_editor_hint() でエディタ専用処理を分岐
リソース管理_exit_tree() で追加した全UI要素を queue_free() する
preloadの活用シーンやリソースは preload() で事前読み込み
エラーハンドリングpush_warning() でユーザーに問題を通知

よくあるミス:

# NG: _exit_tree()でUI要素を解放し忘れ
func _exit_tree():
    pass  # メモリリークやエディタクラッシュの原因

# OK: 必ず解放する
func _exit_tree():
    if dock:
        remove_control_from_docks(dock)
        dock.queue_free()
    if inspector_plugin:
        remove_inspector_plugin(inspector_plugin)

まとめ

  • @toolをスクリプト先頭に付けるとエディタ内で実行される
  • **Engine.is_editor_hint()**でエディタ専用処理を分岐させる
  • EditorPluginクラスで_enter_tree()/_exit_tree()を実装してプラグインを構成
  • カスタムDockadd_control_to_dock()で左右パネルに追加
  • インスペクタプラグインEditorInspectorPluginを継承して特定ノードのUI拡張が可能
  • _exit_tree()で追加した全要素を必ず解放し、メモリリークを防ぐ

さらに学ぶために