Part 4 の今回は、ブロックのオブジェクトを作って、それを画面上に複数配置し、ボールがそのブロックに衝突したらブロックが消えるようにしていく。

それでは前回に引き続きブロック崩しを開発していこう。


Memo:
過去のシリーズをまだご覧になっていない方は、そちらを先にご覧いただくことをおすすめします。
Godot で作るブロック崩し


ブロックのシーンを作る

ブロックを画面に複数配置していくことを考えると、毎回メインシーンにブロック用の子ノードを追加するのは効率的ではないし、ブロックのノードに仕様変更の必要が生じた場合、一つ一つのブロックを修正するのは大変だ。

このように、同じ(または似たような)ノードが複数必要な場合は大まかに以下の2ステップで進めると、開発がとても効率的になる。

  1. 雛形となるシーンを作る
  2. 別のシーンのノードとして必要なだけ雛形のシーンからインスタンスを作る

特に修正が必要な場合、雛形のシーンを修正することによって、自動的にそのインタンスにも同様の変更が加えられるので、メンテナンスのしやすさが断然違ってくる。

ということで、まずは雛形となるブロックのシーンを作っていく。「シーン」メニュー>「新規シーン」を選択する。
Gameノードを選択

シーンドックで「その他のノード」を選択する。
その他のノードを選択

壁を作った時と同様に「StaticBody2D」を選択して「作成」をクリックする。
StaticBody2Dノードを作成

「StaticBody2D」ノードが追加できた。これがルートノードになる。名前を「Brick」に変更しよう。
Brickに名前を変更

一旦、このシーンを保存しておこう。「シーン」メニュー>「シーンを保存」を選択するか、ショートカットキー操作でシーンを保存しよう(シーンを保存:Windows: Ctrl + S / macOS: Cmd + S)。
シーンを保存メニュー

保存先のパスに「res://scene」を選択したら、ファイル名はそのまま「Brick.tscn」として保存しよう。
Brick.tscnとして保存

続けて、シーンドックで「Brick」ノードを選択した状態で「Sprite」を子ノードとして追加しよう。
Spriteノードを作成

「Brick」ノードに「Sprite」ノードが追加された。そのまま「Sprite」ノードを選択した状態にする。
Spriteノードを作成

インスペクタの「Texture」プロパティめがけて、ファイルシステムドックから「Brick.png」をドラッグ&ドロップする。
Spriteノードを作成

「Texture」プロパティにブロック用に用意しておいた画像が反映された。
Spriteノードを作成

続けて「Brick」ノードに「CollisionShape2D」ノードを追加する。
Spriteノードを作成

追加された。そのまま「CollisionShape2D」を選択した状態にする。
Spriteノードを作成

インスペクタの「Shape」プロパティの [空] をクリックして「新規 RectangleShape」を選択して設定する。
Spriteノードを作成

「CollisionShape2D」の形を「Sprite」ノードの Texture の形に合わせていく。2D ワークスペースで作業するが、先にツールバーのスマートスナップを有効にし、グリッドスナップを無効にする。
Spriteノードを作成

赤いポイントをドラッグしながら「Sprite」ノードに合わせる。
Spriteノードを作成



ブロックの色を編集しやすくする

ブロックにスクリプトをアタッチし、そのスクリプトに、簡単にブロックの色を編集できるようにコーディングしていく。

スクリプトをアタッチする

ブロックにスクリプトをアタッチして、必要なプログラミングを実施していく。まずは「Brick」ノードにスクリプトをアタッチしよう。
Spriteノードを作成

スクリプトのファイル保存先に「res://scripts」を指定して、ファイル名を「Brick.gd」とし、作成しよう。
Spriteノードを作成

スクリプトワークスペースが開き、デフォルトのスクリプトが表示された。ここからこのスクリプトを更新していく。
Spriteノードを作成

今回、スクリプトに記述する全体のコードは以下の通りだ。コーディングはちょっと苦手、という方はひとまずこのコードでスクリプトのコードを置き換えてほしい。

tool
extends StaticBody2D


export (Color) var brick_color = Color(1, 1, 1) setget set_color

func _ready():
	set_color(brick_color)

func set_color(color):
	brick_color = color
	if is_inside_tree():
		get_node("Sprite").set_modulate(color)

スクリプト解説

それでは Brick.gd のスクリプトの内容を小分けにして解説していく。

tool
extends StaticBody2D

スクリプトは通常、エディタ上では動かないが、toolキーワードはスクリプトをエディタ上でも動くようにするためのキーワードだ。

なぜこれが必要かというと、最終的に、エディタ(インスペクタ)上で「Brick」ノードの「color」というプロパティの値を編集した時に、スクリプトによって自動的に子ノードである「Sprite」ノードの色も変更させて、画面上に表示されるブロックの色を調整できるようにしたいからだ。あくまで画面上に表示されるのは「Sprite」ノードなので、「Brick」ノードのプロパティをいじっただけでは画面上のブロックの色は変化しない。

extends キーワードは、すでにご存知の通り、StaticBody2D クラスを拡張することを宣言するために記述されている。

export (Color) var brick_color = Color(1, 1, 1) setget set_color

次にbrick_colorというプロパティを定義しつつ、exportキーワードを頭につけることでインスペクタで編集できるようにしている。このプロパティのデータ型をColorとし、デフォルトの色をColor(1, 1, 1)(白)で指定している。

setgetキーワードは、セッターおよびゲッターとしてどの関数を指定するかを示すキーワードだ。このスクリプトでは、セッターとしてset_colorという関数を指定している。

セッターとして関数を指定している場合、そのセッターが紐づいているプロパティの値が外部から変更されたら、そのプロパティの値が変更されるより前に直ちにセッターの関数が実行される。その関数の内容は、そのプロパティに新しい値を代入することが基本的な役割だが、同時に他の作業も実施させることができる。

具体的には、brick_colorというプロパティの値が、インスペクタ上で変更されようとしたら、そのタイミングで自動的にset_colorという関数によってbrick_colorに新しい値が代入されるということだ。set_color関数はプロパティに新しい値を代入すると同時に別の作業も実行するが、その具体的な内容については後ほど解説する。

一方、今回のスクリプトでは使用していないが、ゲッターとして関数が指定されている場合は、そのゲッターが紐づいているプロパティにアクセスされたら、ゲッター関数によって何らかの値が返される。ただし、ゲッターとして関数を指定する場合はvar xxxxx = x setget ,get_methodのように ,(カンマ) が必要なので要注意だ。

func _ready():
	set_color(brick_color)

_ready()関数のブロックの中でset_color()関数が実行されている。set_color()関数の引数には、先に宣言しておいたプロパティbrick_colorを指定している。このset_color()という関数は、先に説明したbrick_colorプロパティのセッターにもなっていた。この関数がちょうどこのあとに定義されているので見ていこう。

func set_color(color):
	brick_color = color
	if is_inside_tree():
		get_node("Sprite").set_modulate(color)

まず定義済みのプロパティbrick_colorに引数colorを代入することで、その値を更新している。続いて、if 構文による条件分岐だ。is_inside_tree()という関数は全てのクラスの親である「Node」クラスにもともと用意されているメソッドで、このスクリプトがアタッチされているノード(ここでは「Brick」ノード)がシーンツリー内に存在するかどうかを Bool 型の値で返す。このようにifの後ろにすぐ Bool 値を返す関数が記述された場合、それは「関数の実行結果が true だったら」という意味になる。ちなみに「関数の実行結果が false だったら」という意味で記述する場合はif not bool_type_method():という書き方になる。

そして、true だった場合は、if 構文のブロック内のコードを実行する。最初に出てくるget_node()は、スクリプトがアタッチされているノード(ここでは「Brick」ノード)から見た子ノードにアクセスするための関数だ。ここではget_node("Sprite")と記述しているので、子ノードである「Sprite」にアクセスしている。

その後続けて.set_modulate(color)とあるが、まず.はその前のオブジェクトが持つプロパティやメソッドであることを示している。つまりset_modulate()関数は「Sprite」のメソッドということになる。この関数は、Sprite クラスの Texture の色を変更する。set_modulate()の引数にset_color()関数の引数であるcolorを指定している。

スクリプトの内容をまとめよう。まず、エディタ(インスペクタ)上で「Brick」ノードのbrick_colorプロパティの値(色)を変更しようとすると、「brick_color」プロパティ自体の値として更新されるだけでなく、同時に子ノードである「Sprite」ノードの Texture の色も同じ色に更新する。また_ready()関数によって、ゲームが開始される際に全てのノードの読み込みが終わった後、ゲーム開始の準備段階でset_color()関数が読み込まれ、引数であるbrick_colorプロパティの値が「Sprite」ノードの Texture の色として適用されるので、「Sprite」ノードの Texture に対して別途、色の設定をする必要は全くない。

難しい部分もあったかもしれないが、とにかくこのスクリプトで実現したかったのは、簡単にブロックの色を編集できるようにする、ということだけなのだ。



ブロックのグループを作って追加する

先にも説明した通り、画面上にブロックを配置する際は「Brick」シーンを雛形(ブループリントや設計図と言っても良いだろう)として、「Brick」シーンのインスタンスを複数作成することになる。複数のインスタンスを作成した後、それらインスタンスに対して何らかの処理を施す場合、それぞれ個別に処理するよりは、それらを一つのグループにまとめて、そのグループに対して1回処理を施したほうが効率的だ。

ということで、ブロックのグループを作っていく。

シーンドックで「Brick」ノードを選択したまま、エディタ右側でインスペクタドックからノードドックに切り替え、グループタブを開く。枠内にグループ名を入力して、右隣の「追加」をクリックするとグループが出来上がる。今回は「Bricks」というグループ名にした。
ノードのグループを作成

現時点では、作ったグループには「Brick」ノードしか含まれていないが、このノードを雛形にしてインスタンスを大量に作成する際、元の「Brick」ノードがすでにグループに追加されているので、生成されたインスタンスも最初からグループに追加されている形になるというわけだ。
グループ作成完了



ボールが当たったらブロックが消えるようにする

それでは久しぶりに「Game」シーンを開こう。「シーン」メニュー>「シーンを開く」を選択する。ショートカットキー操作でも構わない(シーンを開く Windows: Ctrl + O / macOS: Cmd + O)。
シーンを開く

「Game.tscn」を選択して開く。
シーンを選択する

これから Godot のシグナルの機能を利用して「ボールが当たったらブロックが消える」という仕組みを作っていく。シーンドックで「Ball」ノードを選択しよう。
シーンドックでBallノードを選択

エディタ右側にあるノードドックのシグナルタブを開く。あらかじめ様々なシグナルが用意されている。これらはノードの種類によってその数や種類が異なる。

Memo:
シグナルというのは、いわゆる callback で、特定の条件が揃ったら信号を発してくれる便利な機能です。その信号はスクリプト上のメソッドに接続することができ、信号が発せられると接続されているメソッドが発動します。

今回は RigidBody2D ノードに備わっている「body_entered(body: Node)」シグナルを使う。このシグナルを「Ball」ノードのスクリプトに接続していく。
body_enteredシグナルを選択

そのまま「body_entered(body: Node)」シグナルをダブルクリックするか、右下の「接続」をクリックする。
シグナルを接続

「メソッドにシグナルを接続」パネルが表示されたら、「スクリプトに接続」のリストから「Ball」ノードのを選択する。「受信側メソッド」はスクリプトに接続された後に自動的に生成されるメソッドの名前だ。メソッドの名前はデフォルトで「on_ノード名_シグナル名」になる。問題なければ「接続」をクリックする。
シグナルの接続先となるスクリプトを選択

自動的にスクリプトワークスペースで「Ball.gd」スクリプトファイルが開く。スクリプトの一番最後に_on_Ball_body_entered()メソッドが追加されているのがわかるだろう。メソッドが定義されている行の左側にはシグナルに接続していることを示す緑色のアイコンが表示されるので視覚的にもわかりやすい。
スクリプトワークスペース上で接続のアイコンを確認

このメソッドの内容に記述したコードが、シグナルが発せられたタイミングで実行される。今回利用するシグナル「body_entered(body: Node)」の場合は「Ball」ノードにオブジェクトが衝突したら発せられる。このシグナルの引数bodyは「Ball」ノードに衝突したオブジェクトを指す。

結果的に「Ball.gd」のスクリプト全体は以下のようになっている。

extends RigidBody2D


export (float) var ball_speed = 150.0

func _ready():
	var direction = Vector2(1, -1).normalized()
	var velocity = direction * ball_speed
	apply_impulse(Vector2.ZERO, velocity)


func _on_Ball_body_entered(body):
	if body.is_in_group("Bricks"):
		body.queue_free()

それでは_on_Ball_body_entered()メソッドの中身を編集していこう。まず以下の1行は削除する。

pass # Replace with function body.

替わりに、以下のコードを記述する。

	if body.is_in_group("Bricks"):
		body.queue_free()

ここの if 構文全体の意味としては『もしbodyが「Bricks」グループに追加されていればbodyを削除する』という条件分岐のコードになっている。if 構文のbodyはボールが衝突しうるオブジェクトを指す。具体的には「Paddle」ノード、「Wall」ノード、そしてこれから作る予定の「Brick」シーンのインスタンスノードだ。そこで、先に作っておいたノードグループ「Bricks」を利用して、『bodyが「Bricks」グループのメンバーだったら…』という意味のコードにしたのがif body.is_in_group("Bricks"):の部分になる。is_in_group()関数は引数のグループに属しているかどうかを Bool 値で返す。

そして、is_in_group("Bricks")関数が true の結果を返した場合(bodyが「Brick」のインスタンスだった場合)に実行したいのは「その衝突したブロックを消す」という意味のコードだ。queue_free()というのがまさにノード自体をシーンから削除する関数なので、これを実行している。

コードの編集ができたら、「Ball」ノードを選択して、インスペクタ上で、以下の2つのプロパティの値を変更しよう。

  • Contacts Reported:1 に変更
  • Contact Monitor: オンにする
    インスペクタでBallノードのプロパティを選択


レベル(ステージ)のシーンを作る

一般的なブロック崩しゲームは、簡単なレベルから始まり、段々と難易度が上がっていき、最後のレベルが最も難しいというのがオーソドックスなゲームデザインだ。その難易度を決めるのは、ほとんどがブロックの配置や種類(例えば2回当てないと消せないブロックなど)になる。まずは一つ目のレベルとして最も簡単なレベル1のシーンを作っていく。

Memo:
海外では異なる難易度で用意された個別のゲームステージをレベルと表現することが多いのですが、日本のゲームでは昔からステージという表現の方が一般的に浸透しています。レベルという言葉は、日本ではどちらかというとRPGなどのキャラクターの強さを表す指標として使われることが多いです。


新規シーンを作成する

「シーン」メニュー>「新規シーン」を選択する。
新規シーンを選択

「2D シーン」を選択する。これにより、自動的に新規「Node2D」ノードがルートノードとして追加される。
2Dシーンを選択

まず一つ目のレベルシーンを作るので、「Node2D」ノードの名前を「Level1」に変更しよう。
Level1に名前変更


「Level1」ノードに「Brick」シーンのインスタンスを追加する

「Level1」ノードの子ノードとして、作成済みの「Brick」シーンをインスタンス化して追加する。手順は次のどちらでも構わない。

<手順その1>

シーンドック上で「Level1」シーンを右クリックして、「子シーンをインスタンス化」を選択する。
子シーンをインスタンス化

インスタンス化したいシーンのファイルを選択する。ここでは「scene/Brick.tscn」を選択して「開く」をクリックする。
インスタンス化したいシーンを選択

<手順その2>

ファイルシステムドックから、インスタンス化したいシーンのファイルを、シーンドックの親ノード(ここでは「Level1」ノード)にドラッグ&ドロップする。
ファイルシステムからシーンドックにシーンファイルをドラッグ&ドロップ

いずれかの手順によって、「Brick」シーンのインスタンスが「Level1」ノードの子ノードとして追加された。
Brickノード追加完了

この後、このノードを量産していく作業をわかりやすくするために、ノードの名前を「Brick1」に変更する。
Brick1に名前変更

ちなみに、
グループのアイコン は何らかのノードグループに属していることを示している。
インスタンスのアイコン は別のシーンのインスタンスであることを示している。

ここで一度作成した「Level1」のシーンを保存しておこう。保存先パスは「res://scene」、ファイル名は「Level1.tscn」で問題ない。
Level1シーンを保存


「Brick」ノードを量産して配置する

それでは「Brick1」ノードを複製してそれを移動させる、という作業を繰り返していくための下準備をする。まず、ブロックの色を行によって変えることにする。インスペクタで、1行目の1つ目のブロックとなる「Brick1」ノードの「Brick Color」プロパティを選択してカラーピッカーで色を選択(または入力)してほしい。
カラーピッカー

色が更新された。
インスペクタからbrick_colorプロパティの色の変更を確認

次に 2D ワークスペースのグリッドの間隔を調整する。ツールバーの「スナッピングオプション」をクリックして「スナップの設定」をクリックする。
スナップの設定

グリッドのステップを「Brick1」ノードのサイズと同じ横32px、縦16pxに変更して「OK」をクリックする。
グリッドのステップを変更

ツールバーで「移動モード」を有効にし(移動モード Windows: W / macOS W)、グリッドスナップも有効にする。
移動モード、グリッドスナップを有効にする

座標(0, 0)にある「Brick1」ノードを縦に3グリッド分、横に3グリッド分ドラッグして移動させよう。
2D ワークスペースでBrick1をドラッグ

「position」プロパティの値は(96, 48)になっている。
positionプロパティを確認

ここからは簡単だ。以下の操作を10回繰り返して横にブロックが11個並んだ状態を作る。

  1. シーンツリー上一番下にあるブロックのノード(最初は「Brick1」)を選択
  2. ショートカットキー(Windows: Ctrl + D / macOS: Cmd + D)で選択したブロックのノードを複製
  3. 複製したブロックを 2D ワークスペース上で横に1グリッド分移動

Brickノードの複製、移動を繰り返す

それではシーンを実行して確認していこう。ただし、色も確認したいので、先に「デバッグ」メニュー>「コリジョン形上の表示」のチェックを外しておく。チェックが入っていると、コリジョン形上を表す半透明の緑色が重なってしまうので、実際のブロックの色を確認できないからだ。
デバッグメニュー>コリジョン形状の表示をオフ

シーンを実行する。
シーンを実行

自分で指定した色のブロックが横一列に並んでいる。
デバッグパネルでブロックの配置確認

先のブロック複製作業をさらに繰り返して、画面上に4段ほどブロックを並べよう。ゲーム性には直接関係ないが、見た目を少し楽しくするために一段ずつ色を変えていこう。
残り3段分ブロックを複製、移動

それでは再度シーンを実行して、ブロックの配置を確認しよう。
シーンを実行してデバッグパネルで確認



レベルシーンをゲームシーンの子ノードにする

最初のレベルのシーンである「Level1」が完成したので、これをインスタンス化して「Game」シーンに子ノードとして追加していく。

「Game」シーンに切り替えたら、「Level1.tscn」のインスタンスを「Game」ノードの子ノードとして追加する。
シーンドックでGameノードにLevel1シーンのインスタンスを追加

ようやく 2D ワークスペースにブロック崩しに必要な全てのノードが揃った。
デバッグパネルで確認

最後にプロジェクトを実行して遊んでみよう。
シーンを実行

若干パドルのスピードが遅くてハードモードだが、ボールがブロックに当たった時はブロックが消えて、パドルと壁に当たった時は跳ね返る。パドルが壁をすり抜けていること、ボールの跳ね返る角度が毎回45°であることなど、まだ修正すべき点は残っているが、ひとまず必要最小限の要素でブロック崩しが完成した。
デバッグパネルで動作確認



おわりに

以上で Part 4 は完了だ。今回は長めのチュートリアルだったが、最後まで完成できただろうか。

ここまで 4 回のチュートリアルを通してシンプルながらゲーム開発の基本が詰まったブロック崩しを作ってきた。やはり自分で作ったゲームが期待通りに動くと感動だ。次回以降もこのブロック崩しをさらに発展させていこう。