この記事ではフラッピーバードを作るチュートリアルを紹介します。
プロジェクトの作成
まずはプロジェクトを作成します。
ここでは「FlappyBird」という名前にしました。 まずはゲームのメインとなるシーンを作成します。
「2Dシーン」クリックします。 2Dシーンが作成されるので、ノード名を「Main」に変更して Ctrl+S (Cmd+S) でシーンを保存します (そのまま "Main.tscn" として保存)
保存したら、F5 (Cmd+B) または右上の「▶」をクリックしてゲームを実行します。
メインシーンが設定されていないの確認ダイアログが表示されます。「現在のものを選択」をクリックします。

するとゲームが実行できます。(何も表示されていないウィンドウが表示されます)
なお4Kモニターでは実行ウィンドウが小さくなりますが、プロジェクト設定の「表示 > ウィンドウ」の "hiDPIを許可" のチェックを外すと4Kの解像度を考慮しない大きさとなります。(※注意点:高度な設定として用意されている項目なので、検索欄に "hidpi" と入力しないと表示されません)
素材の追加
素材データは以下からダウンロードできます。
素材を追加するフォルダとして「assets」を作成します。
ここにダウンロードした素材をドラッグ&ドロップして追加します。

プロジェクトに追加した "bg_back.png" をキャンバスにドラッグ&ドロップします。

"bg_back" がスプライト (BgBackとして、Mainシーンに追加されました。
Spriteの位置を調整する
キャンバスで位置を調整しても良いですが、インスペクタから行うと、細かい調整が可能となります。 まずは、"bg_back" ノードを選択します。
そしてインスペクタから、Node2D > Transform > Position のところにある「回転アイコン」をクリックして値をリセット。
位置は (0, 0) となるのですが、左上の位置をはみ出してしまうので、Offset > Centered のチェックを外すと、左上基準の座標となります。
これで位置を調整できたのですが、実行するとウィンドウサイズと背景画像のサイズが合っていません。
ウィンドウサイズの調整
ウィンドウサイズを変更するには、メニューから プロジェクト > プロジェクト設定 を選びます。
するとプロジェクト設定が表示されるので、「一般」タブから Display (表示) > Window (ウィンドウ) を選び、Size (サイズ) の値を Width (ビューポートの幅) を「800」 Height (ビューポートの高さ)を「480」に変更します。

実行すると背景画像がぴったり収まります。
プレイヤーを作成する
背景が表示できたので、次はプレイヤーを作成します。 Mainシーンのとなりにある「+」をクリックします。
空のシーンが作られるので「+その他のノード」を選びます。
"characterbody" で検索して、`CharacterBody2D" を作成します。
CharacterBody2Dノードが作成されるので、ノードの名前を "Player" に変更します。
名前を変更したら、Ctrl+S (Cmd+S) でシーンを保存します。
名前は「Player.tscn」としました。
プレイヤー画像の登録
プレイヤー画像は "player.png" です。
この画像は、スプライトシートと呼ばれる 等間隔でパターンアニメーションとなる画像です。
これをキャンバスにドラッグ&ドロップします。

Playerという名前で Spriteノードが作成されます。
"Player" だとルートノードの "Player" と名前が同じので、わかりやすいように 「Sprite」にリネームしておきます。
インスペクタから Sprite2D > Animation > Hframes の値を「4」に変更します。
プレイヤー画像が正しく表示されました。
これはHframes を横方向の画像のパターン数を正しい値「4」に指定したためとなります。
そして`Frame{ の値を切り替えると、パターンの変化を確認できます。

コリジョンの設定
Playerノードを右クリックして、「+子ノードを追加」を選びます。
CollisionShape2Dを選んで「作成」ボタンをクリックします。
インスペクタから CollisionShape2D > Shape > [空] をクリックします。
すると割り当てる Shape を選択するドロップダウンが表示されるので、「新規 CircleShape2D」を選びます。
キャンバスの原点に円形のコリジョンが追加されました。
ここでプレイヤーの位置が原点にない場合は、Spriteノードを選択して、Node2D > Transform > Position にある回転アイコンをクリックして、位置をリセットして原点(0, 0)に移動させます。
SpriteとCollisionShape2Dの中心が一緒になりました。
CollsionShape2Dノードを選択して、薄い円をドラッグして、Spriteを覆うように広げます。
ジャンプの実装
プレイヤーのジャンプ処理を実装します。 Playerノードを選択し、スクリプト追加アイコンをクリックします。
"作成" をクリックしてスクリプトを再生します。
Playerノードに Player.gd スクリプトがアタッチされるので、以下のように記述します。
extends CharacterBody2D # 重力 const GRAVITY_POWER := 1000 # ジャンプ力 const JUMP_POWER := -400 func _process(delta): # 重力を加算 velocity.y += GRAVITY_POWER * delta if Input.is_action_just_pressed("ui_accept"): # Spaceキーでジャンプ処理 velocity.y = JUMP_POWER # 移動処理を行う move_and_collide(velocity * delta)
記述できたら、右上の「シーンを実行」アイコンをクリックします。(Playerノードのみに動作を確認したいのでF5ではなくシーン実行を行います)
実行すると、画面の左端からプレイヤーが落下して画面外に消えていきます。
これは出現位置を画面の左上(原点 (0, 0)) にしているためとなります。 これだと確認がしづらいので、プレイヤーを移動させます。 上の方にある「2Dモード」に切り替えるボタンをクリックします。
これでプレイヤーの位置を動かせる……と思いきや、ドラッグすると、子ノードの CollisionShape2D を選択してしまいます。

これはデフォルトではキャンバスのマウス操作は「子ノードを優先して選択」するためとなります。 子ノードを選ばないようにするためには、[SHIFT] キーを押しながらすべてのノードを選択します。
これで、Playerノードをまとめて移動させることができます。
Spaceキー でジャンプできることを確認します。
画面外に出ないようにする
プレイヤーを画面外に出ないようにします。 スクリプトを編集するために、スクリプトモードへ表示を切り替えます。 2Dモードからスクリプトモードへの変更方法は以下の2つです。
スクリプトを開いたら、Player.gd を以下のように修正します。
extends CharacterBody2D # 重力 const GRAVITY_POWER := 1000 # ジャンプ力 const JUMP_POWER := -400 func _process(delta): # 重力を加算 velocity.y += GRAVITY_POWER * delta if Input.is_action_just_pressed("ui_accept"): # Spaceキーでジャンプ処理 velocity.y = JUMP_POWER + if position.y < 0: + # 画面上にはみ出さないようにする + velocity.y = 100 + + if position.y > 600-64: + # 画面下に落ちないようにオートジャンプ + velocity.y = JUMP_POWER # 移動処理を行う move_and_collide(velocity * delta)
"+" 部分が追加部分ですが、コピーする場合は"+" の文字を消します。 では実行して、画面の上下からはみ出ないことを確認します。
土管シーンの作成
障害物となる土管シーンを作成します。 Playerシーンのタブのとなりにある「+」をクリックします。
新しいシーンが作成されるので、「+その他のノード」をクリックします。
型は StaticBody2D を選びます。
作成したらノードの名前を「Dokan」に変更します。
変更したら Ctrl+S (Cmd+S) で土管シーンを保存しておきます。
土管スプライトの追加
"dokan.png" をキャンバスにドラッグ&ドロップします。

スプライトノードの名前を "Sprite" にしておきます。
インスペクタから位置をリセットしておきます。
土管コリジョンの作成
ルートノードの「Dokan」を右クリックして「+子ノードを追加」をクリック。
CollisionShape2Dを作成。
インスペクタから CollisionShape2D > Shape > [空]をクリックして、「新規 RectangleShape2D」を選びます。
コリジョンサイズを調整
コリジョンの角にあるハンドルをドラッグして、コリジョンが土管を覆うように広げます。

土管を動かす
プレイヤーとの衝突をテストするために、土管を動かすスクリプトを実装します。 「Dokan」ルートノードを選択して、スクリプトアイコンをクリックします。
スクリプトアタッチのダイアログが表示されるので、そのまま「作成」をクリックします。
Dokan.gdスクリプトには以下のように記述します。
extends StaticBody2D # 移動速度 var velocity = Vector2(-100, 0) func _process(delta) -> void: # 位置に速度を足し込む position += velocity * delta
Mainシーンに再び戻る
土管の動作を確認する
Ctrl+S (Cmd+S) で変更を保存して、「Main」シーンのタブに切り替えます。
「2D」をクリックして2Dモードに切り替えます。
Mainシーンのキャンバスに「Dokan.tscn」をMainシーンにドラッグ&ドロップします。

もし以下のように "bg_back" ノードの下に配置される場合は、
"Dokan"ノードをドラッグ&ドロップして "Main"ノードの直下になるように移動しておきます。

左上の「▶」をクリック (F5or Cmd+B) して実行してみます。
すると土管が右から左に移動するのが確認できます。

プレイヤーとの当たり判定を確認する
次にプレイヤーを Mainシーンに配置します。

テスト用にプレイヤーの位置を原点からずらしているので、少し斜め上に配置すると良い位置になります。 実行して当たり判定を確認します。
するとプレイヤーが土管に刺さるような挙動となります。
これは土管にぶつかったときにプレイヤーは特に衝突の反動を入れていないためです。
CharacterBody2Dを使った場合は、衝突時の処理を自分で作る必要があります。
プレイヤーに衝突の反動を入れる
Player.gd を開いて以下のように修正します。
extends CharacterBody2D # 重力 const GRAVITY_POWER := 1000 # ジャンプ力 const JUMP_POWER := -400 + # ジャンプできるかどうか + var _can_jump := true + # スプライト + @onready var _sprite = $Sprite func _process(delta): # 重力を加算 velocity.y += GRAVITY_POWER * delta - if Input.is_action_just_pressed("ui_accept"): + 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: # 画面下に落ちないようにオートジャンプ velocity.y = JUMP_POWER - # 移動処理を行う - move_and_collide(velocity * delta) + # 移動と衝突処理を行う + 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
行頭に「-」がついている行が削除する行で、「+」が追加する行となります。
_can_jump変数を追加して、この変数がtrueの場合のみジャンプできるようにしました。
実行して動作を確認します。
プレイヤースクリプトをリファクタリング
Player.gd の _process() が長くなりすぎたので、関数を短くします。
extends CharacterBody2D # 重力 const GRAVITY_POWER := 1000 # ジャンプ力 const JUMP_POWER := -400 # ジャンプできるかどうか var _can_jump := true # スプライト @onready var _sprite = $Sprite 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: - # 画面下に落ちないようにオートジャンプ - velocity.y = JUMP_POWER - # 移動と衝突処理を行う - 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 + # 移動処理を行う + _update_moving(delta) + + # 衝突処理 + _update_collision(delta) + + # アニメーションの更新 + _update_anim(delta) +# 移動処理を行う +func _update_moving(delta:float) -> void: + # 重力を加算 + 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: + # 画面下に落ちないようにオートジャンプ + velocity.y = JUMP_POWER +# 衝突処理を行う +func _update_collision(delta:float) -> void: + # 移動と衝突処理を行う + var collision = move_and_collide(velocity * delta) + if collision: + # 衝突したのでジャンプできなくする + _can_jump = false + # 左方向に吹き飛ばす + velocity.x -= 300 + # さらに移動量を加える. + move_and_collide(velocity * delta) + +# アニメーションの更新 +func _update_anim(delta:float) -> void: + if _can_jump == false: + # 吹っ飛び中は回転する. + _sprite.rotation -= 10 * delta
移動処理を _update_moving() 、衝突処理を_update_collision()、アニメーションの更新を _update_anim()としました。
正確には move_and_collide() は移動と衝突を同時に行っているのですが、ひとまずこのようにしています。
実行して、修正前と同じ挙動を取るかどうか確認します。
Player.gd のすべてのコード
差分ファイルとしているのが逆にわかりにくいかもしれないので、ここまでの Player.gd をのせておきます。
extends CharacterBody2D # 重力 const GRAVITY_POWER := 1000 # ジャンプ力 const JUMP_POWER := -400 # ジャンプできるかどうか var _can_jump := true # スプライト @onready var _sprite = $Sprite func _process(delta) -> void: # 移動処理を行う _update_moving(delta) # 衝突処理 _update_collision(delta) # アニメーションの更新 _update_anim(delta) # 移動処理を行う func _update_moving(delta:float) -> void: # 重力を加算 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: # 画面下に落ちないようにオートジャンプ velocity.y = JUMP_POWER # 衝突処理を行う func _update_collision(delta:float) -> void: # 移動と衝突処理を行う var collision = move_and_collide(velocity * delta) if collision: # 衝突したのでジャンプできなくする _can_jump = false # 左方向に吹き飛ばす velocity.x -= 300 # さらに移動量を加える. move_and_collide(velocity * delta) # アニメーションの更新 func _update_anim(delta:float) -> void: if _can_jump == false: # 吹っ飛び中は回転する. _sprite.rotation -= 10 * delta
プレイヤーのアニメーションを実装する
土管との衝突を作ったので、ついでにプレイヤーのアニメーションを実装しておきます。 プレイヤーのアニメーション番号は以下のようになっていました。
これをもとにアニメーションの実装を行います。
Player.gdの_update_anim()を以下のように修正します。
# アニメーションの更新 func _update_anim(delta): + # 基本 + _sprite.frame = 0 + if velocity.y < 0: + # 上昇中 + _sprite.frame = 1 if _can_jump == false: # 吹っ飛び中は回転する _sprite.rotation -= 10 * delta + # ダメージ画像にする + _sprite.frame = 2
実行して操作や状態によってスプライトが変化することを確認します。
土管を生成する
土管が連続で出現するようにします。
土管が消える処理を実装する
まずは土管側を修正します。土管スクリプトDokan.gdを開きます。
そして以下のように修正します。
extends StaticBody2D -var velocity = Vector2(-100, 0) +var velocity = Vector2(-150, 0) +# 開始処理 +func start(pos:Vector2, speed_rate:float) -> void: + position = pos + velocity *= speed_rate func _process(delta:float) -> void: position += velocity * delta + if position.x < -128: + # 画面外に出たら消える + queue_free()
移動速度を調整し、開始処理の start() を追加。そして位置が「-128」になったら queue_free() を呼び出してシーンから削除するようにしました。
土管を生成する処理を実装する
タブから Mainシーンを選択して「Dokan」ノードを右クリックして「ノード削除」から削除します。(DELETEキーでも削除できます)
Mainノードにスクリプトをアタッチする
次にMainノードを選択した状態で、「スクリプトアイコン」をクリックします。
「作成」でスクリプトを作成します。
Mainシーンのスクリプト Main.gd には以下のように記述します。
extends Node2D # 土管オブジェクト var Dokan = preload("res://Dokan.tscn") # 出現間隔(最初は3秒) 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 = randf_range(32, 400-32) # 土管を上下に生成 for i in range(2): var dokan = Dokan.instantiate() 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)
簡単に説明すると
_ready()で乱数を初期化_process()で 経過時間deltaをtimeに足し込んでいき、intervalを超えたら_add_dokan()で土管を生成_add_dokan()では、上下に土管を生成し、再びインターバルを開始
となります。 土管を生成している部分の説明です。

ドカン画像の高さの半分がおおよそ 320px (正確には 305.5px) なので、その高さだけ上下に移動させて、160px の間隔を開けています。
では実行して動作を確認します。
衝突の不具合を修正する
衝突時の処理ですが、若干見た目に不具合があります。 横からぶつかった場合は気にならないのですが、上下からぶつかったときにおかしな挙動となります。
面白い挙動なのでアリ……としてもいいかもしれませんが、物理的におかしな動きをしているので修正します。
Player.gdを開いて_update_collision()を以下のように修正します。
# 衝突処理を行う func _update_collision(delta:float) -> void: # 移動と衝突処理を行う var collision = move_and_collide(velocity * delta) if collision: # 衝突したのでジャンプできなくする _can_jump = false # 左方向に吹き飛ばす velocity.x -= 300 + if position.y < collision.get_position().y: + # ドカンよりも上にプレイヤーがいる + velocity.y = -300 # 上にバウンド + else: + # ドカンよりも下にプレイヤーがいる. + velocity.y = 300 # 下にバウンド # さらに移動量を加える. move_and_collide(velocity * delta)
衝突した土管に対する位置で、上下方向の移動量を設定しています。 では実行して、上や下から土管にぶつかってもおかしな挙動とならないことを確認します。
ゲームオーバーとリトライを作る
フォントデータの確認
プロジェクトにはすでにフォントデータは含まれているのでこれを使います。
なおこのデータは以下のサイトからダウンロードしたものとなります。
Labelを配置する
Mainノードを右クリックして、「+子ノードを追加」を選びます。
ここからLabel を選んで「作成」をクリックします。
Labelを作成できたら、名前を「Caption」に変更しておきます。
Labelの設定をする
インスペクタの Text に "GAME OVER" と入力すると テキストが表示されます。

デフォルトフォントを修正するには、Label Settings の <空> をクリックして "新規 LabelSettings」を選択。
Label Settingsの下の部分に追加表示された Font > Font の <空> をクリックして、「クイックロード」を選択。
フォント選択画面に表示されたXolonium-Regular.ttfを選択。
フォントサイズ (Font > Size) を64px、アウトラインサイズ (Outline > Size) を12px、アウトラインカラーを黒にします。
以下のようなフォントとなります。
フォントを中央に表示したいので、Horizontal Alignment と Vertical Alignment の両方を「Center」にします。
そして Transform の Size を画面サイズと同じ 800x480 にします。
これによって画面中央の文字が出るようになりました。
プレイヤーの消滅処理を作る
プレイヤーを消滅させる処理を作ります。
Player.gd の _process()を以下のように修正します。
func _process(delta) -> void: # 移動処理を行う _update_moving(delta) # 衝突処理 _update_collision(delta) # アニメーションの更新 _update_anim(delta) + if position.x < 0 or position.y > 480: + # 画面外に出たら消滅 + queue_free()
画面外に出たら queue_free() で消滅させるようにしました。
ゲームオーバー判定を作る
ゲームオーバー判定を実装します。
Main.gd を開いて以下のように修正します。
extends Node2D # 土管オブジェクト var Dokan = preload("res://Dokan.tscn") # 出現間隔(最初は3秒) 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_to_file("res://Main.tscn") func _add_dokan(): # 出現回数をカウントアップ dokan_cnt += 1 # 高さを決める var xbase = 800 + 120 var ybase = randf_range(32, 400-32) # 土管を上下に生成 for i in range(2): var dokan = Dokan.instantiate() 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)
キャプションの描画順を修正する
"GAME OVER" の文字が土管の後ろに描画されているので、最前面に描画するように修正します。 "Main" シーンを右クリックして「+子ノードを追加」を選びます。
CanvasLayer を選んで「作成」ボタンをクリックします。
「Caption」ノードをドラッグ&ドロップして、ノードの階層を以下のようにします。(CanvasLayerの下にぶら下げる)
「CanvasLayer」ノードを選択して、インスペクタから CanvasLayer > Layer > Layer の値が「1」になっていることを確認します。
この値が大きいほど、画面手前に表示されます。CanvasLayerを使用しない場合は Layer = 0 なので、「Caption」は プレイヤーや土管よりも手前に表示されることになります。
Main.gd の修正
「Caption」の階層を移動させたため、実行するとMain.gdではエラーが発生します。

実際にcaption変数の値を見ると nullアクセスして停止していることがわかります。
Main.gdを以下のように修正します。
extends Node2D # 土管オブジェクト var Dokan = preload("res://Dokan.tscn") # 出現間隔(最初は3秒) var interval = 3 # 生成タイマー var timer = interval # 土管出現回数 var dokan_cnt = 0 # キャプション -@onready var caption = $Caption +@onready var caption = $CanvasLayer/Caption # プレイヤー @onready var player = $Player ...
実行して、「GAME OVER」の文字が土管よりも手前に描画されることを確認します。

完成プロジェクト
完成したプロジェクトは GitHub にアップロードしておきました。
もしうまく動作しない場合はこちらを参考にしてみるとよいかもしれません。



















































































