👌

[Godot Engine 4] 2Dシューティングアクションゲーム「Yozora Maker」のチュートリアル

2023/08/26に公開

はじめに

Godot Engineの公式ドキュメントにあるチュートリアルが終わった後、何をやったらいいのか分からない、という人むけに私が作ったミニゲームをチュートリアルとして公開します。
プレイヤーキャラクターが星を作り、それを避けていくことで高いスコアを目指すミニゲームです。

(こちらに記載する内容とは実装が少し異なりますが) 今回作るゲームはこちらから遊ぶことができます。
https://godotplayer.com/games/yozora_maker

プロジェクトのソースコードはこちらです。
https://github.com/coffee-r/YozoraMakerGodot4x
このチュートリアルを作った際に使用したGodot Engineのバージョンは4.1.1です。

設計について

Godot Engineを触る前にある程度の設計をしておきます。

ゲームデザインをする

どのようなゲームを作るのか箇条書きでまとめておきます。

* プレイヤー
	* 星を作れる
	* 星を作った逆方向に移動できる
	* 星を作る方向は時間経過で回転する
	* 星は無制限に作れるものではなく、作る間隔としてクールタイムを設ける
	* プレイヤーの近くに星がある時はかすり判定を行う
	* プレイヤーが作った星がプレイヤーに当たるとゲームオーバーとする
	* プレイヤーは画面端に当たると反射して移動する
* スコア
	* 星を作った数だけスコアを追加する → 遊び手を褒める
	* かすり判定がある状態で星を作るとボーナススコアを追加する → 遊び手をより褒める
* 星
	* 星同士が当たると反射して移動する
	* 星は画面端に当たると反射して移動する

星を作れば作るだけスコアが高まり、
さらにかすり判定によるボーナススコアが追加がされるとともに
逆に被弾するリスクが高まっていく、といったデザインにしてみました。

大雑把にノードとシーンの構成を考える

  • 画面構成を考え、画面ごとにシーンを作成することを考えます。
* タイトル画面 → titleシーン
* ゲーム画面 → gameシーン
* リザルト画面 → resultシーン
  • 画面の切り替えを担うシーンとしてmainシーンを用意することを考えます。
  • Mainノードの子にtitleシーン、gameシーン、resultシーンのいずれかのインスタンスを保持するようにします。
    例えばタイトル画面を表示するときは
    - Main
    	- titleインスタンス
    
    ゲーム画面の時は
    - Main
    	- gameインスタンス
    
    リザルト画面の時は
    - Main
    	- resultインスタンス
    
  • titleシーンの中身
    • 「画面をクリックしたらゲーム開始」といったようなUI表示をします
    • 画面クリックで最終的にシグナルを発して、画面切り替えの合図を送ります
  • gameシーンの中身
    • プレイヤー、星、画面端が相互作用しながら動作するようにします
      • プレイヤー、星、画面端はシーンとして作成します
    • 現在のスコアのUI表示をします
    • ゲームオーバー時に最終的にシグナルを発して、画面切り替えの合図を送ります
  • resultシーンの中身
    • 最終スコアのUI表示をします
    • 「画面をクリックしたらゲームをリトライ」といったUI表示をします
    • 画面クリックで最終的にシグナルを発して、画面切り替えの合図を送ります

ある程度設計を考えたら、Godot Engineを触っていきます。

プロジェクトの作成

Godotを起動し、新規プロジェクトを作成します。
プロジェクト名は「Yozora Maker」とします。

プロジェクトの設定

プロジェクト > プロジェクト設定より、各種設定を行なっていきます。

  • 画面サイズの設定
    • 表示 > ウィンドウの項目
      • サイズ
        • ビューポートの幅 : 136
        • ビューポートの高さ : 240
      • ストレッチ
        • モード : viewport

  • レンダリング設定 (ドット絵をはっきり表示するための設定をします)
    • レンダリング > テクスチャの項目
      • デフォルトのテクスチャフィルタ : Nearest

  • 背景色
    • レンダリング > 環境の項目
      • デフォルトのクリアー色 : #050018

  • 当たり判定のレイヤー
    • Layer Names > 2D物理の項目
      • Layer1 : Player (プレイヤーが画面端に衝突する時の判定に使用)
      • Layer2 : PlayerLife (プレイヤーと星が衝突しゲームオーバーとする判定に使用)
      • Layer3 : PlayerGraze (プレイヤーと星とのかすり判定に使用)
      • Layer4 : Star (星の衝突判定に使用)
      • Layer5 : Stage (画面端の判定に使用)

素材のインポート

ゲーム開発のための素材をプロジェクトにインポートします。
テクスチャと一部の効果音は私が作ったものを利用していただければと思います。
その他の効果音、BGM、フォントは素晴らしいクリエイターの方々の作品を使わせて頂きましょう。

画像素材

プロジェクトに「textures」フォルダを作成します。

https://github.com/coffee-r/YozoraMakerGodot4x/tree/main/textures
ここから拡張子が.pngのものを7つダウンロードして、texturesフォルダにインポートします。

フォント素材

プロジェクトに「fonts」フォルダを作成します。

患者長ひっく様が作られたフォントをダウンロードし、fontsフォルダにインポートします。
https://hicchicc.github.io/00ff/
「x12y16pxLineLinker.ttf」

BGM素材

プロジェクトに「sounds」フォルダを作成します。

ucchii0様が作られたBGMをmp3形式でダウンロードし、「bgm.mp3」と名前変更して、soundsフォルダにインポートします。
https://ucchii0artist.wixsite.com/ucchii0
「あの先へ行こうか【Free Ver】」

「bgm.mp3」はインポートタブよりループをオンに設定した後、「再インポート」を押下します。

SE素材

くらげ工匠様が作られた効果音をダウンロードし、名前変更して、soundsフォルダにインポートします。
http://www.kurage-kosho.info/system.html

  • ボタン079 (「start.mp3」に名前変更)
  • ボタン080 (「result.mp3」に名前変更)
  • ボタン045 (「collision.mp3」に名前変更)

また以下のリンクから「death.mp3」「graze.mp3」「shoot.mp3」をダウンロードし、soundsフォルダにインポートします。
https://github.com/coffee-r/YozoraMakerGodot4x/tree/main/sounds
(記憶がやや定かではないのですが...こーひーあーるがhttps://jfxr.frozenfractal.com/で確か作ったものです。間違っていたらごめんなさい。)

続いては、ゲームスタート → ゲームプレイ ↔︎ ゲームリザルトのゲームループが回るように各種画面を作っていきます。

ゲーム画面

gameシーンの作成

新規シーンを追加し、ルートノードとして「Node2D」を選択、ルートノードの名前を「Game」と名前変更してシーンを保存します。

画面端の当たり判定

星やプレイヤーが画面端で反射移動するために、画面端として当たり判定を設けます。
新規シーンを追加し、ルートノードとして「StaticBody2D」を選択、ルートノードの名前を「Stage」と名前変更してシーンを保存します。(画面端自体は衝突しても移動する必要がないので、StaticBody2Dを使います)

星やプレイヤーと相互作用するように、インスペクターでStageノードにLayerとMaskを設定します。

  • Layerは5(Stage)のみを有効にする
  • Maskは1(Player)と4(Star)のみを有効にする

Stageノードの子ノードとして「CollisionShape2D」を4つ追加します。
それぞれのCollisionShape2DでインスペクターのShapeに新規SegmentShape2Dを指定します。

4つのCollisionShape2Dを
マウスを使って画面端4辺に沿って線を引くように当たり判定を設置します。

後に星やプレイヤーとの相互作用をテストするため、gameシーンのGameノード配下にStageシーンをインスタンス化しておきます。

新規シーンを追加し、ルートノードとして「CharacterBody2D」を選択、ルートノードの名前を「Star」と名前変更してシーンを保存します。

プレイヤー(ゲームオーバー用当たり判定)、星、画面端と相互作用するように、LayerとMaskを設定します。

  • Layerは4(Star)のみを有効にする
  • Maskは2(PlayerLife)と4(Star)と5(Stage)のみを有効にする

Starノードの子ノードとして、2つのノードを追加します。

  • Sprite2D
    • 追加後、インスペクターのTextureに「star.png」を読み込みます
  • CollisionShape2D
    • 追加後、インスペクターのShapeにCircleShape2Dを指定します。
    • 当たり判定が右に若干よっているため、インスペクターのTransform > Positionのxを「-0.5」に設定します
    • 当たり判定の大きさをマウスを使って調節します

Starノードにスクリプトをアタッチし、_physics_process(delta)で移動処理と反射を記述します。

star.gd
extends CharacterBody2D

func _physics_process(delta):
	# 移動
	var collision = move_and_collide(velocity * delta)
	
	# 星か画面端と当たったら反射
	if collision:
		velocity = velocity.bounce(collision.get_normal

試しに

  • 画面端と星がぶつかった時に星が反射すること
  • 星と星がぶつかった時に星が反射すること

を確認してみましょう。
gameシーンのGameノード配下にstarシーンを10個ほどインスタンス化し、画面内に配置しておきます。

また、検証用に星の初期速度をランダムに設定するよう、star.gdに一時的な処理を書きます。

star.gd
extends CharacterBody2D

func _ready():
	# 初期速度をランダムに設定する
	velocity = Vector2(randf_range(0, 25), randf_range(0, 25))

~~~以下略~~~

gameシーンを実行し、星が画面端に当たった時に反射する、星と星が当たった時に反射する相互作用が確認できれば成功です。

gameシーンのGameノード配下に追加したstarシーンのインスタンスは削除し、star.gdは_ready()関数を削除して元に戻しておきます。

プレイヤー

新規シーンを追加し、ルートノードとして「CharacterBody2D」を選択、ルートノードの名前を「Player」と名前変更してシーンを保存します。

プレイヤーはゲーム開始時に画面中央に位置させたいので、インスペクターからTransform > Positionを設定します。

  • x : 68
  • y : 120

プレイヤーの表示

プレイヤーを表示するために、Playerノードの子ノードとして「AnimatedSprite2D」を追加し、「PlayerAnimatedSprite2D」と命名します。(のちにアニメーションさせたいのでAnimatedSprite2Dを使います。)

詳細なアニメーションは後ほど作るとして、現時点では静止画を表示させます。
PlayerAnimatedSprite2DのインスペクターのAnimation > Sprite Framesから新規SpriteFramesを選択します。

SpriteFramesをクリックし、アニメーションタブを開きます。

「default」のアニメーションに「player.png」を割り当てます。「スプライトシートからフレームを追加する」アイコンをクリックし、「player.png」を読み込みます。

フレームを選択するウィンドウで、サイズを16×16pxに設定し、一番左にあるマス目を選択してフレームを追加します。

playerシーンを実行し、画面中央にプレイヤーが表示されればOKです。

星の発射方向の表示と回転

Playerの子ノードに「Sprite2D」を追加し、「PlayerShootDirector」と名前変更します。
PlayerShootDirectorのインスペクターで設定を行います。

  • Textureに「director.png」を読み込み
  • VisibillityのModulateのA : 255から128に変更
  • Transform > Position > x : 17

プレイヤーの右側に半透明の矢印が表示されるでしょう。

PlayerShootDirectorノードにスクリプトをアタッチし、星の発射方向を示す矢印を回転させます

PlayerShootDirector.gd
extends Sprite2D

# 回転速度
@export var speed: float = 4.2

# プレイヤーとの距離
@export var distance_to_player: float = 17.0

# 経過した時間のカウント
var delta_count = 0

# 回転させるかどうかのフラグ
var is_moving = true

func _process(delta):
	# 回転させるフラグが立っている時はカウントする
	if is_moving:
		delta_count += delta
	
	# 回転・位置を決定する
	rotation = speed * delta_count
	position = Vector2(1,0).rotated(rotation) * distance_to_player

現在のシーンを実行し、矢印が時計回りに回転すればOKです。

プレイヤーの3つの当たり判定

プレイヤーと画面端、プレイヤー(ゲームオーバー判定用)と星、プレイヤー(星のかすり判定用)と星とで相互作用をさせるため、3つの当たり判定を設定していきます。

  • 画面端で反射する用
    • PlayerノードのLayerを1(Player)のみに設定
    • PlayerノードのMaskを5(Stage)のみに設定
    • Playerノードの子ノードに「CollisionShape2D」を追加し、「PlayerCollisionShape2D」と名前変更
      • 「PlayerCollisionShape2D」のインスペクターでShapeに「新規CircleShape2D」を設定
  • ゲームオーバー判定用
    • Playerノードの子ノードとして「Area2D」を追加、「PlayerLifeArea2D」と名前変更
    • 「PlayerLifeArea2D」の子ノードに「CollisionShape2D」を追加
      • Shapeに「新規CircleShape2D」を設定
    • 「PlayerLifeArea2D」のLayerを2(PlayerLife)のみに設定
    • 「PlayerLifeArea2D」のMaskを4(Star)のみに設定
  • 星のかすり判定用
    • Playerの子ノードとして「Area2D」を追加、「PlayerGrazeArea2D」と名前変更
    • 「PlayerGrazeArea2D」の子ノードに「CollisionShape2D」を追加
      • Shapeに「新規CircleShape2D」を設定
    • 「PlayerGrazeArea2D」のLayerを3(PlayerGraze)のみに設定
    • 「PlayerGrazeArea2D」のMaskを4(Star)のみに設定

当たり判定の大きさを調節します。
大小関係として
PlayerGrazeArea2D > Player > PlayerLifeArea2D
とします。

プレイヤーの移動

Playerノードにスクリプトをアタッチします。
マウスクリックした時に、星を発射する矢印の正反対にプレイヤーが移動するようにスクリプトを書きます。
また、画面端に衝突した時に反射する記述も加えます。

player.gd
extends CharacterBody2D

# 移動速度
@export var move_speed: float = 25.0

# マウス入力された時に移動速度を決定する
func _input(event):
	if event is InputEventMouseButton && event.is_pressed():
		# 移動の向きを決定する
		var move_direction = $PlayerShootDirector.position.rotated(deg_to_rad(180)).normalized()
			
		# 移動速度を決定する
		velocity = move_direction * move_speed

func _physics_process(delta):
	# 移動
	var collision: KinematicCollision2D
	collision = move_and_collide(velocity * delta)
	
	# 物体(画面端)と当たったら反射
	if collision:
		velocity = velocity.bounce(collision.get_normal())

gameシーンのGameノードの子ノードとしてplayerシーンをインスタンス化し、動作を確認してみましょう。

  • プレイヤーがマウスクリックとともに矢印の反対方向に移動すること
  • プレイヤーが画面端に当たった時に反射すること

が確認できればOKです。

プレイヤーの移動方向転換とともに星を作る

プレイヤーから星を作り出した時に星の初期位置と初期速度を設定できるようにするため、先にstarシーンのStarノードにアタッチされているstar.gdを編集します。

star.gd
extends CharacterBody2D

# 移動の速さ
var move_speed: float = 25.0

func _physics_process(delta):
	~~~中略~~~

# プレイヤーから星が作られた時に呼び出し、位置や速度を初期化する
func start(start_position: Vector2, start_move_direction: Vector2):
	position = start_position
	velocity = start_move_direction.normalized() * move_speed

マウスクリックした時に、プレイヤーの矢印の方向に向けて星を発射するようにするため
player.gdを編集します。

player.gd
extends CharacterBody2D

# 移動速度
@export var move_speed: float = 25.0

# 星
@export var star_scene : PackedScene

# マウス入力された時
func _input(event):
	if event is InputEventMouseButton && event.is_pressed():
		# 移動の向きを決定する
		var move_direction = $PlayerShootDirector.position.rotated(deg_to_rad(180)).normalized()
			
		# 移動速度を決定する
		velocity = move_direction * move_speed
		
		# 星を発射する
		var star = star_scene.instantiate()
		star.z_index = -1
		star.start($PlayerShootDirector.global_position, $PlayerShootDirector.position.normalized())
		add_sibling(star)

func _physics_process(delta):
	~~~中略~~~

playerシーンのPlayerノードのインスペクターにある「Star Scene」に、starシーンを読み込みましょう。

playerシーンを実行し、マウスクリックでプレイヤーから星が発射されれば成功です。

星の発射に照準を合わせる時間を持たせる

操作感触をより良くするために、遊び手がマウス入力した後すぐさま星を発射するのではなく、一瞬間をおいてから星を発射するようにします。またこの時プレイヤーを一瞬だけ移動停止させます。

Playerノードの子ノードとして「Timer」を追加し、「ShootWaitTimer」と名前変更します。

インスペクターでWait Timeを「0.15」、One Shotをオンに設定します。

ShootWaitTimerのノードタブより、「timeout()」シグナルを選択し、Playerに接続します。
その後、player.gdを編集します。

player.gd
extends CharacterBody2D

# 移動速度
@export var move_speed: float = 25.0

# 星
@export var star_scene : PackedScene

# マウス入力された時
func _input(event):
	if event is InputEventMouseButton && event.is_pressed():
		# タイマーを起動する
		$ShootWaitTimer.start()
		
		# 回転方向オブジェクトを停止
		$PlayerShootDirector.is_moving = false

func _physics_process(delta):
	# 移動
	var collision: KinematicCollision2D
	if ($ShootWaitTimer.is_stopped()):
		collision = move_and_collide(velocity * delta)
	else:
		collision = move_and_collide(Vector2.ZERO)
	
	# 物体(画面端)と当たったら反射
	if collision:
		velocity = velocity.bounce(collision.get_normal())

func _on_shoot_wait_timer_timeout():
	# 移動の向きを決定する
	var move_direction = $PlayerShootDirector.position.rotated(deg_to_rad(180)).normalized()
		
	# 移動速度を決定する
	velocity = move_direction * move_speed
		
	# 星を発射する
	var star = star_scene.instantiate()
	star.z_index = -1
	star.start($PlayerShootDirector.global_position, $PlayerShootDirector.position.normalized())
	add_sibling(star)
	
	# 回転方向オブジェクトを動かす
	$PlayerShootDirector.is_moving = true

星の発射直前に一瞬間をおくようになれば成功です。

星の発射にクールタイムを設ける

現段階でマウスクリックを連打すると、回転方向の矢印が止まったままになり、挙動としておかしい状態です。これらを防ぐために入力にクールタイムを設けます。

Playerノードの子ノードとして「Timer」を追加し、「ShootCoolTimer」と名前変更します。

インスペクターでWait Timeを「0.275」、One Shotをオンに設定します。

player.gdを編集します。

player.gd
~~~中略~~~

# マウス入力された時
func _input(event):
	if $ShootCoolTimer.is_stopped() && event is InputEventMouseButton && event.is_pressed():
		# 2つのタイマーを起動する
		$ShootWaitTimer.start()
		$ShootCoolTimer.start()
		
		# 回転方向オブジェクトを停止
		$PlayerShootDirector.is_moving = false

~~~以下略~~~

現在の星のかすり数をカウントする

PlayerGrazeArea2Dノードにスクリプトをアタッチし、今現在どれだけの星がプレイヤーにかすっているかをカウントします。

PlayerGrazeArea2Dのノードタブから「body_entered」シグナル、「body_exited」シグナルをそれぞれ選択し、PlayerGrazeArea2Dに接続します。その後PlayerGrazeArea2D.gdを編集します。

PlayerGrazeArea2D.gd
extends Area2D

# かすり中の星の数
var grazing_count = 0

# かすり判定に星が入った時
func _on_body_entered(body):
	grazing_count += 1
	print('graze in ' + var_to_str(grazing_count))

# かすり判定から星が出て行った時
func _on_body_exited(body):
	grazing_count -= 1
	print('graze out ' + var_to_str(grazing_count))

gameシーンを実行して星をたくさん作ってみて、ログに現在かすっている星の数が表示されればOKです。

星の発射時発射したことと星のかすり数をシグナルとしてemitする

星を発射したことと、現在の星のかすり数、現在の星の数はスコアの計算に必要になるので、シグナルをemitします。
(スコアの計算をどこでやるかは悩ましいですが、今回は小規模なゲームですのでスコアのUIの部分にて計算しちゃいます。)
player.gdを編集します。

player.gd
extends CharacterBody2D

# スコア追加シグナル
signal add_score

~~~中略~~~

func _on_shoot_wait_timer_timeout():
	# スコア追加通知
	emit_add_score()
	
	~~~中略~~~

func emit_add_score():
	# 現在の星の数を取得
	var star_count = get_tree().get_nodes_in_group('stars').size()
	
	# スコア追加シグナルを発信する
	print('シグナル add_score')
	emit_signal("add_score", star_count, $PlayerGrazeArea2D.grazing_count)

また、現在の星の数を取得できるようにするため、starシーンにグループの設定をします。
starシーンを開き、ノードタブのグループで「stars」を追加します。

ゲームオーバー時にシグナルをemitする

プレイヤーの3つの当たり判定のうち、ゲームオーバー判定用の当たり判定「PlayerLifeArea2D」に星が当たったら、ゲームオーバー関連の処理ができるようにシグナルをemitします。
PlayerLifeArea2Dのノードタブから「body_entered」シグナルを選択し、Playerに接続します。その後player.gdを編集します。

player.gd
extends CharacterBody2D

# スコア追加シグナル
signal add_score

# プレイヤー死亡シグナル
signal player_death

~~~中略~~~

func _on_player_life_area_2d_body_entered(body):
	# プレイヤー死亡シグナル
	print('シグナル player_death')
	emit_signal('player_death')

ここまでで、プレイヤーの基本挙動は完成です。

スコア

スコアデータの管理

スコアのデータはゲーム画面とリザルト画面を跨いで保持する必要があります。今回はシングルトンとして実装します。

ファイルシステムで右クリックをして、新規作成 > スクリプトで 「res://score.gd」を作成

プロジェクト > プロジェクト設定 > 自動読み込みのタブで「score.gd」を選択して追加

score.gdを編集します。

score.gd
extends Node

var value: int = 0

スコアはゲーム開始時に毎回0に初期化される必要があります。
そのため、gameシーンのGameノードにスクリプトをアタッチし、スコアを0に初期化するようにします。

game.gd
extends Node2D

func _ready():
	Score.value = 0

スコアの計算と表示

gameシーンのGameノードの子ノードとして「CanvasLayer」ノードを追加し、「GameUI」と名前変更します。

GameUIの子ノードに「Label」ノードを追加し、「ScoreLabel」と名前変更します。
アンカーのプリセットに「上伸長」を選択したのち、インスペクターで各種設定を行います。

  • Text : 0
  • Horizontal Alignment : Center
  • Vertical Alignment : Center
  • Theme Overrides > Fonts : 事前にインポートしておいたフォントファイルを読み込み

ScoreLabelの子ノードに「Timer」ノードを追加し、「ViewGrazingTimer」と名前変更します。
インスペクターからWait Timeを「0.5」、One Shotをオンに設定します。

ScoreLabelにスクリプトをアタッチします。
ScoreLabelに対して2つのシグナルを接続します。

  • Playerのノードタブから「add_score()」シグナルをScoreLabelに接続
  • ViewGrazingTimerのノードタブから「timeout()」シグナルをScoreLabelに接続

ScoreLabel.gdではスコアの計算とスコア表示を行います。
また、プレイヤーが星にかすっている時に星を作った場合には「Graze!」と表示します。

ScoreLabel.gd
extends Label

func _on_player_add_score(star_count, grazing_count):
	# 星にかすっている時は星の数 * かすり数だけスコアを加算し、Grazeと表示
	# 星にかすっていない時は単純に+1スコアを追加し、スコアを表示
	if grazing_count >= 1:
		Score.value += star_count * grazing_count
		text = 'Graze!'
		$ViewGrazingTimer.start()
	else:
		Score.value += 1
		text = var_to_str(Score.value)

func _on_view_grazing_timer_timeout():
	# スコアを表示
	text = var_to_str(Score.value)

gameシーンを実行してみましょう。期待する動作としては以下です。

  • プレイヤーが星を作った時にスコア+1が追加される
  • プレイヤーが星の近くで星を作った時に「Graze!」と表示され、いくらか多くのスコアが追加される

プレイヤーのゲームオーバーををメインシーンに伝える

gameシーンのGameノードの子ノードとして「Timer」ノードを追加し、「GameEndTimer」と名前変更します。

GameEndTimerはインスペクターの設定でWait Timeを「0.75」に、One Shotをオンに設定します。

Gameノード配下にあるPlayerインスタンスのノードタブを開き、「player_death()」シグナルをGameに接続します。
また、GameEndTimerのノードタブを開き、「timeout()」シグナルをGameに接続します。

game.gdを編集し、プレイヤーのゲームオーバー通知をmainシーンが最終的に受け取れるようにスクリプトを編集します。

game.gd
extends Node2D

signal game_end

func _ready():
	# スコアの初期化
	Score.value = 0

func _on_player_player_death():
	# タイマー開始
	$GameEndTimer.start()

func _on_game_end_timer_timeout():
	# ゲーム終了シグナル発信
	print('シグナル game_end')
	emit_signal('game_end')

ここまでくれば、ゲーム画面の基礎的な挙動ができているかと思います。

リザルト画面

スコアの表示UI

新規シーンを追加し、ルートノードとして「Node2D」を選択、ルートノードの名前を「Result」と名前変更してシーンを保存します。

Resultノードの子ノードとして「CanvasLayer」ノードを追加し、「ResultUI」と名前変更します。

ResultUIの子ノードとして「Control」ノードを追加し、「ResultScoreControl」と命名します。アンカーのプリセットで中央水平伸長を選択します。

ResultScoreControlの子ノードとして「Label」ノードを追加し、「ResultScoreLabel」と命名します。
アンカーのプリセットで中央水平伸長を選択します。

ResultScoreLabelのインスペクターで各種設定を行います。

  • Text : 0
  • Horizontal Alignment : Center
  • Vertical Alignment : Center
  • Theme Overrides > Fonts : 事前にインポートしておいたフォントファイルを読み込み

ResultScoreLabelにスクリプトをアタッチし、シングルトンとして定義したScoreの値をこのLabelに反映するようにします。

ResultScoreLabel.gd
extends Label

func _ready():
	# textにスコアを反映
	text = var_to_str(Score.value)

試しにscore.gdの値を0からいろんな値に変えてresultシーンを実行してみましょう。スコアの値がLabelとして表示されるはずです

また、ResultUIの子ノードとして「Label」ノードを追加し、「RetryLabel」と命名します。
アンカーのプリセットで中央水平伸長を選択します。
インスペクターで各種設定を行います。

  • Text : RETRY CLICK
  • Horizontal Alignment : Center
  • Vertical Alignment : Center
  • Theme Overrides > Fonts : 事前にインポートしておいたフォントファイルを読み込み
  • Layout > Transform > Position > y : 164

画面タップでゲームリトライのシグナルを発する

Resultノードの子ノードとして「Timer」ノードを追加し、「GameRetryTimer」と名前変更します。
インスペクターの設定でWait Timeを「0.4」、One Shotをオンに設定します。

Resultノードにスクリプトをアタッチします。
GameRetryTimerのノードタブから「timeout()」シグナルをResultに接続します。

result.gdでは画面をクリックした時にタイマーを開始し、timeout()シグナルを受け取ったらゲームリトライのシグナルを発信します。(このシグナルは、後に作るメインシーンの画面切り替えで使用します)

result.gd
extends Node2D

signal game_retry

func _input(event):
	if event is InputEventMouseButton && event.is_pressed():
		# タイマー開始
		$GameRetryTimer.start()

func _on_game_retry_timer_timeout():
	# ゲームリトライシグナル発信
	print('シグナル game_retry')
	emit_signal('game_retry')

ここまでで、リザルト画面の基礎的な挙動ができているかと思います。

タイトル画面

画面UI

新規シーンを追加し、ルートノードとして「Node2D」を選択、ルートノードの名前を「Title」と名前変更してシーンを保存します。

Titleノードの子ノードとして「CanvasLayer」ノードを追加し、「TitleUI」と名前変更します。

TitleUIノードの子ノードとして「Label」を2つ作り、それぞれ「TitleLabel」「StartLabel」と名前変更します。

インスペクターなどで各種設定を行います。

  • 2つのLabel共通
    • アンカーのプリセットを中央水平伸長
    • Horizontal Alignmentを「Center」
    • Vertical Alignmentを「Center」
    • Theme Overrides > Fonts : 事前にインポートしておいたフォントファイルを読み込み
  • TitleLabel
    • Text : YOZORA MAKER (改行する)
    • Layout > Transform > Position > y : 44
  • StartLabel
    • Text : CLICK START
    • Layout > Transform > Position > y : 164

画面タップでスタート

Titleノードの子ノードとして「Timer」ノードを追加し、「GameStartTimer」と名前変更します。
インスペクターの設定でWait Timeを「1.5」、One Shotをオンに設定します。

Titleノードにスクリプトをアタッチします。
GameStartTimerのノードタブから「timeout()」シグナルをTitleに接続します。

title.gdでは画面をクリックした時にタイマーを開始し、timeout()シグナルを受け取ったらゲーム開始のシグナルを発信します。(このシグナルは、後に作るメインシーンの画面切り替えで使用します)

title.gd
extends Node2D

signal game_start

func _input(event):
	if event is InputEventMouseButton && event.is_pressed():
		# タイマーを開始
		$GameStartTimer.start()

func _on_game_start_timer_timeout():
	# ゲーム開始シグナルを発信
	emit_signal('game_start')

ここまでで、タイトル画面の基礎的な挙動ができているかと思います。

メインシーン

タイトル画面、ゲーム画面、リザルト画面を切り替えることを担うmainシーンを実装していきます。
新規シーンを追加し、ルートノードとして「Node2D」を選択、ルートノードの名前を「Main」と名前変更してシーンを保存します。

Mainノードにスクリプトをアタッチします。
Mainノードの子ノードとしてTitleシーンをインスタンス化します。

Titleのノードタブから「game_start()」シグナルをMainに接続します。

main.gdに画面切り替えの処理を書きます。

main.gd
extends Node2D

@export var game_scene: PackedScene
@export var result_scene: PackedScene

func _on_title_game_start():
	# タイトルシーンを削除
	$Title.queue_free()
	
	# ゲームシーンをインスタンス
	var game = game_scene.instantiate()
	add_child(game)
	
	# シグナル接続
	game.game_end.connect(_on_game_end)

func _on_game_end():
	# ゲームシーンを削除
	$Game.queue_free()
	
	# リザルトシーンをインスタンス
	var result = result_scene.instantiate()
	add_child(result)
	
	# シグナル接続
	result.game_retry.connect(_on_game_retry)

func _on_game_retry():
	# リザルトシーンを削除
	$Result.queue_free()
	
	# ゲームシーンをインスタンス
	var game = game_scene.instantiate()
	add_child(game)
	
	# シグナル接続
	game.game_end.connect(_on_game_end)

MainノードのインスペクターでGame Scene、Result Sceneを設定します。

mainシーンを実行して、ゲームループが回るか検証しましょう。

ここまででこのゲームの基礎的な部分は作り終えました。お疲れ様でした。
ここから先はゲームをブラッシュアップしていく作業になります。

画面演出

画面遷移時のトランジションアニメーション

現在の作りでは画面の切り替えが瞬間的に行われています。
ゲームでよくある画面の切り替え方としてはフェードイン/アウトなどがあります。今回は画面を塗りつぶすようなトランジションアニメーションを作っていきます。

mainシーンのMainノードの子に「CanvasLayer」ノードを追加し、「TransitionUI」と名前変更します。
このCanvasLayerは常にこのゲームの最前面に表示してほしいので、インスペクターのLayer > Layerに128と入力しておき、最前面で表示されるようにします。

さらにTransitionUIの子ノードに「ColorRect」ノードを追加します。アンカーのプリセットで「Rect全面」を選択し、画面いっぱいに広がるようにします。またインスペクターで以下の設定をします。

  • Color : #fbf236
  • Layout > Transform > Position > x : -136

TransitionUIノードにスクリプトをアタッチし、アニメーションを実装します。

TransitionUI.gd
extends CanvasLayer

func _ready():
	# 位置の初期化
	$ColorRect.position = Vector2(-136, 0)

func transition_in():
	# 位置の初期化
	$ColorRect.position = Vector2(-136, 0)
	
	# 位置アニメーション
	var tween = get_tree().create_tween()
	tween.tween_property($ColorRect, "position", Vector2(0, 0), 0.4).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CIRC)
	await tween.finished

func transition_out():
	# 位置アニメーション
	var tween = get_tree().create_tween()
	tween.tween_property($ColorRect, "position", Vector2(136, 0), 0.4).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_CIRC)
	await tween.finished

Mainノードにアタッチされているmain.gdでトランジションアニメーションを呼び出すようにします。

main.gd
extends Node2D

@export var game_scene: PackedScene
@export var result_scene: PackedScene

func _on_title_game_start():
	# トランジションアニメーション
	await $TransitionUI.transition_in()
	
	# タイトルシーンを削除
	$Title.queue_free()
	
	# ゲームシーンをインスタンス
	var game = game_scene.instantiate()
	add_child(game)
	
	# シグナル接続
	game.game_end.connect(_on_game_end)
	
	# トランジションアニメーション
	await $TransitionUI.transition_out()

func _on_game_end():
	# トランジションアニメーション
	await $TransitionUI.transition_in()
	
	# ゲームシーンを削除
	$Game.queue_free()
	
	# リザルトシーンをインスタンス
	var result = result_scene.instantiate()
	add_child(result)
	
	# シグナル接続
	result.game_retry.connect(_on_game_retry)
	
	# トランジションアニメーション
	await $TransitionUI.transition_out()

func _on_game_retry():
	# トランジションアニメーション
	await $TransitionUI.transition_in()
	
	# リザルトシーンを削除
	$Result.queue_free()
	
	# ゲームシーンをインスタンス
	var game = game_scene.instantiate()
	add_child(game)
	
	# シグナル接続
	game.game_end.connect(_on_game_end)
	
	# トランジションアニメーション
	await $TransitionUI.transition_out()

mainシーンを実行し、画面切り替え時にアニメーションが行われることを確認します。

ゲームーオーバー時のプレイヤーと星の挙動

  • プレイヤーと星が衝突したことを視認しやすくするため、衝突時にプレイヤーと星の移動を止めます
  • プレイヤーの発射方向の矢印はゲームオーバー後表示が不要なので非表示にします
  • ゲームオーバー後はマウスでの入力を受け付けないようにします

player.gdに死亡フラグを用意し、実装していきます。

player.gd
extends CharacterBody2D

~~~中略~~~

# 死亡フラグ
var is_dead: bool = false

# マウス入力された時
func _input(event):
	if is_dead == false && $ShootCoolTimer.is_stopped() && event is InputEventMouseButton && event.is_pressed():
		~~~中略~~~

func _physics_process(delta):
	~~~中略~~~
	
	# 死亡している場合はプレイヤーと星の速度を0にする
	if is_dead:
		velocity = Vector2.ZERO
		get_tree().call_group("stars", "stop")

~~~中略~~~

func _on_player_life_area_2d_body_entered(body):
	# 死亡判定
	is_dead = true
	
	# 発射矢印隠す
	$PlayerShootDirector.hide()
	
	# プレイヤー死亡シグナル
	print('シグナル player_death')
	emit_signal('player_death')

stars.gdを開き、stop()を実装します。

star.gd
extends CharacterBody2D

~~~中略~~~

# プレイヤーがゲームオーバーになった時に呼び出し、速度を0にする
func stop():
	velocity = Vector2.ZERO

gameシーンを実行し、ゲームーオーバー時の挙動が確認できれば成功です。

プレイヤーキャラクターのアニメーション

playerシーンで
Playerの子ノードのPlayerAnimatedSprite2Dを選択し、スプライトアニメーションを9つ追加します。
それぞれのアニメーションでフレームを追加します。

  • death
  • idle_down
  • idle_left
  • idle_right
  • idle_up
  • shoot_down
  • shoot_left
  • shoot_right
  • shoot_up

player.gdを編集し、プレイヤーの表示画像が星の発射方向、星の発射、プレイヤーの死亡を元に切り替わるようにします。

player.gd
extends CharacterBody2D

~~~ 中略 ~~~

func _process(delta):
	# プレイヤーのスプライトアニメーション
	animation()

func animation():
	# 死亡フラグが立っている時
	if is_dead:
		$PlayerAnimatedSprite2D.animation = 'death'
		return
	
	# 星の発射方向の角度を取得
	var degree = rad_to_deg($PlayerShootDirector.position.normalized().angle())
	
	# 角度からアニメーションの向きを決定する
	var direction
	if degree >= -45 && degree < 45:
		direction = 'right'
	elif degree >= 45 && degree < 135:
		direction = 'down'
	elif degree >= -135 && degree < -45:
		direction = 'up'
	else:
		direction = 'left'
	
	# 星の発射待ちの時とそうでない時とでアニメーションを分ける
	if $ShootWaitTimer.is_stopped():
		$PlayerAnimatedSprite2D.animation = 'idle_' + direction
	else:
		$PlayerAnimatedSprite2D.animation = 'shoot_' + direction

gameシーンを実行し、プレイヤーの画像が状況により切り替われば成功です。

ヒット演出

プレイヤーと星が衝突した時のヒット演出を簡易的に作成します。

新規シーンを追加し、ルートノードとして「Node2D」を選択し、「HitEffect」という名前でシーンを保存します。
TransformのPositionをx:68、y:120に設定します。
HitEffectの子ノードに「Sprite2D」を2つ追加し、「Sprite2DHorizon」、「Sprite2DVertical」と名前変更します。
2つのSprite2DにはインスペクターからTextureにwhite_square.pngを読み込みます。

2つのSprite2DのTransform > Scaleを設定します。

  • Sprite2DHorizon x:500 y:3
  • Sprite2DVertical x:3 y:500

HitEffectノードにスクリプトをアタッチし、アニメーションを実装します。

hit_effect.gd
extends Node2D

func _ready():
	# スケールのアニメーション
	var tween = get_tree().create_tween()
	tween.set_parallel(true)
	tween.tween_property($Sprite2DHorizon, "scale", Vector2(500, 0.0), 0.1).set_delay(0.75)
	tween.tween_property($Sprite2DVertical, "scale", Vector2(0.0, 500), 0.1).set_delay(0.75)

HitEffectはplayer.gdでゲームオーバー時にプレイヤーと星の衝突地点に表示するようにします。

player.gd
extends CharacterBody2D

~~~中略~~~

# 星
@export var star_scene : PackedScene

# ヒットエフェクト
@export var hit_scene : PackedScene

~~~中略~~~

func _on_player_life_area_2d_body_entered(body):
	# 死亡判定
	is_dead = true
	
	# 発射矢印隠す
	$PlayerShootDirector.hide()
	
	# ヒットエフェクト再生
	var hit_effect = hit_scene.instantiate()
	hit_effect.z_index = -2
	hit_effect.position = body.position
	add_sibling(hit_effect)
	
	# プレイヤー死亡シグナル
	print('シグナル player_death')
	emit_signal('player_death')

~~~中略~~~

スクリプト編集後、playerシーンで、PlayerノードのインスペクターにHit Sceneを設定しておきます。

次にgameシーンのGameノードの子ノードに「Camera2D」を追加します。
Camera2Dノードにスクリプトをアタッチし、カメラシェイクを実装します。
(https://2dgames.jp/godot-camera2d-screen-shake/ を参考にさせて頂きました。)
Camera2Dノードには、Playerノードの「player_death()」シグナルを接続します。

Camera2D
extends Camera2D

# reference https://2dgames.jp/godot-camera2d-screen-shake/

var time = 0

func _ready():
	position.x = 136 / 2
	position.y = 240 / 2

func _process(delta):
	if time > 0:
		time = max(0, time - delta)
		offset.x = 2 * randf_range(-2, 2) * time
		offset.y = 2 * randf_range(-2, 2) * time

func _on_player_player_death():
	time = 0.3

gameシーンを実行し、プレイヤーと星が衝突した時にヒット演出がされるかを確認します。

タイトル画面のキャラクターアニメーション

タイトル画面を押したらゲーム始まった感を出すため、キャラクターアニメーションを加えます。

titleシーンのTitleノードの子ノードに「AnimatedSprite2D」を追加し、「TitleCharacterAnimatedSprite2D」と名前変更します。
インスペクターより設定を行います。

  • Transform > Positionを x:68、y:120 に設定
  • Animation > Sprite Framesに新規SpriteFramesを設定

FPSが12フレームレートのアニメーションを2種類作成します。

  • default
  • dash

defaultには「読み込み後自動再生」をチェックしておきます。

「スプライトシートからフレームを追加する」より、character.pngをサイズ48×48pxで読み込みます。

defaultのフレーム追加

dashのフレーム追加

次に、キャラクターの位置をtweenでアニメーションさせるのと、タイトル画面タップでアニメーションの切り替えを行うのを実装します。
TitleCharacterAnimatedSprite2Dにスクリプトをアタッチします。

TitleCharacterAnimatedSprite2D.gd
extends AnimatedSprite2D

func _ready():
	# 初期位置に配置
	position = Vector2(204, 120)
	
	# 位置アニメーション
	var tween = get_tree().create_tween()
	tween.tween_property(self, "position", Vector2(68, 120), 1).set_delay(1).set_trans(Tween.TRANS_CIRC)

func animate_out():
	# スプライトアニメーション変更
	animation = "dash"
	
	# 位置アニメーション
	var tween = get_tree().create_tween()
	tween.tween_property(self, "position", Vector2(100, 120), 0.5).set_trans(Tween.TRANS_CIRC)
	tween.tween_property(self, "position", Vector2(-204, 120), 0.75).set_trans(Tween.TRANS_CIRC)

title.gdで画面タップ時にanimate_out()を呼び出すようにします。

title.gd
extends Node2D

signal game_start

func _input(event):
	if event is InputEventMouseButton && event.is_pressed():
		# タイマーを開始
		$GameStartTimer.start()
		
		# キャラクターアニメーション
		$TitleCharacterAnimatedSprite2D.animate_out()

func _on_game_start_timer_timeout():
	# ゲーム開始シグナルを発信
	emit_signal('game_start')

titleシーンを実行し、キャラクターアニメーションが再生されれば成功です。

タイトル画面のUIアニメーション

画面がインタラクション可能であることを示すために、「CLICK START」のテキストを点滅させます。
titleシーン内のStartLabelノードにスクリプトをアタッチします。

StartLabel.gd
extends Label

func _ready():
	# 点滅アニメーション
	var tween = get_tree().create_tween()
	tween.tween_property(self, "modulate", Color(1,1,1,0), 1).set_trans(Tween.TRANS_CUBIC)
	tween.tween_property(self, "modulate", Color(1,1,1,1), 1).set_trans(Tween.TRANS_CUBIC)
	tween.set_loops()

titleシーンを再生し、「CLICK START」のテキストが点滅していることを確認します。

ゲーム画面のスコアのUIアニメーション

UIとして認識させる目的で画面切り替え直後に位置アニメーションを挟みます。
gameシーンのScoreLabelのスクリプトを編集します。

ScoreLabel.gd
extends Label

func _ready():	
	# 位置初期化
	position = Vector2(0,-20)
	
	# 位置のアニメーション
	var tween = get_tree().create_tween()
	tween.tween_property(self, "position", Vector2(0, 0), 0.2).set_ease(Tween.EASE_OUT).set_trans(Tween.TRANS_BACK).set_delay(1)

~~~以下略~~~

リザルト画面のUIアニメーションとキャラクターアニメーション

洗練した印象を与えるために、流れるようなUIアニメーションをリザルト画面に実装します。

リザルトの星表示アニメーション

新規シーンを追加し、ルートノードとして「TextureRect」を選択、「ResultStar」と名前変更してシーンを保存します。
インスペクターよりTextureに「result_star.png」を読み込みます。

次にresultシーンを開きます。
ResultScoreControlの子ノードに「HBoxContainer」ノードを追加し、「HBoxResultStarContainer」と名前変更します。
アンカーのプリセットで中央水平伸長を設定し
インスペクターのAlignmentで「Center」を指定、
Layout > Transform > Size > y に11を指定します。
位置をスコアのテキストの少し上に配置します。

HBoxResultStarContainerにスクリプトをアタッチし、スコアに応じて星をいくつか表示するアニメーションを実装します。

HBoxResultStarContainer.gd
extends HBoxContainer

# 星
@export var result_star: PackedScene

func animate():
	await get_tree().create_timer(0.5).timeout
	
	# 表示する星の数を決める
	var star_count = 0
	if Score.value < 100:
		star_count = 1
	elif Score.value < 300:
		star_count = 2
	elif Score.value < 500:
		star_count = 3
	elif Score.value < 1000:
		star_count = 4
	elif Score.value < 1500:
		star_count = 5
	elif Score.value < 2000:
		star_count = 6
	else:
		star_count = 7
	
	for i in star_count:
		await get_tree().create_timer(0.25).timeout
		var star = result_star.instantiate()
		add_child(star)
		
	await get_tree().create_timer(0.25).timeout

スクリプト実装後、HBoxResultStarContainerのインスペクターでResult Starにresult_star.tscnを読み込ませます。

スコアのペイントアニメーションと位置アニメーション

resultシーン内の
ResultScoreLabelの目のアイコンをクリックして非表示にします。

ResultScoreControlの子ノードとして「ColorRect」を2つ追加し、それぞれ
「PaintBackColorRect」「PaintFrontColorRect」と名前変更します。

インスペクターで設定を行います。

  • PaintBackColorRect
    • Color #c3565c
  • PaintFrontColorRect
    • Color #fbf236
  • 2つとも共通に設定
    • Layout > Transform > Size を x:100 y:20
    • アンカーのプリセットとして「中央」を選択

ResultScoreControlにスクリプトをアタッチし、スコアテキストの上に被せる形でペイントアニメーションを実装します。

ResultScoreControl.gd
extends Control

func _ready():
	# ペイントアニメーションのアンカーの初期設定
	$PaintBackColorRect.offset_right = -50
	$PaintFrontColorRect.offset_right = -50

func animate():
	# ペイントアニメーション
	var tween = get_tree().create_tween()
	tween.set_parallel(true)
	tween.tween_property($PaintBackColorRect, "offset_right", 50, 0.25).set_trans(Tween.TRANS_CIRC)
	tween.tween_property($PaintFrontColorRect, "offset_right", 50, 0.25).set_trans(Tween.TRANS_CIRC).set_delay(0.1)
	await tween.finished
	
	# スコアテキスト表示
	$ResultScoreLabel.show()
	
	# ペイントアニメーション
	tween = get_tree().create_tween()
	tween.set_parallel(true)
	tween.tween_property($PaintFrontColorRect, "offset_left", 50, 0.25).set_trans(Tween.TRANS_CIRC)
	tween.tween_property($PaintBackColorRect, "offset_left", 50, 0.25).set_trans(Tween.TRANS_CIRC).set_delay(0.1)
	await tween.finished
	
	# 位置アニメーション
	tween = get_tree().create_tween()
	tween.tween_property(self, "position", Vector2(0, 50), 0.5).set_trans(Tween.TRANS_CIRC)
	await tween.finished

Retryラベルの点滅

「RETRY CLICK」のテキストを点滅をさせます。
また、スコアのペイントアニメーション中はスコアの部分に集中して見てほしいので、Resultシーン遷移直後は画面下部に隠しておきます。
RetryLabelにスクリプトをアタッチします。

RetryLabel.gd
extends Label

func _ready():
	position = Vector2(0, 410)
	
	# 点滅
	var tween = get_tree().create_tween()
	tween = get_tree().create_tween()
	tween.tween_property(self, "modulate", Color(1,1,1,1), 1).set_trans(Tween.TRANS_CUBIC)
	tween.tween_property(self, "modulate", Color(1,1,1,0), 1).set_trans(Tween.TRANS_CUBIC)
	tween.set_loops()

func animate():
	var tween = get_tree().create_tween()
	tween.tween_property(self, "position", Vector2(0,180), 1.0).set_trans(Tween.TRANS_CIRC)

リザルト画面のキャラクターアニメーション

おまけの演出としてタイトル画面と同じようにキャラクターアニメーションをつけます。
Resultノードの子ノードとして「AnimatedSprite2D」ノードを追加し「ResultCharacterAnimatedSprite2D」と名前変更します。

(スプライトアニメーションについてはタイトル画面と同じなので箇条書きで省略させていただきます。)

  • Transform > Positionを 68×120に設定
  • 新規SpriteFramesを作成
  • defaultとdashのアニメーションを作成
  • FPSは12フレームレート
  • defaultに読み込み後自動再生をチェックする
  • character.pngをSpriteを読み込み、48×48pxで読み込む

ResultCharacterAnimatedSprite2Dにスクリプトをアタッチし、アニメーションを実装します。

ResultCharacterAnimatedSprite2D.gd
extends AnimatedSprite2D

func _ready():
	# 初期位置に配置
	position = Vector2(204, 120)
	
func animate_in():
	# 位置アニメーション
	var tween = get_tree().create_tween()
	tween.tween_property(self, "position", Vector2(68, 120), 0.75).set_trans(Tween.TRANS_CIRC)

func animate_out():
	# スプライトアニメーション変更
	animation = "dash"
	
	# 位置アニメーション
	var tween = get_tree().create_tween()
	tween.tween_property(self, "position", Vector2(-204, 120), 0.75).set_trans(Tween.TRANS_CIRC)

result.gdから各種アニメーションを動作させるようにする

result.gd
extends Node2D

signal game_retry

func _ready():
	await $ResultUI/ResultScoreControl/HBoxResultStarContainer.animate()
	await $ResultUI/ResultScoreControl.animate()
	$ResultCharacterAnimatedSprite2D.animate_in()
	$ResultUI/RetryLabel.animate()

func _input(event):
	if event is InputEventMouseButton && event.is_pressed():
		# タイマー開始
		$GameRetryTimer.start()
		
		# キャラクターアニメーション
		$ResultCharacterAnimatedSprite2D.animate_out()

func _on_game_retry_timer_timeout():
	# ゲームリトライシグナル発信
	print('シグナル game_retry')
	emit_signal('game_retry')

mainシーンからゲームをプレイし、リザルト画面で流れるようにアニメーションされるか確認してみましょう。

背景を作る

「Yozora Maker」は夜空がテーマとなっているので、ここでは簡易的に星空っぽい背景を作ります。
mainシーンのMainノードの子ノードとして「TileMap」を追加します。
TileMapのインスペクターでTile Setに新規TileSetを設定します。

background.pngをタイル編集エリアにドラック&ドロップし、タイルを作成します。

作成したタイルを画面上に配置し、星空っぽくしましょう。(あまりキラキラしすぎていると逆にゲームプレイの視認性が悪くなるので適度に)

サウンド

最後に音を入れて仕上げましょう。

BGM

mainシーンのMainノードの子ノードとして「AudioStreamPlayer2D」を追加し、「BGM」と名前を変更します。
インスペクターからStreamに「bgm.mp3」を読み込ませ、Autoplayをオンにします。

SE

タイトル画面

画面タップ時に効果音を鳴らします。
titleシーンのTitleノードの子ノードとして「AudioStreamPlayer2D」を追加し、「StartSE」と名前変更します。
Streamに「start.mp3」を読み込ませます。
Titleノードのスクリプトを編集します。

title.gd
extends Node2D

signal game_start

func _input(event):
	if event is InputEventMouseButton && event.is_pressed():
		# タイマーを開始
		$GameStartTimer.start()
		
		# キャラクターアニメーション
		$TitleCharacterAnimatedSprite2D.animate_out()
		
		# ゲーム開始SEを再生
		$StartSE.play()

func _on_game_start_timer_timeout():
	# ゲーム開始シグナルを発信
	emit_signal('game_start')

ゲーム画面

starシーンのStarノードの子ノードとして「AudioStreamPlayer2D」を追加し、「CollisionSE」と名前変更します。
Streamに「collision.mp3」を読み込ませます。
Starノードのスクリプトを編集します。

star.gd
extends CharacterBody2D

# 移動の速さ
var move_speed: float = 25.0

func _physics_process(delta):
	# 衝突判定
	var collision = move_and_collide(velocity * delta)
	
	# 星か画面端と当たったら反射
	if collision:
		velocity = velocity.bounce(collision.get_normal())
		$CollisionSE.play()

~~~以下略~~~

playerシーンのPlayerノードの子ノードとして「AudioStreamPlayer2D」を3つ追加し、「ShootSE」「GrazeSE」「DeathSE」と名前変更します。
それぞれのStreamで対応するmp3ファイルを読み込みます。
Playerノードのスクリプトを編集します。

player.gd
extends CharacterBody2D

~~~中略~~~

func emit_add_score():
	# 効果音再生
	if $PlayerGrazeArea2D.grazing_count >= 1:
		$GrazeSE.play()
	else:
		$ShootSE.play()
	
	# 現在の星の数を取得
	var star_count = get_tree().get_nodes_in_group('stars').size()
	
	# スコア追加シグナルを発信する
	print('シグナル add_score')
	emit_signal("add_score", star_count, $PlayerGrazeArea2D.grazing_count)

func _on_player_life_area_2d_body_entered(body):
	# 死亡判定
	is_dead = true
	
	# 発射矢印隠す
	$PlayerShootDirector.hide()
	
	# ヒットエフェクト再生
	var hit_effect = hit_scene.instantiate()
	hit_effect.z_index = -2
	hit_effect.position = body.position
	add_sibling(hit_effect)
	
	# ヒットSE再生
	$DeathSE.play()
	
	# プレイヤー死亡シグナル
	print('シグナル player_death')
	emit_signal('player_death')

~~~以下略~~~

リザルト画面

resultシーンのHBoxResultStarContainerの子ノードとして「AudioStreamPlayer2D」を2つ追加し、「ShootSE」「GrazeSE」と名前変更します。
インスペクターのStreamに、名前に対応するmp3を読み込ませます。

HBoxResultStarContainerノードのスクリプトを編集します。

HBoxResultStarContainer.gd
extends HBoxContainer

# 星
@export var result_star: PackedScene

func animate():
	~~~中略~~~
	
	for i in star_count:
		await get_tree().create_timer(0.25).timeout
		var star = result_star.instantiate()
		add_child(star)
		
		# 星の数によって効果音を変える
		if i <= 3:
			$ShootSE.play()
		else:
			$GrazeSE.play()
		
	await get_tree().create_timer(0.25).timeout

ResultScoreControlノードの子ノードに「AudioStreamPlayer2D」を追加し、「ResultSE」と名前変更します。
インスペクターのStreamに名前に対応したmp3ファイルを読み込ませます。

ResultScoreControlノードのスクリプトを編集します。

ResultScoreControl.gd
extends Control

func _ready():
	# ペイントアニメーションのアンカーの初期設定
	$PaintBackColorRect.offset_right = -50
	$PaintFrontColorRect.offset_right = -50

func animate():
	# 効果音
	$ResultSE.play()
	
	~~~以下略~~~

Resultノードの子ノードに「AudioStreamPlayer2D」を追加し、「StartSE」と名前変更します。
インスペクターのStreamに名前に対応したmp3ファイルを読み込ませます。

Resultノードのスクリプトを編集します。

result.gd
extends Node2D

~~~中略~~~

func _input(event):
	if event is InputEventMouseButton && event.is_pressed():
		# タイマー開始
		$GameRetryTimer.start()
		
		# キャラクターアニメーション
		$ResultCharacterAnimatedSprite2D.animate_out()
		
		# 効果音
		$StartSE.play()

~~~以下略~~~

mainシーンを実行し通しプレイをして、サウンドが入っていることを確認します。

終わりに

以上でこのチュートリアルは終わりです。まだまだ改善できそうな箇所はありそうですが、それなりにゲームっぽくなったのではないでしょうか。
余裕がある方は背景をスクロールさせてみたり、そもそものゲームデザインを変えてみるのにtryしてみるのもいいかと思います。

Discussion