🤖

Godotで_init(args)を書くとシーンのインスタンス化のときに困る

2020/12/16に公開
1

はじめに

この記事はGodotエンジンでシーンをインスタンス化したときに、うまくいかなかったことを日本語で共有する記事です。また内容は3.2.3.stable.officialのバージョンで確認しました。

概略は以下のようなものになります。

  • _init(args)を書いてあるシーンはインスタンス化の際、初期化できず、拡張した関数を呼べない
  • 本来、_init(args)はクラスをnewするときに使う
  • シーンの初期化のナイーブな解決

_init(args)を書いてあるシーンはインスタンス化の際、初期化できず、拡張した関数を呼べない

Godotではシーンは再利用可能なリソースで、インスタンス化することで便利に使うことができます。
ところが、なんらかの引数を持つような_init(args)を定義したスクリプトをアタッチしてあるシーンではインスタンス化の際、初期値を与えることができず、またそのインスタンスは継承されたノードで追加した関数を呼ぶことができません。

Scene.gd
# Scene.tscn のルートノードにアタッチしてあるスクリプト。
# ルートノードはSpriteとした
extends Sprite

# 独自の関数を定義する
func change_stats():
	print("change_stats")

func _init(s:String):
	print("hello from _init" + str)
Main.gd
const Child_Scene = preload("res://Scene.tscn")
func instance_scene():
	var s_instance = Child_Scene.instance() # 引数に何かを与えるとエラーになる
	print(s_instance.get_class()) # -> Sprite
	s_instance.change_stats() #  Invalid call. Nonexistent function 'change_stats' in base 'Sprite'.
	add_child(s_instance)

上記の例では単にprintしてあるだけですが、この挙動では以下のようなことに対応できません。

  • シーンに初期値を設定したい場合
  • シーンでノードを拡張した関数を使いたい場合

_init(args)はクラスをnewするときだけ

調査の結果、引数を伴わない_init()の場合では拡張された関数を正しく呼べることがわかりました。

Scene.gd
extends Sprite
func _init():
	print("hello from _init")

func print_some(s): # 関数を拡張
	print("some " + s)
Main.gd
const ChildScene = preload("res://Scene.tscn")
ChildScene.instance() # -> hello from _init
ChildScene.instance().print_some("hoge") # 拡張された関数も呼べる

ただ、当然、この例では初期値を渡すことができません。

本来、引数つきの_init(args)は、スクリプトからクラスをnewする際に使われるものなのだそうです。

Scene.gd
extends Sprite
func _init(s):
	print("hello " + s)

func print_some(s): # 関数を拡張
	print("some " + s)
Main.gd
const SceneClass = preload("res://Scene.gd") # シーンではなく、スクリプトをロード
SceneClass.new("uho") # -> hello uho
SceneClass.new("uho").print_some("hoge") # 拡張された関数も呼べる

あるいはclass_nameを用いてエンジンにクラスを登録することで、ロードする必要がなくなります。

Scene.gd
extends Sprite
class_name ExtendSprite # クラスをエンジンに登録する
func _init(s):
	print("hello " + s)
	
func print_some(s): # 関数を拡張
	print("some " + s)
Main.gd
ExtendSprite.new("uho") # -> hello uho
ExtendSprite.new("uho").print_some("hoge") # 拡張された関数も呼べる

クラスを初期化する方法とシーンを初期化する方法は、今のところのGodotでは別のものであるようです。

シーンの初期化方法のナイーブな解決

これまで、_init(args)ではうまくシーンを初期化できず、この方法はクラスを初期化することしかできないことを見てきました。それではシーンの初期化はどのように行えばよいのでしょうか?

ナイーブな方法ですが、初期化用の関数を作ってインスタンス化直後に呼ぶという方法があります。

Scene.tscnを作成して、ルートノードに以下のようなスクリプトをアタッチしましょう。

Scene.gd
func initialize( pos: Vector2 ): # 初期化するための関数
	set_position(pos)
	return self # selfを返すとメソッドチェーンで一行で書けるので良い

Scene.tscnをインスタンス化するコードではこのように使用できます。

Main.gd
const Scene = preload("res://Scene.tscn")

func some_function():
	var pos = Vector2.new(0, 0)
	var s = Scene.instance().initialize(pos) # instance()の後に呼び出す
        add_child(s) # _readyが呼ばれる

参考にした記事

おわりに

クラスによるnew()か、シーンによるinstance()か、少し設計として混乱しやすいような気がしました。何か意図があるのでしょうか。何か詳しい方はコメントしていただけると嬉しいです。

Discussion

fgkcomfgkcom

同じような内容にはまっていました。大変助かりました。 ☺️

本来、引数つきの_init(args)は、スクリプトからクラスをnewする際に使われるものなのだそうです。

ぐぐ 😫

ナイーブな方法ですが、初期化用の関数を作ってインスタンス化直後に呼ぶという方法があります。

私もこれでいこうと思います! 🙇