【Godot Engine】FlappyBirdのチュートリアルを作るメモ
Godot Engineで FlappyBirdのチュートリアルを作る過程を残しておきます
2Dシーンを作成して、Mainにリネーム
Ctrl+S
(Cmd+S
) で保存する
以下の背景画像をプロジェクトに登録する ("bg_back.png" とする)
"bg_back.png" をキャンバスにドラッグ&ドロップする
bgノードのインスペクタから Offset > Centered
のチェックを外す(左上揃えにする)
Node2D > Transform > Position > x/y
を (0, 0) に初期化する
実行すると背景画像と画面サイズが合っていないので、メニュー > プロジェクト > プロジェクト設定
を選ぶ
プロジェクト設定から 一般タブ > Display > Window
を選び、Width
を「800」Height
を「480」にすると背景画像が画面に収まるようになる
「Mainシーン」タブのとなりの「+」をクリックして新規シーンを作成
「+その他ノード」を選ぶ
KinematicBody2D
を選んで「作成」ボタンをクリックする
作成したノードを「Player」にリネームして Ctrl+S
(Cmd+S
) で保存しておく
以下の画像を "player.png" として保存して、プロジェクトに追加する
"player.png" をキャンバスにドラッグ&ドロップする
playerノード (Sprite
) のインスペクタから、Animation > Hframes
の値を「2」に変更する
また Node2D > Transform > Position > x/y
の値を (0, 0) にする
すると、プレイヤー画像の1コマ目が原点 (0, 0) に表示されるようになる
KinematicBody2D
には重力はなかった……
重力を持たせたいなら、RigidBody2D
なので、型をRigidBody2D
に変更
単体実行で、重力落下することを確認
当たり判定用にCollisionShape2D
を追加
CollisionShape2Dのインスペクタから、Shape
の値を CircleShape2D
に設定。
コリジョンサイズをマウス操作で広げる
ジャンプ処理を RigidBody2D.add_force()
で実装
extends RigidBody2D
func _process(delta):
if Input.is_action_just_pressed("ui_accept"):
# Spaceキーでジャンプ処理
# 移動量をリセット
linear_velocity = Vector2(0, 0)
# ジャンプ
add_force(Vector2(0, 0), Vector2(0, -100))
しかしコレジャナイ感じに。
こちらのページによると……
・add_force()
Adds a continuous force to the body. Imagine a rocket’s thrust, steadily pushing it faster and faster. Note that this adds to any already existing forces. The force continues to be applied until removed.体に継続的な力を加えます。ロケットの推力を想像してみてください。着実にどんどん速く押していきます。これにより、既存の力が追加されることに注意してください。力は取り除かれるまで加えられ続けます。
add_force()
は継続的な力を加えるとのこと。
どうやら、RigidBody2D.apply_impulse()
が正解のよう
・apply_impulse()
Adds an instantaneous “kick” to the body. Imagine hitting a baseball with a bat.瞬間的な「キック」を体に加えます。バットで野球を打つことを想像してみてください。
ということで以下のように修正
extends RigidBody2D
func _process(delta):
if Input.is_action_just_pressed("ui_accept"):
# Spaceキーでジャンプ処理
# 移動量をリセット
linear_velocity = Vector2(0, 0)
# ジャンプ
- add_force(Vector2(0, 0), Vector2(0, -100))
+ apply_impulse(Vector2(0, 0), Vector2(0, -100))
とりあえず呼び出す関数としてはこれで良さそう
以下の土管画像を "dokan.png" として保存する
パラメータを調整。
RigidBody2D > Gravity Scale
を 「10」に増やす
ジャンプ力を 「-100」から「-400」に変更
extends RigidBody2D
func _process(delta):
if Input.is_action_just_pressed("ui_accept"):
# Spaceキーでジャンプ処理
# 移動量をリセット
linear_velocity = Vector2(0, 0)
# ジャンプ
- apply_impulse(Vector2(0, 0), Vector2(0, -100))
+ apply_impulse(Vector2(0, 0), Vector2(0, -400))
動きが鋭くなりました。
画面外に出ないようにスクリプトを以下のように修正する
extends RigidBody2D
+# デバッグ用フラグ
+var is_debug := true
func _process(delta):
if Input.is_action_just_pressed("ui_accept"):
# Spaceキーでジャンプ処理
# 移動量をリセット
linear_velocity = Vector2(0, 0)
# ジャンプ
apply_impulse(Vector2(0, 0), Vector2(0, -400))
+ if position.y < 0:
+ # 画面外に出ないようにする
+ linear_velocity = Vector2(0, 0)
+ if position.y > 600-64:
+ if is_debug:
+ # 画面外に出ないようにする暫定処理
+ linear_velocity = Vector2(0, 0)
+ # ジャンプ
+ apply_impulse(Vector2(0, 0), Vector2(0, -400))
これで画面外に出なくなる
ちなみにテスト用に移動させる場合は、「移動モード」で移動させる。
「選択モード」で移動させると、スプライトだけが移動してしまう
なので「移動モード」に変更して移動させると、ルートノードごとまとめて移動できる
土管シーンの作成。
タブの「+」をクリックして新規にシーンを作成する
「+その他ノード」を選択する
StaticBody2D
を選んで「作成」ボタンをクリック。
これは、衝突判定はあるが移動しないオブジェクト
"dokan.png" をキャンバスにドラッグ&ドロップする
dokanノード(Sprite
)を選択して、インスペクタから、Node2D > Transform > Position
のところにある回転アイコンをクリックして、Positionの値を (0, 0) にリセットする
CollisionShape2D
を追加
ルートノードの「StaticBody2D」を「Dokan」にリネームしておく
CollisionShape2Dノードを選んで、インスペクタから Shape
を 「RectangleShape2D」に設定する
コリジョンのサイズを画像にあわせる
Dokan に以下のスクリプトをアタッチする
extends StaticBody2D
var velocity = Vector2(-100, 0)
func _process(delta):
position += velocity * delta
Mainシーンに Player.tscn と Dokan.tscn をテスト用に配置してみる
Playerと土管がぶつかることが確認できる
Playerシーンに戻ってアニメーションの実装
Player.gdを以下のように修正する
extends RigidBody2D
# デバッグ用フラグ
var is_debug := true
+# スプライトデータ
+onready var sprite := $player
func _process(delta):
if Input.is_action_just_pressed("ui_accept"):
# Spaceキーでジャンプ処理
# 移動量をリセット
linear_velocity = Vector2(0, 0)
# ジャンプ
apply_impulse(Vector2(0, 0), Vector2(0, -400))
if position.y < 0:
# 画面外に出ないようにする
linear_velocity = Vector2(0, 0)
if position.y > 600-64:
if is_debug:
# 画面外に出ないようにする暫定処理
linear_velocity = Vector2(0, 0)
# ジャンプ
apply_impulse(Vector2(0, 0), Vector2(0, -400))
+ # アニメーション
+ if linear_velocity.y < 0:
+ # 上昇中
+ sprite.frame = 1
+ else:
+ # 下降中
+ sprite.frame = 0
実行すると上昇時に画像が変化する
ゲームオーバーの実装
土管との衝突判定を行う
メニュー > プロジェクト > プロジェクト設定
を開く。
一般タブ > Layer Names > 2d Physics
から、Layer 1
に 「player」、Layer 2
に「dokan」と指定する
いったん、PlayerシーンとDokanシーンを保存して閉じる。
そして開き直す(開き直さないと Layer Nameが反映されないため)
Playerのコリジョンレイヤー、マスクの設定
PlayerシーンのPlayerノードを選択して、インスペクタから PhysicsBody2D > Collision
の Layer
と Mask
を以下のように設定する。
これは、Playerは 「player」レイヤーに所属し、「dokan」レイヤーと衝突する……という設定
Dokanのコリジョンレイヤー、マスクの設定
DokanシーンのDokanノードを選択して、インスペクタから PhysicsBody2D > Collision の Layer と Mask を以下のように設定する。
コリジョンレイヤーとマスクを設定した後、問題なく土管に衝突するかどうかを確認する
土管にぶつかったらプレイヤーを操作できないようにする
Playerノードを選択して、インスペクタから「ノード」タブを選択する
シグナル一覧から body_entered()
を選んで右クリックして「接続」を選ぶ(またはダブルクリック)
シグナルへの接続確認画面が表示されるので、「接続」を選ぶ
Player.gd
にシグナルを接続する関数が追加される
Player.gd
を以下のように修正する (can_jump
変数を追加する)
extends RigidBody2D
# デバッグ用フラグ
var is_debug := true
# スプライトデータ
onready var sprite := $player
+# 操作できるかどうか
+var can_jump = true
func _process(delta):
- if Input.is_action_just_pressed("ui_accept"):
+ if can_jump and Input.is_action_just_pressed("ui_accept"):
# Spaceキーでジャンプ処理
# 移動量をリセット
linear_velocity = Vector2(0, 0)
# ジャンプ
apply_impulse(Vector2(0, 0), Vector2(0, -400))
if position.y < 0:
# 画面外に出ないようにする
linear_velocity = Vector2(0, 0)
if position.y > 600-64:
if is_debug:
# 画面外に出ないようにする暫定処理
linear_velocity = Vector2(0, 0)
# ジャンプ
apply_impulse(Vector2(0, 0), Vector2(0, -400))
# アニメーション
if linear_velocity.y < 0:
# 上昇中
sprite.frame = 1
else:
# 下降中
sprite.frame = 0
+# 土管にぶつかったときの処理
+func _on_Player_body_entered(body):
+ can_jump = false # 操作できなくする
これで衝突時の判定ができる……と思いきやできない。
ここを見ると「"Contact Monitoring" にチェックを入れると動くよ!」と書いてある
ということで、チェックを入れて実行……
しかしシグナルが来ない……
getcollidingbodies()
で衝突している body が取れる……との情報もあったのでためしてみたけれど取れなかった……
仕方ないので、Playerノードの型を RigidBody2D
から KinematicBody2D
に変更……
そして Player.gd を大幅修正……
-extends RigidBody2D
+extends KinematicBody2D
# デバッグ用フラグ
var is_debug := true
+# 重力
+const GRAVITY_POWER := 1000
+# ジャンプ力
+const JUMP_POWER := -400
# スプライトデータ
onready var sprite := $player
# 操作できるかどうか
var can_jump = true
+# 移動速度
+var velocity := Vector2()
func _process(delta):
+ # 重力
+ velocity.y += GRAVITY_POWER * delta
if can_jump and Input.is_action_just_pressed("ui_accept"):
# Spaceキーでジャンプ処理
- # 移動量をリセット
- linear_velocity = Vector2(0, 0)
# ジャンプ
- apply_impulse(Vector2(0, 0), Vector2(0, -400))
+ velocity.y = JUMP_POWER
if position.y < 0:
# 画面外に出ないようにする
- linear_velocity = Vector2(0, 0)
+ velocity.y = 100
if position.y > 600-64:
if is_debug:
# 画面外に出ないようにする暫定処理
- linear_velocity = Vector2(0, 0)
# ジャンプ
- apply_impulse(Vector2(0, 0), Vector2(0, -400))
+ velocity.y = JUMP_POWER
# アニメーション
- if linear_velocity.y < 0:
+ if velocity.y < 0:
# 上昇中
sprite.frame = 1
else:
# 下降中
sprite.frame = 0
+ # 移動と衝突を行う
+ var collision = move_and_collide(velocity * delta)
+ if collision:
+ # 衝突したので動けなくする
+ can_jump = false
+ # 左方向に吹き飛ばす
+ velocity.x -= 300
+ move_and_collide(velocity * delta)
+ if can_jump == false:
+ # 吹っ飛び中は回転する
+ sprite.rotation -= 10 * delta
実行して動作を確認する
上に乗ったときに吹き飛ばない不具合が発生……
衝突時の処理を変更。
# 移動と衝突を行う
var collision = move_and_collide(velocity * delta)
if collision:
# 衝突したので動けなくする
can_jump = false
# 左方向に吹き飛ばす
- velocity.x -= 300
+ velocity.x = -300
+ velocity.y -= 100
move_and_collide(velocity * delta)
逆さ土管に衝突したときの処理がおかしくなるかもしれないけれどひとまずこれで……
土管生成処理の実装
MainシーンからDokanノードを削除
Dokanスクリプト dokan.gd
を修正(出現と消滅処理を追加)
extends StaticBody2D
-var velocity = Vector2(-100, 0)
+var velocity = Vector2(-150, 0)
+# 開始処理
+func start(pos, speed_rate):
+ position = pos
+ velocity *= speed_rate
func _process(delta):
position += velocity * delta
+ if position.x < -128:
+ # 画面外に出たら消える
+ queue_free()
Mainノードに以下のスクリプトをアタッチする
extends Node2D
# 土管オブジェクト
var Dokan = preload("res://Dokan.tscn")
# 出現間隔
var interval = 3
# 生成タイマー
var timer = interval
# 土管出現回数
var dokan_cnt = 0
func _ready():
# 乱数を初期化
randomize()
func _process(delta):
timer += delta
if timer > interval:
# インターバルを超えたら土管を出現させる
timer -= interval
_add_dokan()
func _add_dokan():
# 出現回数をカウントアップ
dokan_cnt += 1
# 高さを決める
var xbase = 800 + 120
var ybase = rand_range(32, 400-32)
# 土管を生成
for i in range(2):
var dokan = Dokan.instance()
var py = ybase
if i == 0:
# 上のドカン
py += -320
else:
# 下のドカン
py += 320 + 160
# 土管の出現回数が増えるとスピードアップ
var speed_rate = 1 + 0.5 * dokan_cnt
dokan.start(Vector2(xbase, py), speed_rate)
add_child(dokan)
# インターバルを減らす
interval = max(0.5, interval-0.2)
ひとまずゲームらしくなりました
まずは Dokan.gd のコード解説
# 開始処理
func start(pos, speed_rate):
position = pos
velocity *= speed_rate
これは開始処理(ドカンを生成したときのパラメータ設定)で、pos
が初期位置、speed_rate
が速度倍率となっています。
func _process(delta):
position += velocity * delta
if position.x < -128:
# 画面外に出たら消える
queue_free()
ドカン画像の横幅がおおよそ 128px としてそれを超えたら消滅処理 queue_free()
を呼び出しています。
Main.gd の土管生成処理について
ドカン画像の高さの半分がおおよそ 320px (正確には 305.5px) なので、その高さだけ上下に移動させて、160px の間隔を開けています。
生成タイミングは、time
変数に delta
を足し込み、interval
を超えたら、生成処理を行っています。
残った問題点として、上の土管に衝突したときの挙動がおかしいのでこれを修正します。
Player.gd
を以下のように修正します
# 移動と衝突を行う
var collision = move_and_collide(velocity * delta)
if collision:
# 衝突したので動けなくする
can_jump = false
# 左方向に吹き飛ばす
velocity.x = -300
- velocity.y -= 100
+ if position.y < collision.position.y:
+ # ドカンより上にプレイヤーがいる
+ velocity.y = -300
+ else:
+ # ドカンより下にプレイヤーがいる
+ velocity.y = 300
move_and_collide(velocity * delta)
これで上のドカンにぶつかっても大丈夫
ゲームーオーバーの文字の描画
標準のフォントは文字が小さいのでここからフォントをダウンロードします。
Xolonium-Regular.ttf
をプロジェクトに追加する
Mainシーンに Label
を追加
名前を 「Caption」に変更
「Caption」ノードから、Control > Custom Fonts > Font > [空]
をクリックして、「読込み」を選択する
対象を「すべてのファイル(*)」に変更して、「Xolonium-Regular.ttf」を選ぶ
これでフォントが読み込める……と思ったけどなぜかエラー
順番が間違っていたようす。
まずは「Caption」ノードから、Control > Custom Fonts > Font > [空]
をクリックして、「新規 DynamicFont」を選択する
作成した Dynamic Font をクリックして、Font > FontData > [空]
をクリックして、「読込み」を選ぶ
すると正しくフォントが選べるようになるので "Xolonium-Regular.ttf" を選択して「開く」ボタンをクリックする
試しに Label > Text
に「GAME OVER」と入力すると、指定したフォントで文字が描画されるようになる
パラメータを以下のように設定します。
-
Rect > Position > (x, y)
: (0, 120) -
Rect > Size > (x, y)
: (800, 256)
-
Custom Fonts > Font > Settings > Size
: 64
これにより以下のようなレイアウトとなる
Player.gd に死亡処理を追加
extends KinematicBody2D
# デバッグ用フラグ
var is_debug := true
# 重力
const GRAVITY_POWER := 1000
# ジャンプ力
const JUMP_POWER := -400
# スプライトデータ
onready var sprite := $player
# 操作できるかどうか
var can_jump = true
# 移動速度
var velocity := Vector2()
func _process(delta):
# 重力
velocity.y += GRAVITY_POWER * delta
if can_jump and Input.is_action_just_pressed("ui_accept"):
# Spaceキーでジャンプ処理
velocity.y = JUMP_POWER
if position.y < 0:
# 画面外に出ないようにする
velocity.y = 100
if position.y > 600-64:
if is_debug:
# 画面外に出ないようにする暫定処理
# ジャンプ
velocity.y = JUMP_POWER
# アニメーション
if velocity.y < 0:
# 上昇中
sprite.frame = 1
else:
# 下降中
sprite.frame = 0
# 移動と衝突を行う
var collision = move_and_collide(velocity * delta)
if collision:
# 衝突したので動けなくする
can_jump = false
# 左方向に吹き飛ばす
velocity.x = -300
if position.y < collision.position.y:
# ドカンより上にプレイヤーがいる
velocity.y = -300
else:
# ドカンより下にプレイヤーがいる
velocity.y = 300
move_and_collide(velocity * delta)
if can_jump == false:
# 吹っ飛び中は回転する
sprite.rotation -= 10 * delta
+ if position.x < 0 or position.y > 480:
+ # 画面外に出たら消滅する
+ queue_free()
Main.gdを以下のように修正
extends Node2D
# 土管オブジェクト
var Dokan = preload("res://Dokan.tscn")
# 出現間隔
var interval = 3
# 生成タイマー
var timer = interval
# 土管出現回数
var dokan_cnt = 0
+# キャプション
+onready var caption = $Caption
+# プレイヤー
+onready var player = $Player
func _ready():
# 乱数を初期化
randomize()
+ # キャプションは初期状態では非表示にしておく
+ caption.visible = false
func _process(delta):
timer += delta
if timer > interval:
# インターバルを超えたら土管を出現させる
timer -= interval
_add_dokan()
+ if is_instance_valid(player) == false:
+ # プレイヤーが消滅した
+ # キャプションを更新
+ caption.visible = true
+ caption.text = "GAME OVER\n\n RETRY: DOWN KEY"
+
+ # 下キーが押されたらリトライ
+ if Input.is_action_just_pressed("ui_down"):
+ # Mainシーンを読み込み直してリトライする
+ get_tree().change_scene("res://Main.tscn")
func _add_dokan():
# 出現回数をカウントアップ
dokan_cnt += 1
# 高さを決める
var xbase = 800 + 120
var ybase = rand_range(32, 400-32)
# 土管を生成
for i in range(2):
var dokan = Dokan.instance()
var py = ybase
if i == 0:
py += -320
else:
py += 320 + 160
# 土管の出現回数が増えるとスピードアップ
var speed_rate = 1 + 0.5 * dokan_cnt
dokan.start(Vector2(xbase, py), speed_rate)
add_child(dokan)
# インターバルを減らす
interval = max(0.5, interval-0.2)
実行してゲームオーバー時の表示とリトライ(下キー)ができることを確認する
キャプション文字の描画順がおかしいので、CanvasLayer
で描画順を制御する
「Caption」ノードをドラッグ&ドロップして、ノードの階層を以下のようにする
「CanvasLayer」ノードを選択して、インスペクタから CanvasLayer > Layer > Layer
の値が「1」になっていることを確認する
実行すると caption
変数で nullエラーとなる
これはノードの階層が変化したため。
Main.gd を以下のように修正
# 土管出現回数
var dokan_cnt = 0
# キャプション
-onready var caption = $Caption
+onready var caption = $CanvasLayer/Caption
# プレイヤー
onready var player = $Player
あとキャプション文字の位置がずれてしまっている
これは CanvasLayer
にぶら下げた影響で Rect > Size
の値が変化してしまっているため
Rect > Size > x
を「800」に戻してレイアウトを修正
実行して「GAME OVER」の文字が最前面に表示されることを確認する
プレイヤー画像を調整
以下の画像を "player2.png" として保存して、プロジェクトに追加する
Playerシーンの "player" ノード (Sprite
) のインスペクタから割り当てる画像を、"player.png" から "player2.png" に変更
↓↓↓
横幅が 256px で 1つあたり 64px なので、
4分割するので、Animation > Hframes
に「4」 を指定する
PlayerスクリプトPlayer.gd
の _process()
を以下のように修正する
func _process(delta):
# 重力
velocity.y += GRAVITY_POWER * delta
if can_jump and Input.is_action_just_pressed("ui_accept"):
# Spaceキーでジャンプ処理
velocity.y = JUMP_POWER
if position.y < 0:
# 画面外に出ないようにする
velocity.y = 100
if position.y > 600-64:
if is_debug:
# 画面外に出ないようにする暫定処理
# ジャンプ
velocity.y = JUMP_POWER
# アニメーション
if velocity.y < 0:
# 上昇中
sprite.frame = 1
else:
# 下降中
sprite.frame = 0
+ if can_jump == false:
+ # ジャンプできない=ダメージ中
+ sprite.frame = 2
# 移動と衝突を行う
var collision = move_and_collide(velocity * delta)
if collision:
# 衝突したので動けなくする
can_jump = false
# 左方向に吹き飛ばす
velocity.x = -300
if position.y < collision.position.y:
# ドカンより上にプレイヤーがいる
velocity.y = -300
else:
# ドカンより下にプレイヤーがいる
velocity.y = 300
move_and_collide(velocity * delta)
if can_jump == false:
# 吹っ飛び中は回転する
sprite.rotation -= 10 * delta
if position.x < 0 or position.y > 480:
# 画面外に出たら消滅する
queue_free()
ゲームオーバー時にダメージ中の画像になることを確認する
ひとまずこれでチュートリアルは完成
これから記事にしていきます。
記事にしました