Part 1 の今回は、ゲームのプレイ画面にパドルを配置して動かすところまでやっていく。
ところで、ゲーム開発初心者が初めて挑戦するゲームにうってつけなのがブロック崩しだ。ブロック崩しはシンプルながらその開発を通して、ゲーム作りの基本をたくさん学習することができるので Godot 初心者やゲーム開発初学者にとてもおすすめだ。習得したゲーム開発の基本スキルは他のジャンルのゲームでもできることばかりだ。
これから数回に分けて、上の GIF ムービーのようなブロック崩しの作り方について解説していく。
Memo:
プロジェクトの新規作成の手順についてはこちらの記事をご参照ください。
Godot のプロジェクトマネージャの操作
画面サイズを設定する
まずはプロジェクトを新規作成しよう。
下準備として、ゲーム画面のサイズを指定していく。今回、スプライト用の画像のサイズが小さいため、それに合わせて少し小さめで指定する。
「プロジェクト」メニュー >「プロジェクト設定」を選択する。
「一般」タブ > サイドバー「Display」カテゴリ内の「Window」を選択する。
「Size」セクションの「Width」、「Height」がそれぞれ画面の幅と高さだ。やや小さめにしたいので、デフォルトサイズの半分として、Width: 512、Height: 300 に設定する。また、テストプレイしてデバッグする時のディスプレイサイズは2倍の大きさにしたいので、Test Width: 1024、Test Height: 600に設定する。
続けて、さらに下の方にスクロールすると「Strech」セクションが表示される。ここでは「Mode」を「2d」に「Aspect」を「keep」に変更する。
最後に一番下の「閉じる」をクリックしたら画面サイズの編集完了だ。
アセットを用意する
ブロック崩しで必要になるメインのオブジェクトは以下の3つになる。
- パドル(プレイヤー)
- ボール
- ブロック
これらに使用する画像を用意した。こちらの Dropbox の共有フォルダ から「sprites」フォルダを丸ごとダウンロードしてほしい。
画像ファイルをファイルシステムへインポートする
ダウンロードしたら、それを Godot の「ファイルシステム」へドラッグ&ドロップしよう。
するとファイルシステム内に「Sprites」フォルダにまとめられた状態のままで画像ファイルが表示される。
ドット絵をシャープに表示する
今回インポートした画像のようにサイズの小さなドット絵の場合は、デフォルトの設定により、ぼやけた感じで画面上に表示されてしまうため、これをドット絵特有のシャープな表示に変更しよう。
まずファイルシステム上で先ほどインポートした画像ファイルを全て選択する。
次に「インポート」タブを選択し、「プリセット」をクリックする。
そこで「2D Pixel」を選択する。
最後に「再インポート」をクリックすればキレイなドット絵表示に切り替わる。
プロジェクトのアイコンを変更する
ついでに、プロジェクトのアイコン画像も用意しておいたので、デフォルトの Godot ロゴの画像を上書きしよう。ファイルシステムにコピーした「Sprites」フォルダ内の「Icon.png」を「Sprites」フォルダの外(つまりルートフォルダ「res://」直下)に移動する。
同じ名前のファイルを上書きして良いか確認するダイアログが表示されるので「上書き」を選択する。
本当にプロジェクトのアイコンが変更されたか、念のため確認しておこう。
「プロジェクト」メニュー>「終了してプロジェクト一覧を開く」を選択する。
確認のダイアログが表示されたら「はい」を選択する。
プロジェクトのアイコン画像がきちんと変更されているのが確認できた。
そのままプロジェクトマネージャー上で「Breakout」プロジェクトをダブルクリックし、ブロック崩しのプロジェクトに戻ろう。
シーンを作る
いよいよシーンを作成していく。シーンというのは、1つ以上のノードで構成されたゲームの構成要素の単位だ。例えば、オーソドックスなRPGだったら、キャラクターのシーン、モンスターのシーン、バトルのシーン、ワールドマップのシーン、町のシーン、コンフィグ画面のシーン、などのシーンを別々用意しておき、それらを必要に応じて画面に表示させたり引っ込めたりするわけだ。
シーンには一つ以上のノードが必要で、シーン内に最初に追加するノードが自動的にルートノードになる(あとで変更もできる)。ノードというのは、その一つ一つがシーンの構成要素であり、基本的にそれぞれが特有の機能を持っている。例えば、2D の「Sprite」ノードなら、テクスチャ画像を設定して画面上に表示されるキャラクターやアイテムを表現できるし、Label ノードなら テキストを設定してその文字列を画面に表示することができる。数種類ある Collision系のノードは、衝突判定を自動的に行ってくれる。
このように、特徴的な機能をもつノードをうまく組み合わせてシーンを作り、そのようなシーンをまた複数組み合わせることでゲーム全体を作っていく。ちょうど枝分かれしている木のような構成だ。実際に Godot でもゲームを構成するシーンをまとめてシーンツリーと呼ぶ。これが Godot でのゲーム作りの基本だ。
さて、ここでは 2D のブロック崩しを作るので、シーンドックの「ルートノードを生成」の選択肢から「2D シーン」を選択する。
すると、自動的に「Node2D」という名前のノードが生成される。これがルートノードになる。右クリックして「名前の変更」をクリックして名前を変更しておこう(シーンドック上で「Node2D」ノードを選択してクリックまたはEnterキーでも構わない)。
今回は「Game」という名前にした。
プレイヤーのパドルを作る
「Game」ノードを選択した状態で「+」ボタンをクリックして、「Game」ノードに別のノード(子ノード)を追加する。ショートカットキー操作も便利だ(ノードを追加 Windows: ctrl + A / macOS: Cmd + A)。
「Node を新規作成」のパネルが開いたら「kinematic」と検索しよう。検索結果の中に「KinematicBody2D」が見つかったらそれを選択して「作成」をクリックする。
これで「Game」ノードの子ノードとして「KinematicBody2D」ノードが追加された。
![added KinematicBody2D(/images/tutorials/gd0004_breakout/breakout_1/img019.png)
「KinematicBody2D」ノードの名前も変更しておこう。名前は「Paddle」とする。このように、主要なノードをシーンツリーに追加したら、早めに名前を変更しておく癖をつけていこう。そうすればプロジェクトが大きくなっても同じ名前のノードがたくさんあって困るということがない。
今度は「Paddle」ノードに子ノードを追加する。「Game」ノードから見たら孫ノードだ。「Paddle」を選択して「+」ボタンをクリックする。もちろんショートカットキー操作でも構わない。
「sprite」と検索し、検索結果のうち「Sprite」を選択して「作成」をクリックする。
「Paddle」ノードに「Sprite」ノードが追加された。「Sprite」はゲーム画面に画像を表示するノードだ。プレイヤーキャラクターや敵キャラクター、背景のオブジェクトやアイテムなどなど、様々な場面で使用される。
シーンドック上で「Sprite」ノードを選択した状態で、インスペクタドックを見てほしい。現在シーンドック上で選択されている「Sprite」ノードのプロパティが表示されている。今、一番上の「Texture」というプロパティは空になっている。このプロパティに画像ファイルを指定することでゲーム画面に画像が表示されるという仕組みだ。
では「Texture」プロパティめがけて「ファイルシステム」から先に用意しておいた「Sprites」フォルダ内の「Paddle1.png」という画像ファイルをドラッグ&ドロップしよう。
Textureに画像が反映した。
画面中央にある 2D ワークスペースのちょうど赤と緑の線が交わる箇所に表示されている(小さく表示されてわかりにくい場合は拡大しよう)。この位置が x, y 座標の (0, 0) に当たる。
同様に「Paddle」ノードにもう一つ別の子ノードを追加する。
今度は「collisions」と検索し、「CollisionShape2D」を選択して「作成」をクリックしよう。似たような名前の「CollisionPolygon2D」の方ではないので要注意だ。
これで「Paddle」に2つ目の子ノード「CollisionShape2D」が追加された。「CollisionShape2D」は2Dノードのいわゆる当たり判定用のノードだ。例えば、一般的なプラットフォーマー(横スクロールアクション)ゲームで、プレイヤーキャラクターが敵キャラクターや地面、壁、アイテムなどにぶつかったかどうかを判定するために使う。
シーンドック上で「CollisionShape2D」を選択した状態で、インスペクタを見てほしい。一番上の「Shape」プロパティが [空] になっている。[空] と表示されている箇所をクリックして形を指定していく。
形をSpriteの画像に合わせたいので、オプションの中から「新規RectangleShape2D」(Rectangle: 長方形)を選択する。
設定された。
2D ワークスペースを見ると、そこに「Sprite」の画像に重なった 半透明の青緑色をした四角形がある。これが「CollisionShape2D」の形だ。今の時点ではまだ「Sprite」ノードの「Texture」プロパティの画像の形と「CollisionShape2D」ノードの「Shape」プロパティの形が一致していない。今からこれを一致させていく。
形を調整する前に少し下準備をする。2D ワークスペース上部のツールバーを見てほしい。スナッピングオプション(3つの点が縦に並んだアイコン)を見つけたらそれをクリックする。
表示されたオプションのうち「ピクセルスナップを使用」にチェックを入れて閉じる。
赤丸で囲んだ丸い点をドラッグして、形を「Sprite」ノードの「Texture」に合わせよう。先の設定によって、1 ピクセルずつスナップされるので簡単に調整できるはずだ。
形が一致したら調整完了だ。
それではここで一度ゲーム画面を表示してみましょう。エディタ右上のアイコンのうち、「シーンを実行」アイコンをクリックしよう。ショートカットキー操作でも構わない(シーンを実行 Windows: F6 / macOS: Cmd + R)。
すると、今回のように初めてシーンを実行する時にまだシーンが保存されてなければ「実行前にシーンを保存」パネルが表示される。今回はファイルを整理するためにシーン用のフォルダを作成する。右上の「フォルダを作成」をクリックする。
フォルダ名を入力する。今回は「scene」とする。
パネル上部の保存先のフォルダパスが間違いないことを確認する。
ファイル名を確認して(今回はそのまま Game.tscn )問題なければ「保存」をクリックする。
デバッグパネルが開いた。よく見ると「Paddle」ノードが左上の角にあるのがわかる。これは「Paddle」ノードの位置が(0, 0) にあるためだ。ゲーム画面の x, y 座標は左上の角が (0, 0) で右に行くと x 座標が上がり、下に行くと y 座標が上がる。学校などで習う x, y 座標と違い、ゲーム画面では y 座標は下に行くほど大きな値になる。このことについて慣れるまで違和感があるかもしれませんが、大事なポイントなので覚えておこう。
では「Paddle」ノードの初期位置を変更していこう。まず、手動でドラッグする可能性を考慮して、「Paddle」ノードの子ノードを選択不可する。選択不可にするには、シーンドックで「Paddle」ノードを選択した状態で、2D ワークスペース上部のツールバーにある「オブジェクトの子を選択不可にする」アイコンをクリックする。
例えば「Paddle」ノードを移動させようと思った時に、過って子ノードの「Sprite」だけ移動させてしまいそうになっても、選択不可にしておけば、子ノードを動かしてしまう心配はなく、常に親ノードと子ノードの位置関係が変わらない状態をキープできる。
ツールバーの「オブジェクトを選択不可にする」アイコンをクリックすると、シーンドックの「Paddle」ノードの右側に、ツールバーと同じアイコンが表示される。
手動で 2D ワークスペース内を移動させても良いのだが、今回はきっちり中央下よりに配置するため、インスペクタで位置を変更する。シーンドックで「Paddle」ノードを選択した状態で、インスペクタから「Transform」プロパティをクリックして開く。一番上の「Position」プロパティの (x, y) の値を (256, 256) とする。
再度シーンを実行してみよう。
今度はデバッグパネルの画面中央下寄りの位置に「Paddle」が表示された。
パドルを動かす
ようやくゲーム画面に「Paddle」ノードが表示されたが、まだ動かせない。ここから少しばかりプログラミングをして「Paddle」ノードを動かせるようにしていく。
インプットマップにアクションを追加する
Paddle を操作するために必要なキー操作をあらかじめ用意しておく。
「インプットマップ」タブを開こう。ここにはデフォルトで登録済みのインプット操作の項目がすでに表示されている。だが、今回は敢えて、カスタマイズした操作を登録することにする。
パネル上部の「アクション」に操作の名前を入力する。「move_right」と入力して「追加」をクリックする。
すると、登録された「move_right」が一番下に表示される。その右側にある「+」ボタンをクリックして「キー」を選択する。
パネル中央に、キー操作を受け付けてくれるウインドウが表示される。この状態で、キーボードのいずれかのキーを押下すると、そのキーの名前が表示される。ここではキーボードの右方向キーを押下する。そのまま「OK」をクリックする。間違って「Enter」キーを押さないようにしよう。押すと「Enter」キーが登録されてしまうからだ。
同様に「move_left」もキーボードの左方向キーを割り当てて登録ておこう。
スクリプトをアタッチする
プログラミングを始める前の下準備として、スクリプトを「Paddle」ノードにアタッチ(添付)する。
シーンドックで「Paddle」ノード選択してから、右上の「スクリプト」アイコンをクリックしよう。
「ノードにスクリプトをアタッチする」パネルが表示されたら「パス」の項目右側のフォルダアイコンをクリックする。
「スクリプトを開く / 場所を選択する」パネルが出るので、上部「パス」がルート「res://」になっている状態で、右上の「フォルダを作成」をクリックする。もし「res://scene」になっていたら、パネル左上の「↑」アイコンをクリックして一階層上の「res://」に移動できる。
スクリプト用のフォルダを用意したいので、「scripts」と入力して「OK」をクリックする。
パネル上部で、パスが「scripts」フォルダのパスになっていることを確認する。
パネル下部で、ファイル名はそのまま「Paddle.gd」として「開く」をクリックする。
「ノードにスクリプトをアタッチする」パネルに戻り、パスが更新されたことを確認できたら「作成」をクリックする。
スクリプトをコーディングする
スクリプトをアタッチすると、Script ワークスペースに切り替わる。現在、デフォルトだでにコードが少し記述されている状態だが、このスクリプトでは一番上のextends KinematicBody2D
以外は不要なので、全て選択して削除しよう。
シーンドックには「Paddle」ノードにスクリプトがアタッチされていることがスクリプトアイコンでわかるようになっている。
このスクリプトに記述する GDScript によるコードは以下になる。プログラミングがよくわからない人は、一旦丸ごとスクリプトワークスペースにコピー&ペーストしてほしい。
extends KinematicBody2D
# パドルの速さ:1秒あたりの移動距離(pixel)
export (int) var speed = 150
func _process(delta):
# 毎フレーム、パドルの向きを(0, 0)にリセットする
var direction = Vector2.ZERO
# キーボードで右方向キーを押している場合は、パドルの向きを(1, 0)に更新する
if Input.is_action_pressed("move_right"):
direction.x = 1
# キーボードで左方向キーを押している場合は、パドルの向きを(-1, 0)に更新する
if Input.is_action_pressed("move_left"):
direction.x = -1
# position(パドルの位置)に移動距離(速さ:speed x 向き:direction x 時間:delta)を加算
position += speed * direction * delta
ここからスクリプトの内容について少し説明していく。プログラミングが苦手な人は一旦スキップしてもらって構わない。
スクリプト解説
まずは最初の1行目から。
extends KinematicBody2D
これはスクリプトファイルがアタッチされている「Paddle」ノードのクラスである KinematicBody2D を拡張するために必要なコードになる。「KinematicBody2D」というクラスはもともと用意されているが、それをさらに拡張させることによって「Paddle」ノードを開発者が思った通りに動かせるようにしていくわけだ。
次にspeed
というプロパティだ。
Memo:
プログラミングの世界ではクラスの中の変数をプロパティといいます。
# パドルの速さ:1秒あたりの移動距離(pixel)
export (int) var speed = 150
最初に#
から始まる行はコメントだ。#
で始まる行は、プログラム実行時には直接関係ないものとしてスキップされるため、これを利用してスクリプト上にメモを残すことができる。
次の行を見てみよう。最初に出てくるexport (int)
という記述についてはあとで説明する。まずはvar
というキーワードだが、これを頭につけるとプロパティを定義できる(クラス内の変数をプロパティと言いる)。var
に続けて、自分でプロパティの名前を決めて記述する。ここではspeed
という名前の変数にしている。そして=
のあとにプロパティに代入したい値を記述する。ここでは150
と記述している。このプロパティで、パドルの移動速度(秒速)を設定している。つまり、毎秒 150 ピクセルの速さで移動するという内容になる。
さて、冒頭のexport
のキーワードだが、これをつけると、インスペクタドックでスクリプトがアタッチされたノード(ここでは「Paddle」ノード)のプロパティとして表示されるようになる。今は150と指定しているが、テストしてみたら150だと遅すぎる(または速すぎる)、となった場合にインスペクタの方で気軽にプロパティの値を修正できるのだ。
そのあと(int)
と記述しているが、これはデータタイプを指定している。int は整数を表すデータタイプ(データ型)で、同じ数値でも float という浮動小数点(例えば、1.3141、256.159など)という別のデータタイプもあって、それを明確に区別するために記述している。ただし、GDScriptは動的なプログラミング言語なので、変数に代入された値によってデータタイプを自動判別してくれる。つまり、今回のコードの場合は記述する必要は特にない。
次は_process()
という関数だ。
func _process(delta):
まず GDScript ではfunc
というキーワードを頭につけることでメソッドを定義できる(クラス内の関数をメソッドと言う)。そのあと続けてメソッドの名前を記述する。ここでは_process
だ。さらにそのあと()
で囲った中に引数を入力する。引数というのは、メソッドを使うときに任意で指定できる値で、この引数の値の違いによって、毎回メソッドの実行結果が変わってきます。例えるなら、ジュースを作るミキサーという機械がメソッドで、そこに入れる食材が引数だ。フルーツや野菜など入れる食材によって、出来上がるジュースが変わるのと同じだ。
ただし、今回定義しているこの_process
という関数は実はもともと KinematicBody2D クラスに用意されているメソッドなのだ。先にextends
キーワードでこのクラスを拡張することを宣言しているので、もともと用意されているメソッドに内容を追加したりして、再定義しているわけだ。
この_process
関数は、毎フレーム(デフォルトでは1秒間に60フレーム)読み込まれて、その内容が実行される。引数のdelta
はもともと指定されている引数で、一つ前のフレームにかかった時間を保持している。ゲームというのはその時々の処理の重さなどによって、毎フレームきっちり1/60秒の長さにはならないようだ。そこで、先に定義したパドルの速さにこのdelta
を掛け合わせることで、一定の速度でパドルを動かすことが可能になる。
ではここから_process()
関数の中をみていくが、GDScript ではメソッドブロック(メソッドの中身)を記述する際は、どこまでが中身なのかがわかるように、インデントして(ずらして)記述していく。このインデントがないとエラーになるので注意ほしい。
# 毎フレーム、パドルの向きを(0, 0)にリセットする
var direction = Vector2.ZERO
パドルの移動する方向を代入するdirection
という名前のプロパティにしている。ひとまず初期値としてVector2.ZERO
を代入した。GDScriptには Vector2 というデータタイプ(データ型)が用意されていて、これがいわゆるベクトル座標(x, y)形式のデータになる。 Vector2(x, y) という形で記述する。今回記述しているVector2.ZERO
は Vector2 というクラスの ZERO というプロパティを表している。ちょっと特別な書き方だが、Vector2(0, 0)
と同じ意味になる。他にも以下のように「長さが 1」で、方向だけを表す際に使用しやすいプロパティが Vector2 クラスには用意されている。
Vector2.UP
:Vector2(0, -1)
と同じ *ゲーム画面では下にいくほど y 座標が上がるVector2.DOWN
:Vector2(0, 1)
と同じ *ゲーム画面では上にいくほど y 座標が下がるVector2.RIGHT
:Vector2(1, 0)
と同じVector2.LEFT
:Vector2(-1, 0)
と同じ
話が少し逸れたが、毎フレームでプロパティdirection
をパドルの方向をどちらにも向いていない (0, 0) にリセットすることで、プレイヤーが操作をしていない時はきちんとパドルが止まるようするのが狙いだ。
# キーボードで右方向キーを押している場合は、パドルの向きを(1, 0)に更新する
if Input.is_action_pressed("move_right"):
direction.x = 1
# キーボードで左方向キーを押している場合は、パドルの向きを(-1, 0)に更新する
if Input.is_action_pressed("move_left"):
direction.x = -1
Input
というのはクラスの名前で、このクラスには様々な入力操作を扱うプロパティやメソッドが用意されている。is_action_pressed()
というメソッドもその一つで、引数に先に登録しておいた(またはデフォルトで登録されている)インプットマップの名前を入力することで、そのキーを押しているかどうかの値を返してくれる。ちなみにこの真(当てはまる)か偽(当てはまらない)いずれかの値しか取らないデータタイプを bool という。bool は boolean の略だ。つまり、このメソッドは bool 型のデータを結果として返してくれるのだ。
この bool 型はよく if 構文で利用される。例に漏れず、このスクリプトでもそのような記述をしている。なお、GDScript では if 構文もメソッドと同様にその中身を記述するときはインデントが必要なので注意してほしい。
インプットマップ「move_right」が押されている場合、つまり、プレイヤーが右方向キーを押している時は、direction.x = 1
が読み込まれる。direction
は先に定義しておいた Vector2 型のプロパティだった。.x
がついているので、これはdirection
プロパティの x 座標のことを指している。その x 座標に1
を代入せよ、という意味になる。 direction = Vector2(1, 0)
と記述するのと同じ意味になる。どちらでも構わない。インプットマップ「move_left」も同様だ。
# position(パドルの位置)に移動距離(速さ:speed x 向き:direction x 時間:delta)を加算
position += speed * direction * delta
position
というプロパティが急に出現している。変数(プロパティ)を定義するにはvar
というキーワードが必要なのに、これはなぜだろうか。答えは、もともと KinematicBody2D クラスでこのposition
プロパティが定義済みだからだ。extends
で拡張しているので、もともと定義されているプロパティはそのまま使えます。そして、このposition
は Vector2 型のプロパティで、ゲーム画面上のそのノードの位置を表する。
+=
という記述は、その変数に対して、今その変数に代入されている値に+
より後の値を足す、という意味になる。これはposition = position + (speed * direction * delta)
と記述するのと同じ意味になる。つまり、略記だ。そして移動距離を「速さ×方向×時間」で表すことができるので、speed * direction * delta
となる。
パドルを動かす
それでは、最後にパドルが実際に動くか見ていく。さっきは「シーンを実行」でデバッグパネルを起動したが、今度は「実行」ボタンでプロジェクトを実行してみよう(とはいえ、今は一つのシーンしかないので出力される画面は同じだ)。ショートカットキー操作も便利だ(実行 Windows: F5 / macOS: Cmd + B)。
すると、まだプロジェクトのメインシーンが設定されていないので、以下のダイアログが表示される。「選択」をクリックしよう。
今のところ唯一作成済みのシーン「Game.tscn」ファイルを選択して「開く」をクリックする。
デバッグパネルが開いたら、キーボードの左右の方向キーで動かしてみよう。思った通りに動いただろうか。もしパドルのスピードが気に入らない時は、シーンドックで「Paddle」ノードを選択してから、インスペクタで「speed」プロパティの値を変えて調整してみてほしい。
おわりに
以上で Part 1 は終了だ。次回 Part 2 は、ボールのオブジェクトを作ってそれをパドルから発射するところまでやっていく。