俺に解るように説明する "Godot Engine 3.x" 入門+

ゲームエンジン Godot Engine に関すること。入門とか使い方とかチュートリアルとか、あれとかこれとか。日本語解説。

Godot Engine GDScript 11 「神シーン常駐システム完成」...1/2

GDScript の 10 でやりたかった神シーン常駐システムが完成。出来てしまえば、なぜ苦戦していたのか分からないくらい簡単な構造だった。ものの5分のあれば出来てしまうような内容で愕然。

f:id:ore2wakaru:20170825170907p:plain

下準備

GDScript の 10 で作った「シーンGOD」「シーンA」「シーンB」の3つのシーン、それと「シーンGOD」のメインシーン設定。これが前提。

スクリプトは2か所に貼る。「シーンGOD」のルートノードである「Node_GOD」と、「シーンA」のルートノード「Node」。

「シーンGOD」、「Node_GOD」用スクリプト

まずは、こちらから。いきなり答え。ファイルネームは “sn_God.gd” としたけど、何でもイイ。

f:id:ore2wakaru:20170902101754p:plain

【解説】

◆ 1行目:

extends Node

特になし。

◆ 3~4行目:

onready var my_sn_A = preload("res://sn_A.tscn").instance()
onready var my_sn_B = preload("res://sn_B.tscn").instance()

「 onready var と var 」

Godot では変数を宣言する時には var キーワードを使用する。ただ、場合によって変数の初期値を設定したい時、そのタイミングを _ready() 関数が呼び出されると同時にしたい場合、var の前に onready キーワードを置いて onready var として使用する。

_ready() 関数は以前実験でも確かめたように、シーンに含まれるすべてのノードがシーンツリーに追加されたで呼び出される。このタイミングは決まっているので、子ノード達がシーンツリーに入っていることが変数の初期化に必要な場合には、onready var が確実。

もちろん、var で宣言だけして、_ready() 関数のボディーで値を入れてやるのと同じことだが、めんどくさいので、onready var 一発ということ。var はどうでもいい時、onready var は子ノードの存在が必要な時、がそれぞれの使い時。

今回は、子ノードがシーンツリーに入っていることが変数の初期化に必要じゃないんだけど、カッコいいから使ってみた感じ。別に var だけでもまったく何にも全然問題はナイ。

それと面白かったのは “var"。変数は英語で "variable"(発:リアブル)だから、こっから "var” を取ってきたんだろうと思いきや、なんと “variant"(発:リアント、意:異形)の "var” だという。「異形」って、冴えない主人公がどっか変な世界にでも迷い込んだ系の・・・

「 preload() と load() 」

onreadyvar も黄色いからキーワードと分かるが、preload() もなにげに黄色いのに注意。これはキーワードではなく、ビルトイン “Built-in” 関数print() と同じ Godot API > @GDScript に定義されている例のアレ)。黄色いのは、Godot が使用方法を決定しているので、ユーザーに対して勝手に書き換えて使うなよっていう注意の意味があるんだなキット。

さて、後々色んなシーンを付けたり外したりしたいので、変数 “my_sn_A” には「シーンA」を “my_sn_B” には「シーンB」をそれぞれ割り当てておきたい。このような場面では load() 関数または preload() 関数を使う。( ) の中にはパス名を入れる(右クリ [Copy Path] のやつ)。

違いは恐らく、load() 関数は出てきた時に割り当てる、preload() 関数はゲームが始まる前にあらかじめ割り当てておく、という事だと思う。長短に関しては、load() はゲーム中に割り当てるので場合によってはカクつく。それがイヤなら preload() を使うのだが、こちらは前もって割り当てるのでシーンが大きいとその分メモリを前もってガッツリ食ってしまう。こんな感じか。

今回は preload() を使っているが、これも別に load() にしても全く問題はナイ。なんかコッチの方がカッコイイかなと思っただけ。

「 instance() っているの?」

シーンを変数に割り当てるだけならイラナイが、追加したり取り除いたりしたい場合は、instance() 関数をドット(".“)を挟んでこの段階で付ける必要がある。instance() 関数は PackedScene クラスで定義されている。

これはどんなもんかと言うと、よく分からないのだが恐らく、「シーンをノードのカタマリととらえてねー、ノードだよー」とするものだと思う。シーンをくっつけていく時、後で出てくる add_child() 関数を使うのだが、これでくっつける事が出来るのは、あくまでノード。なので、子ノードとしてシーンを追加出来る様に、シーンのノード化をするもの、と考えるのが妥当だろう。(又は、リソースのノード化)

(もちろん、元のシーンをノード化、つまり完全別物にしてしまっては何かあった時、本体が残ってないので都合が悪い。本体はそのまま残して、コピペしてそのコピペの「ペ」の部分をノード化したんじゃなかろーかって感じだ。俺の想像。インスタンスだし。)

「まとめ」

これで、"my_sn_A" には「シーンAがノード化された物」が、"my_sn_B" には「シーンBがノード化された物」が入ったことになる。以下まとめ図。

f:id:ore2wakaru:20170902142835p:plain

あと、これは個人の趣味なんだけど、自作の変数や関数は “my_” で始めている。Godot が用意している変数・関数と万一にも被らない様にするため。別に “ore_” で始めてもいいし、c# みたいにアンダースコアを使わずに、"mySceneA" とかやってもいいし、変数は小文字で、関数は大文字で初めてもいい。python 系だと、全部小文字で初めて、アンダースコア “_” で単語を区切るのが一般的な様子。

それから、もし日本語の変数や関数が使えたら使ってたんだけどなー。これなら絶対被ることはないだろーし。だがエラーになる。残念。

◆ 6~7行目:

func _ready():
    my_add_sn_A()

シーンの全ノードがシーンツリーに追加されたら呼ばれる関数が _ready() 関数だった。(Unity の Start()UE4Event Begin Play とはちょっと違うよね、コレ。) ボディーで自作関数の my_add_sn_A() 関数を呼んでいる。自作関数の my_add_sn_A() 関数は簡単に言うと、「(ノード化された)シーンA」を子ノード「Node_sn_A」に付ける仕事をする。my_add_sn_A() 関数の定義はこの後。

このスクリプトが付いている「シーンGOD」はメインシーンに設定されているハズ。なので、[ゲーム再生] ボタンが押されると自動的にシーンツリーに追加される。そして、この _ready() 関数が呼ばれる。その中で my_add_sn_A() 関数が呼ばれて「シーンA」が追加される感じだ。ここまでは自動的に進む。

このスクリプトが張り付いている「シーンGOD」にはルートに「Node_GOD」、その子として「Node_sn_A」と「Node_sn_B」の全部で3つのノードがある。「シーンA」を子ノード「Node_sn_A」に付けるには、当然だが、「Node_sn_A」の準備が出来ていないとマズい。準備できていない状態で my_add_sn_A() 関数を呼んでも、「シーンA」を「Node_sn_A」に付けることは出来ない。だから、全ノード(3つ)の準備が完了したら呼ばれる _ready() 関数のボディで呼ぶ必要がある。_enter_tree() 関数ではダメという事。しつこいようだが、念のため。

◆ 9~11行目:

#シーンAを「Node_sn_A」に追加する
func my_add_sn_A():
    get_node("Node_sn_A").add_child(my_sn_A)

自作関数 my_add_sn_A() を定義する。「シーンA」をノード「Node_sn_A」の子として追加するためのもの。_ready() 関数内で呼ばれているので、すぐに発動する。意外と最初の頃は、インデントを入れるのが出来るようになっても、":“(コロン)を忘れる。注意。

「コメントって必要なの?」

“#” は、このマーク以降、1行がコメントになるということ。日本語もそのままスクリプトエディタに打ち込めるので、あえて英語で書くなんてのはナンセンス。簡潔に日本語で分かりやすく書いておくべし。

書かなくても大丈夫だと思っても、やっぱり時間が経つと何をやってるんだか忘れるもの。中身を読めば思い出せると思っても、それはあれだ。書くのに30秒、読んで何をやっているのか思い出すのに40秒だとする。書いといた方が10秒お得って感じだ。

なに? コメント読むのに2秒かかるって? なら、8秒お得って事にしよう。なに? 読むのに20秒かかるコメントを書いた場合はって・・・。大事だからもう一度書いておこう、「簡潔に」書いておくべし。

「get_node() に入れる文字列」

get_node() 関数は過去に何度か出てきた Node クラスで定義されているものだが、これは自ノード以外のノードを操作する時使う関数だった。( ) の中に “ノード名” を入れて使う。注意しなければならないのは、普通にノード名を書くと、スクリプトが付いているノードの子ノードが対象になるということだ。

今回は、子ノードである「Node_sn_A」を操作対象にしたいので、普通に get_node("Node_sn_A") となっている。

Node_oya
    Node_jibun
        Node_ko_A
        Node_ko_B
            Node_mago
    Node_imouto
    Node_otouto

では、上のようなヒエラルキーハイアラーキー)の場合、「Node_jibun」からそれぞれのノードを操作対象にしたい場合どうするかというと、以下表。

操作対象 記述 備考
Node_oya get_node("../") 親は何でも “../” で指定
Node_ko_A get_node("Node_ko_A") 普通に書くと子ノードが対象に
Node_ko_B get_node("Node_ko_B") 子どもBも普通にどうぞ
Node_mago get_node("Node_ko_B/Node_mago") さらに下位は “/” でつなぐ
Node_imouto get_node("../Node_imouto") 同一階層は親の子と考え “../XXXX”
Node_otouto get_node("../Node_otouto") 同じく2号

親の親なら、get_node("../../") と増やしていけば、階層を登っていける。この他の指定の仕方は、また今度。

「add_child() で子として追加出来るのは」

ノードである。add_child()Node クラスで定義されている関数で、( ) 内のノードを自ノードの子として新たに追加するもの。

ここでは、add_child(my_sn_A) として “my_sn_A” を子ノードとして追加するようにしている。このように記述するために、先に instance() 関数でシーン(リソース)をノード化したわけだ。

「まとめ」

f:id:ore2wakaru:20170902171004p:plain

◆ 13~15行目:

#シーンAを取り除く
func my_remove_sn_A():
    my_sn_A.queue_free()

一方こちらは、シーンを取り除く関数。取り除きたいノードの後ろにドット “.” を付けて queue_free()Node クラス)。これで、消せる。シーンをノード変数化しておいたから、これが使えるわけだ。なんでかよく分からないが上手くいってる。すごいな俺。

「 queue_free() と free() 」

ノードを消すには、他に free() という関数(こちらは Object クラス)もある。これは、即座に消すタイプ。queue_free() は消すノードに仕事が残っている場合は、その仕事が終わった後に消すタイプ。なので、queue_free() は使いようによっては画面に情報がいくらか滞在してしまう。よって、即座に消すなら free()、 仕事が終わってから消すなら queue_free()

今回は、消すノード “my_sn_A” の方に仕事をさせているので、あえて queue_free() を使っている。

「 remove_child() じゃダメ?」

ダメ。知らんが、なんかこれだと、デリートされずに残るらしい。メモリリークの原因になるっぽい。知らんけど。add_child() で付けたんだから、remove_child() で外したいよねホントはねー、なんか名前が揃って綺麗じゃんねー。

◆ 17~23行目:

こちらは、「シーンB」用の付けたり外したり。やってることは「シーンA」用のと同じ。

#シーンBを「Node_sn_B」に追加する
func my_add_sn_B():
    get_node("Node_sn_B").add_child(my_sn_B)

#シーンBを取り除く
func my_remove_sn_B():
    my_sn_B.queue_free()


いやはや、書き出すと半日かかるなり。「シーンA」の方の作業は次回にしよう。

そして一応、全文コピペ用。

「シーンGOD」、「Node_GOD」用スクリプト

extends Node

onready var my_sn_A = preload("res://sn_A.tscn").instance()
onready var my_sn_B = preload("res://sn_B.tscn").instance()

func _ready():
    my_add_sn_A()

#シーンAを「Node_sn_A」に追加する
func my_add_sn_A():
    get_node("Node_sn_A").add_child(my_sn_A)

#シーンAを取り除く
func my_remove_sn_A():
    my_sn_A.queue_free()

#シーンBを「Node_sn_B」に追加する
func my_add_sn_B():
    get_node("Node_sn_B").add_child(my_sn_B)

#シーンBを取り除く
func my_remove_sn_B():
    my_sn_B.queue_free()