Part 2 の今回は、ボールのオブジェクトを作って、パドルからボールを発射するところまで進めていく。

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

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


ボールを作る

前回の Part 1 でプレイヤーが操作するパドルを作ったので、今度はボールを作る。

ルートノード「Game」の子ノードとして「RigidBody2D」ノードを追加する。
RigidBody2D を Game の子ノードとして追加

名前は「Ball」とする。
名前を Ball に変更

次に「Ball」ノードに必要な子ノードを追加していく。これは「Paddle」ノードの時と同様の作業だ。まずは「Sprite」ノードを追加しよう。
Ball ノードに Sprite ノードを追加

続けて「CollisionShape2D」ノードを追加しよう。
Ball ノードに CollisionShape2D ノードを追加

「Ball」ノードに2つの子ノードが追加された。
Ball に2つの子ノードを追加完了

それぞれのプロパティを設定していこう。

まずは「Sprite」ノードから。シーンドックで「Sprite」を選択した状態にして、右側のインスペクタドックを見ると「Texture」プロパティが [空] になっている。
Sprite ノードの Texture プロパティ

その「空」めがけて、ファイルシステムドックから「Ball.png」をドラッグ&ドロップする。
Ball.png を Texture へドラッグ&ドロップ

「Texture」プロパティにボールの画像が反映したのを確認しておこう。
画像が Texture に適用された

次にシーンドックで「CollisionShape2D」ノードを選択し、インスペクタを見ると「Shape」プロパティが [空] になっている。
CollisionShape2D ノードの Shape プロパティ

その[空]をクリックして「新規 CircleShape2D」を選択する。
新規 CircleShape2D を選択

「Shape」プロパティが設定されました。2D ワークスペースを見てみよう。
Shape プロパティ設定完了

半透明の緑色の円が「CollisionShape2D」ノードの形だ。サイズが「Sprite」ノードの Texture よりやや大きいので、赤丸のポイントをドラッグしながら大きさを「Sprite」ノードの Texture に合わせよう。
Collision Shape を調整

これくらいにする。
Sprite と同じサイズ

それではここからインスペクタドックで「Ball」ノードの変更が必要なプロパティを設定していこう。シーンドックで「Ball」ノードを選択してインスペクタを見てほしい。

上から順番にいこう。まずはの「Mode」プロパティを変更する。「Character」にする。これによりボールが回転するのを防ぐことができる。
Mode プロパティ

次に「Physics Material」プロパティの [空] をクリックして「新規 PhysicsMaterial」を選択する。
PhysicsMaterial プロパティ

設定した「PhysicsMaterial」をクリックするとさらにプロパティが表示される。「Friction」プロパティを 0 にして摩擦の影響でボールのスピードが落ちないようにする。また「Bounce」プロパティを 1 に変更して、パドルに当たったらきっちり跳ね返るようにする。
Friction\u3000及び Bounce プロパティ

続いて「Gravity Scale」プロパティを設定する。この値は 0 にしないと、ボールが重力の影響を受けて、常に画面の下方向に落ちる力がかかってしまう。
Gravity Scale プロパティ

「Linear」>「Damp」プロパティも0にする。これもボールの勢いがなくならないようにするためだ。
Linear > Damp プロパティ

では 2D ワークスペースで「Ball」ノードを移動させる前に、「Ball」ノードの子を選択不可にしておこう。
Ball ノードの子ノードを選択不可

シーンドックでも選択不可になっていることが確認しておく。
シーンドックで選択不可設定確認

ドラッグして「Ball」ノードを移動させる際に、ツールバーでいくつか操作する。

まず、位置をパドルに合わせやすいように、ツールバーから「グリッドスナップ」を有効にする。
ツールバー>グリッドスナップ

そして「移動モード」アイコンをクリックして、ドラッグで移動できるようにする。ショートカットキーを覚えておくと便利だ( Windows: W / macOS: W )。
ツールバー>移動

2D ワークスペースのディスプレイ表示範囲の左上角(0, 0)から、パドルのちょうど真上にドラッグして「Ball」ノードを動かする。
2DワークスペースでBallノードをドラッグ

「Ball」ノードの「Transform」>「Position」プロパティを確認しておこう。x座標が 256 で「Paddle」ノードと同じなので、今ボールがパドルのちょうど真上に位置していることがわかる。もしズレてしまった場合は、プロパティの値を直接触って調整すると効率的だ。
BallノードのPositionプロパティ

ここで一度、シーンを実行してみよう。
シーンを実行

なんだかパドルに対してボールが大きすぎる印象だ。サイズを変更しよう。
ボールのサイズが大きい

シーンドックで「Paddle」ノードの子ノード「Sprite」を選択した状態で、インスペクタの Node2D クラスの「Transform」の「Scale」プロパティで x, y の値をそれぞれ 0.5 とする。サイズを半分にするという意味だ。
SpriteノードのScaleプロパティ

次に同じく「Paddle」ノードの子ノード「CollisionShape2D」ノードを選択して、2D ワークスペース内でポイントをドラッグして、サイズを「Sprite」ノードに合わせよう。
CollisionShape2Dのサイズ調整
CollisionShape2Dのサイズ調整

Memo:
「Paddle」ノードの「Scale」プロパティを直接変更すれば1回の作業で済むのではないか、とお思いになる方もいらっしゃるかもしれません。しかし残念ながら、子ノードの「CollisionShape2D」ノードのサイズが変わってなければ、物理エンジンによって「Paddle」ノードの「Scale」プロパティの値が上書きされてしまいます。

ボールがパドルから離れてしまったので、「Ball」ノードを選択した状態にして、2D ワークスペース上でドラッグして移動させよう。この時、ツールバーでピクセルスナップを有効にして、グリッドスナップは無効にしておくと移動しやすいと思う。
Ballノードを移動

さっきより良いバランスになった。
Ballノードの位置確認

念のため、インスペクタで「Ball」ノードの「Position」プロパティを確認しておこう。以下のスクリーンショットのようになっていれば問題ないだろう。
BallノードのPositionプロパティ



ボールを発射する

それではここからスクリプトを作ってボールを動かしていく。シーンドックで「Ball」ノードを選択した状態で、スクリプトをアタッチする。
Ballノードにスクリプトをアタッチ

先に作っておいた「scripts」フォルダに保存したいので、フォルダアイコンをクリックしよう。
フォルダアイコンをクリック

もしパスが「res://scene」になっていたら、一つ上の階層に戻ってから「scripts」フォルダを選択しよう。パスが「res://scripts」になったら、ファイル名を「Ball.gd」として「開く」をクリックする。
scriptsフォルダを選択、ファイル名決定

パスが更新された。「作成」をクリックする。
スクリプトを作成

スクリプトワークスペースが開いた。
スクリプトワークスペース

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

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)

スクリプト解説

あとで使う_ready()関数以外の部分を削除しよう。一旦、以下のようになる。

extends RigidBody2D



func _ready():
	pass # Replace with function body.

ボールの速さをball_speedという名前のプロパティで定義する。

# ボールの速さ:1秒間に移動する距離(pixel)
export (float) var ball_speed = 150.0

インスペクタで簡単に編集できるようにexportキーワードを頭につける。(float)はこのプロパティの値が float というデータ型であることを示している。int 型でも大きな問題はないが、今後ボールがだんだん速くなる仕組みを作る時に、より滑らかにスピードアップできた方が良いので float 型にしている。と、散々説明しておいてなんだが、値を150.0と浮動小数で記述していることから、GDScript が自動的に float 型と判断するため、(float)の記述は無くても構わない。

次に_ready()関数を編集していく。まずはpassから始まる行を削除して、別のコードに書き換えていく。

	pass # Replace with function body.

Memo:
funcキーワードで宣言したメソッドのブロック(インデントで区分されたコードのかたまり)には何かコードを記述しないとエラーになります。まだ記述する内容が決まっていない場合に、一旦passと入れておくことで、そのエラーを回避することができます。passにはそれ以上の意味は特にありません。

_ready()関数のブロックに以下のコードを記述する。インデントを忘れないように注意が必要だ。

  # ボールが発射される向き
  var direction = Vector2(1, -1).normalized()

_ready()関数内でプロパティを2つ定義している。

一つ目のプロパティはdirectionだ。これはボールが飛んでいく方向を定義している。Vector2(1, -1)で右上に飛んでいくのはなんとなくおわかりいただけるかと思うが、.normalized()とはなんだろう。

まず、座標上 (0, 0) から (1, -1) までの長さが 1 ではないということを理解するところから始めよう。(1, 0) と (0, -1) はどちらも (0, 0) からの距離が 1 だ。その両方の点と接する円があったとすると、その円周に位置する点は全て (0, 0) からの距離が 1 ということになる。しかし、(1, -1) は、その円より外側にあるので、(0, 0) からの距離は 1 より大きい(厳密には √2 )ということになる。
プロジェクト実行

プロパティdirectionは方向のみを表すプロパティにしたいので、長さを 1 にする必要がある。そこで (1, -1) の方向はそのままで、長さだけちょうど 1 にしてくれる便利な関数がnormalized()なのだ。これは Vector2 クラスでもともと用意されているメソッドなので最初から使える。

次にボールの速度を表すプロパティvelocityだ。単純に先に定義した方向の情報を持たない速さを示すプロパティball_speedと方向を示すdirectionの掛け算で定義している。

  # ボールの速度(方向の情報をもったスピード:方向 x 速さ)
  var velocity = direction * ball_speed

最後に RigidBody2D のメソッドapply_impulse()を使って、ボールに衝撃を与える。

  # 一回だけオブジェクト(ボール)に衝撃を与える
  apply_impulse(Vector2.ZERO, velocity)

これを_ready()関数内に記述したのは、シーンを実行した時にゲームの準備処理段階で1回限りの衝撃をボールに加えてゲーム開始と同時にボールを飛ばすためだ。第一引数の offset はVector2.ZEROで (0, 0) を指定、第二引数 impulse はボールの速度を衝撃として与えるため、先に定義しておいたvelocityを指定している。apply_impulse()メソッドについて詳しくは Godot 公式ドキュメンテーション 参照のこと。

では改めてプロジェクトを実行してみよう。
プロジェクト実行

斜め45°の方向へ真っ直ぐ飛んでいった。
デバッグパネルで確認



おわりに

Part 2 はここまでで終了だ。次回 Part 3 では、ボールが画面外に飛んで行かないように壁を作っていく。