概要
動作確認環境: 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")
プラグインの有効化手順:
- 「プロジェクト」→「プロジェクト設定」→「プラグイン」タブ
- 一覧からプラグインを見つけ、チェックボックスを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_MENU | 3Dエディタメニュー |
CONTAINER_CANVAS_EDITOR_MENU | 2Dエディタメニュー |
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()を実装してプラグインを構成 - カスタムDockは
add_control_to_dock()で左右パネルに追加 - インスペクタプラグインは
EditorInspectorPluginを継承して特定ノードのUI拡張が可能 _exit_tree()で追加した全要素を必ず解放し、メモリリークを防ぐ