_enable_plugin 関数が呼ばれた直後、_handles 関数が呼ばれず描画ができない問題

■Godot Version
v4.3.stable.official [77dcf97d8] win64

■はじめに
EditorPlugin 派生クラスの _enable_plugin 関数が呼ばれた後、編集ノードを選択するまで _handles 関数が呼ばれず描画ができない問題とその対処法について述べます。

この問題は、通常のプラグイン利用時には発生しづらく、発生したとしても簡単な操作で対処できます。
このことから、これは軽微な問題かもしれません。
しかし、プラグイン開発時には起こりやすい問題なので、既知の問題としてフォーラムで共有してほしく、投稿しました。
(Godot Forum の方には開発者へ伝える目的で投稿し ttps://forum.godotengine.org/t/problem-that-handles-function-is-not-called-immediately-after-enable-plugin-function-is-called-and-drawing-cannot-be-performed/101300 で承認・公開されました)

■現象
公式のプラグインによる描画のサンプルコードを用いて、2D ワークスペース上のマウスカーソルの位置に円を描画するプラグインを作成したところ、「プロジェクト設定」ダイアログの「プラグイン」タブでそのプラグインの「有効」をオフからオンにした直後から、シーンドックで別のノードを選択するまでの間、 _handles, _forward_canvas_draw_over_viewport 関数が呼ばれず、プラグインの 2D ワークスペースへの描画処理ができない。
サンプルコード → ttps://docs.godotengine.org/ja/4.x/classes/class_editorplugin.html#class-editorplugin-private-method-forward-canvas-gui-input

■再現方法

  1. 「プロジェクト設定」ダイアログの「プラグイン」タブで「有効」のチェックをオフにした状態からオンにしてダイアログを閉じます。

  2. 2D ワークスペース上にマウスカーソルを置きます。

  3. マウスカーソル位置に円が描画されません。(_handles も呼び出されていません)←問題の部分

  4. シーンドックで別のノードを選択します。(_handles が呼び出されます)←現状の対処法

  5. 2D ワークスペース上にマウスカーソルを置くと円が描画されました。

■現象を再現するためのプラグインのスクリプト
以下は、公式のサンプルコードを含んだ、 2D ワークスペースのマウスカーソル位置に円を描画するプラグインのスクリプトです。
これを「プロジェクト設定」ダイアログの「プラグイン」タブで追加して試すことができます。

sc_path_tool.gd プラグイン用スクリプト全文(対処前)

@tool
extends EditorPlugin

## ノードが SceneTree に入ったとき (インスタンス化時、シーン変更時、またはスクリプトで add_child を呼び出した後など) に呼び出されます。
func _enter_tree():
	# Initialization of the plugin goes here.
	print("ScPathTool plugin を初期化します")
	
	return

## ノードが SceneTree から出ようとしているときに呼び出されます(たとえば、解放時、シーンの変更時、またはスクリプトで Remove_child を呼び出した後)。
func _exit_tree():
	# Clean-up of the plugin goes here.
	print("ScPathTool plugin を終了します")
	return

## ユーザーがプロジェクト設定ウィンドウの [プラグイン] タブで EditorPlugin を有効にすると、エンジンによって呼び出されます。
func _enable_plugin():
	print("_enable_plugin called")
	return

## ユーザーがプロジェクト設定ウィンドウの [プラグイン] タブで EditorPlugin を無効にすると、エンジンによって呼び出されます。
func _disable_plugin():
	print("_disable_plugin called")
	return

## 2D エディタのビューポートが更新されるときにエンジンによって呼び出されます。
## 描画に [param overlay] コントロールを使用します。
## [method EditorPlugin.update_overlays] を呼び出すことで、ビューポートを手動で更新できます。
func _forward_canvas_draw_over_viewport(overlay: Control) -> void:
	print("_forward_canvas_force_draw_over_viewport called.", overlay)
	# 以下のコードは、公式のサンプルです。
	# https://docs.godotengine.org/ja/4.x/classes/class_editorplugin.html#class-editorplugin-private-method-forward-canvas-draw-over-viewport
	# マウスポインタの位置に円を描きます。
	overlay.draw_circle(overlay.get_local_mouse_position(), 64, Color.WHITE)
	return

## 現在の編集シーンにルートノードがあり、_handlesが実装され、2DビューポートでInputEventが発生したときに呼び出されます。
func _forward_canvas_gui_input(event):
	print("_forward_canvas_gui_input called.", event)
	# 以下のコードは、公式のサンプルです。
	# https://docs.godotengine.org/ja/4.x/classes/class_editorplugin.html#class-editorplugin-private-method-forward-canvas-draw-over-viewport
	if event is InputEventMouseMotion:
		# マウスポインタを移動するとビューポートを再描画します。
		update_overlays()
		return true
	return false

## プラグインが特定のタイプのオブジェクト (リソースまたはノード) を編集する場合は、この関数を実装します。
## true を返すと、エディターが関数 _edit と _make_visible を要求したときに呼び出される関数を取得します。
## _forward_canvas_gui_input メソッドと _forward_3d_gui_input メソッドを宣言している場合、これらも呼び出されます。
func _handles(object: Object) -> bool:
	print("_handles called. object = ", object)
	return true

■対処するために作成したスクリプト
プラグインのスクリプトの _enable_plugin 関数内で、以下の request_call_editor_plugin_handles_event 関数を呼び出すことで、スクリプトでノードの再選択を行うことで、 _handles 関数が呼ばれるようにすることで、対処できます。

sakuracrowd_util.gd

extends Object
class_name SakuraCrowdUtil

## [method EditorPlugin._handles] イベント関数がシステムから呼び出されるように促します。
## 方法として、選択中のノードの再選択([method SakuraCrowdUtil.reselect_nodes])、
## 選択されていない場合はルートノードの選択([method SakuraCrowdUtil.add_root_node_to_selection])を行います。
## いずれかの方法を行った場合は true, それ以外は false を返します。
static func request_call_editor_plugin_handles_event(editor_plugin: EditorPlugin) -> bool:
	if reselect_nodes(editor_plugin) == false:
		if add_root_node_to_selection(editor_plugin) == false:
			print("SakuraCrowdUtil.request_call_editor_plugin_handles_event: 何も行いませんでした。")
			return false
		else:
			print("SakuraCrowdUtil.request_call_editor_plugin_handles_event: add_root_node_to_selection を行いました。")
	else:
		print("SakuraCrowdUtil.request_call_editor_plugin_handles_event: reselect_nodes を行いました。")
	return true

## 現在選択されているノード群を一度解除してから再び選択します。
## 選択されているノードがない場合はルートノードを再び選択します。
## ルートノードがない場合は何もしません。
## 再選択を行った場合は true, それ以外は false を返します。
static func reselect_nodes(editor_plugin: EditorPlugin) -> bool:
	var editor_interface: EditorInterface = editor_plugin.get_editor_interface()
	
	# すべての選択中のノードの選択を解除して、そのノード群を取得します。
	var deselected_nodes: Array[Node] = deselect_nodes(editor_plugin)
	
	if deselected_nodes.size() > 0:
		# 選択解除したノード群を再選択します。
		for node in deselected_nodes:
			editor_interface.get_selection().add_node(node)
		print("SakuraCrowdUtil.reselect_nodes: 選択中のノードを再選択しました。")
		return true
	return false

## 選択中のノード群を解除します。
## 解除したノード群の配列を返します。配列の要素数 size() は 0 かもしれません。
static func deselect_nodes(editor_plugin: EditorPlugin) -> Array[Node]:
	var editor_interface: EditorInterface = editor_plugin.get_editor_interface()
	
	# 選択中のノード群を取得します。
	var selected_nodes: Array = editor_interface.get_selection().get_selected_nodes()
	
	# 選択中のノードを1つずつ解除します。
	if selected_nodes.size() > 0:
		for node in selected_nodes:
			editor_interface.get_selection().remove_node(node)
		print("SakuraCrowdUtil.deselect_nodes: 選択中のノードを選択解除しました。")
	
	return selected_nodes

## ルートノードを選択中のノードに追加します。
## 追加した場合は true, それ以外は false を返します。
static func add_root_node_to_selection(editor_plugin: EditorPlugin) -> bool:
	var editor_interface: EditorInterface = editor_plugin.get_editor_interface()
	
	# 選択されているノードがない場合、ルートノードを再選択します。
	var scene_tree: SceneTree = editor_plugin.get_tree()
	var root_node: Node = scene_tree.edited_scene_root
	if root_node:
		editor_interface.get_selection().add_node(root_node)
		print("SakuraCrowdUtil.add_root_node_to_selection: ルートノードを選択しました。")
		return true
	else:
		print("SakuraCrowdUtil.add_root_node_to_selection: ノードが選択されていませんでしたし、ルートノードもありませんでした。")
	return false

sc_path_tool.gd(対処するための関数の呼び出しを追加した関数の定義)

## ユーザーがプロジェクト設定ウィンドウの [プラグイン] タブで EditorPlugin を有効にすると、エンジンによって呼び出されます。
func _enable_plugin():
	print("_enable_plugin called")
	# 有効になった直後に、 _handles イベント関数の呼び出しを促します。
	SakuraCrowdUtil.request_call_editor_plugin_handles_event(self)
	return

■参照してほしい記事
以下は、私が以上のことに関して書いた記事です。現象を再現している動画やスクリーンショット画像などを参考にしていただければ幸いです。

・現象の再現の記事
Godot4 プラグインで2Dワークスペースに円を描画するサンプルの実装と確認 | Compota-Soft-Press

・対処用スクリプトを追加した結果の記事
Godot4 選択ノードの解除・再選択・ルートノードの選択追加のスクリプト例 | Compota-Soft-Press

1 Like