Part 13 の今回は、ブロック崩しの HUD にハイスコア、ハイレベル(過去最高クリアレベルのことをこう呼ぶことにする)の要素を追加し、ゲームオーバーになった時点でそのデータが自動的に保存されるようにして、一度ゲームを終了しても記録が消えない仕組みを作っていく。


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


HUD をアップデートする

今回、「ハイスコア」と「ハイレベル」の要素をゲームに追加する。これに伴い、HUD にもそれぞれのノードを追加して、プロパティを編集していく。

では、「Game.tscn」シーンを開いて手順を進めていこう。


ノードを追加する

「HUD」ノードの「LeftBox」ノードの次に、新たに「VBoxContainer」クラスのノードを追加し、名前を「MidBox」に変更しよう。
さらに「MidBox」ノードには「Label」クラスのノードを 2 つ追加し、それぞれの名前を「HighScore」、「HighLevel」に変更しよう。これで「HUD」ノードの構成は以下のようになったはずだ。

HUDの構成


追加したノードのプロパティを編集する

「LeftBox」ノードと「MidBox」ノードの位置と枠の大きさを編集する。HUD のレイアウトは、ブロックの配置の都合上、全て画面上端に並べる必要がある。

2D ワークスペースで「MidBox」をドラッグして画面上端中央に配置しても問題ないが、インスペクタドックで数値を調整するならそれぞれのノードで「Margin」プロパティが以下の値になるようにしよう。

  • 「LeftBox」ノード
    LeftBoxのMarginプロパティ
  • 「MidBox」ノード
    MidBoxのMarginプロパティ

これで、画面上端中央に「MidBox」ノードが配置された。

次にインスペクタドックで、「MidBox」ノードの子ノードである「HighScore」ノードと「HighLevel」ノードの「Text」プロパティを設定する。

  • 「HighScore」ノードの「Text」プロパティ:「H Scr: 0」
  • 「HighLevel」ノードの「Text」プロパティ:「H Lvl: 1」

この時点で、ゲーム画面は以下のようになったはずだ。

ゲーム画面でHUDの見た目を確認


Game.gd スクリプトを更新する

HUD の見た目は更新できたので、次はスクリプトで表示される値を制御していく。ここで、ゲームデータを保存する機能と保存されたゲームデータを読み込む機能を新たに実装していく。

ではここから「Game.gd」スクリプトを開いて編集していこう。


必要な定数と変数を追加する

まずは新たにいくつか変数を定義していく。「# 追加」のコメントを記述している行が今回定義した定数と変数だ。

extends Node2D

const POINT = 100
const MAX_LIFE = 5
const SCORE_FILE_PATH = "user://score_record.save" # 追加

export var drop_rate = 0.2

var level_num: int = 1
var high_level_num: int = 0 # 追加
var score: int = 0
var high_score: int = 0 # 追加

#(中略)

onready var hud_level = $HUD/LeftBox/Level
onready var hud_high_level = $HUD/MidBox/HighLevel # 追加
onready var hud_score = $HUD/LeftBox/Score
onready var hud_high_score = $HUD/MidBox/HighScore # 追加

#(後略)

まずconstキーワードで定義しているのはSCORE_FILE_PATHという定数だ。その値を"user://score_record.save"としてデータの保存先となるファイルパスを指定した。

このように、データを保存するファイルの頭にuser://をつけるとプロジェクトの所定の場所にデータを保存してくれる。このファイルにデータを書き込めば、シーンを切り替えたり、ゲームを一度終了してから再開しても、ファイルからデータを読み込んで、以前プレイした時の状態を引き継ぐことが可能だ。

次に変数high_level_numだが、これはゲーム開始時に前回までのハイレベルを渡しておくためのものだ。同様に、もう一つの変数high_scoreには、ゲーム開始時に前回までのハイスコアを渡しておく。また、ゲームオーバーの時に、記録が更新される際にも利用する。

続いてonreadyキーワード付きの変数だが、これらは特定のノードを指すのに使っている。hud_high_levelは「HighLevel」ノードを、hud_high_scoreは「HighScore」ノードを指している。


保存用のメソッドを追加する

引き続き「Game.gd」スクリプトに、ゲームのデータを保存するためのメソッドを追加する。

func save_data():
	var data = {
	"last_level": level_num,
	"high_level": high_level_num,
	"last_score": score,
	"high_score": high_score,
	}
	var file = File.new()
	file.open(SCORE_FILE_PATH, File.WRITE)
	file.store_line(to_json(data))
	file.close()

save_dataというメソッドを定義した。まず変数dataをメソッド内で定義している。メソッド内で定義した変数は、そのメソッドの外では使えないので注意してほしい。dataは辞書型データで{"key": value}の組み合わせで複数のデータをまとめることができる。

ここではデータとして保存したい値を格納したlevel_numhigh_level_numscorehigh_scoreの4つの変数それぞれを Value とし、対応する Key を"last_level"、“high_level”、“last_score”、“high_score"として定義した。

そのあとはfileという変数を定義している。newメソッドを実行して作成されたFileクラスのノードを値として代入している。これ以降Fileクラスのメソッドが続く。

データを保存するには保存先のファイルを開く必要がある。openは第一引数で指定したパスのファイルを開くメソッドだ。ここでは、先に定義した定数SCORE_FILE_PATH"user://score_record.save")を第一引数として渡している。第二引数にはファイルを開くときのモードを指定する。ここではデータを書き込んで保存したいので、WRITEを指定して書き込みモードにしている。

次にstore_lineメソッドでファイルにデータを書き込んでいる。store_xxxという名前のメソッドは他にもいくつかあるが、このstore_lineは1行のstring型データをファイルに書き込む。そして、store_lineの引数が書き込むデータになるが、ここでは先に定義した辞書型の変数datato_jsonで変換した値を指定している。dataは辞書型データで、スクリプト上は見やすさのため改行しているが、実際には1行のデータだ。

ここでto_jsonについても説明しておく。まず「JSON」というのは「JavaScript Object Notation」の略で、JavaScript というプログラミング言語でのオブジェクトの記述方式で保存されたデータフォーマットだ。そして、その構造は GDScript を含む各種プログラミング言語の辞書型データにもそっくりの{"key": value}という形式だ。先に保存したいデータを辞書型でまとめておき、保存データを指定するときにその辞書型データをto_jsonメソッドの引数にして、JSON形式に変換して保存するようにしたというわけだ。

最後にcloseメソッドでファイルを閉じている。開きっぱなしにして、メモリを消耗しないよう注意しよう。


読み込み用のメソッドを追加する

「Game.gd」スクリプトで、今度は保存したデータを読み込むメソッドを追加する。

func load_data():
	var file = File.new()
	if file.file_exists(SCORE_FILE_PATH):
		file.open(SCORE_FILE_PATH, File.READ)
		var data = parse_json(file.get_line())
		if data != null:
			high_score = data["high_score"]
			high_level_num = data["high_level"]
		file.close()
	hud_high_score.text = "H Scr: " + str(high_score)
	hud_high_level.text = "H Lvl: " + str(high_level_num)

データを読み込むのはload_dataというメソッドだ。

先ほどの書き込みメソッドsave_dataと同様に、まずはFileクラスのnewメソッドにより、Fileクラスのノードを作成する。そこからまたFileクラスのメソッドが続くが順番に見ていこう。

まずはif構文で、file_existsメソッドによりデータ保存先のファイル(定数SCORE_FILE_PATHの値)があるか確認している。ファイルがあればifブロックの中の処理を続行する。

ではifブロックの中のコードを見ていこう。まず、ファイルをopenメソッドで開く。ここではモードをREADにして読み込みモードにしている。

次はget_lineメソッドでファイルに保存されている一行のデータを読み込む。この時、この読み込んだデータはJSON形式なので、これをparse_jsonメソッド GDScript の辞書型データに変換して、変数dataに格納している。

2段回目のif構文だが、もし変数dataが空っぽのnullでなければ、変数high_scoreにファイルに保存しているハイスコアのデータを、変数high_level_numにはファイルに保存しているハイレベルのデータを代入する。

そこまでできたら、忘れずにファイルをcloseメソッドで閉じる。

最後に、ファイルから得たそれぞれのデータをstring形に変換して、頭にH Str: H Lvl: を付け足して、HUD の「HighScore」ノードと「HighLevel」ノードそれぞれの「Text」プロパティに適用している。


追加したメソッドを適切なタイミングで実行させる

データを保存するメソッドと保存したデータを読み込むメソッドを用意できたので、それらのメソッドを適切なタイミングで実行するようにコードを更新していく。

まずは_readyメソッドを編集する。

func _ready():
	randomize()
	add_new_level()
	add_new_ball()
	update_hud_life()
	load_data() # 追加

単純に、先ほど作成したload_dataメソッドを追加しただけだ。これで、ゲーム開始前にデータを保存しているファイルからハイスコアやハイレベルの値を取得して HUD に反映してくれる。


次に_on_Ball_tree_exitedメソッドを編集する。ちなみにこのメソッドは、ボールオブジェクトが消える時に発信されるシグナルによって呼ばれる。

func _on_Ball_tree_exited():
	print("_on_Ball_tree_exited() called")
	var no_ball = true
	for child in get_children():
		if child.is_in_group("Balls"):
			print("found ball")
			no_ball = false
			break
	
	if no_ball:
		if is_playing:
			life -= 1
			if life <= 0:
				if high_score < score: # 追加
					high_score = score
				if high_level_num < level_num: # 追加
					high_level_num = level_num
				save_data() # 追加
				get_tree().change_scene("res://scene/GameOverView.tscn")
			else:
				update_hud_life()
				life_down_sound.play()

		#(後略)

このメソッド内のif no_ball:でネストされたif is_playing:でさらにネストされたif life <= 0:ブロック内を編集する。このブロック内のコードは、『画面からボールが一つも無くなった場合』、『ゲームプレイ中であり』、『ライフが 0』の場合に実行される。つまり、ゲームオーバーになる場合だ。

元々このブロックには、シーンを「GameOverView.tscn」に切り替えて、ゲームオーバー画面に遷移するためのコードget_tree().change_scene("res://scene/GameOverView.tscn")のみを記述していた。今回はそのコードの前に、ハイスコアとハイレベルを更新するコードを追加した。

具体的には、これまでのハイスコアを表す変数high_scoreの値が今プレイしていた時のスコアを表す変数scoreの値より小さければ、scoreの値を新しいハイスコアとしてhigh_scoreへ代入する、というプログラムをif構文で記述した。

同様に、これまでのハイレベルを表す変数high_level_numの値が今プレイしていた時のレベルを表す変数level_numの値より小さければ、level_numの値を新しいハイレベルとしてhigh_level_numへ代入するよう記述した。



ゲームオーバー画面を更新する

次にハイスコア、ハイレベルの表示が必要なのがゲームオーバー画面だ。プレイヤーの心情としては、ゲームオーバーになった時に、最終的に自分のスコアがどの程度でどのレベルまで到達したのかを確認したい。さらに、その結果はこれまでの最高記録を塗り替えたのかどうかも知りたいものだ。

したがって、ゲームオーバー画面には以下の4つの結果を表示するようにしていく。

  • ハイスコア(過去最高獲得スコア)
  • ラストスコア(今のプレイで最終的に獲得したスコア)
  • ハイレベル(過去最高到達レベル)
  • ラストレベル(今のプレイで最終的に到達したレベル)

それでは「GameOverView.tscn」シーンを開いて編集していこう。


ノードを追加して、プロパティを編集する

以下の手順で、シーンドックおよびインスペクタドックにて、必要なノードを追加し、プロパティを編集していこう。

  1. シーンドックで「VBox」ノードに「VBoxContainer」クラスのノードを一つ追加して、名前を「ResultsContainer」に変更する。

  2. シーンドックで「ResultsContainer」ノードの順番を「GameOverLavel」ノードの下、「Message」ノードの前に移動する。
    シーンドック上のResultsContainerノードの順番

  3. インスペクタドックで「ResultsContainer」ノードの「Custom Constants」>「Separation」プロパティを10にして有効にする。
    ResultsContainerのSeparationプロパティ

  4. シーンドックで「ResultsContainer」ノードに「Label」クラスのノードを 1 つ追加し、名前を「HighScore」に変更する。
    HighScoreノードの追加

  5. インスペクタドックで「HighScore」ノードの「Align」プロパティを「Center」に変更する。
    HighScoreノードのAlignプロパティ

  6. インスペクタドックで「HighScore」ノードの「Uppercase」プロパティを「オン」にする。
    HighScoreノードのUppercaseプロパティ

  7. インスペクタドックで既存の「Message」ノードの「Custom Fonts」>「Font」をコピーする。
    MessageノードのDynamicFontをコピー

  8. インスペクタドックで「HighScore」ノードの「Custom Fonts」>「Font」に貼り付けする。
    HighScoreノードのDynamicFontに貼り付け

  9. さらに「HighScore」ノードの「Custom Fonts」>「Font」をユニーク化する。
    HighScoreノードのDynamicFontをユニーク化

  10. さらに「HighScore」ノードの「Custom Fonts」>「Settings」>「Size」プロパティを16にする。
    HighScoreノードのDynamicFontのSizeを編集

  11. シーンドックで「HighScore」ノードを3つ複製し、それらの名前を上から順番に「LastScore」、「HighLevel」、「LastLevel」に変更する。この時点でシーンドックは以下のようになる。
    GameOverView.tscnノード追加後のシーンドック

  12. インスペクタドックで「HighScore」ノードの「Text」プロパティを「High Score: 0」にする。
    HighScoreノードのTextプロパティを編集

  13. 同様に「LastScore」ノードの「Text」プロパティを「Last Score: 0」にする。

  14. 同様に「HighLevel」ノードの「Text」プロパティを「High Level: 0」にする。

  15. 同様に「LastLevel」ノードの「Text」プロパティを「Last Level: 0」にする。

ここまでの手順ができたら、2D ワークスペースから以下のようになっているのを確認できるはずだ。
GameOverView編集後の2Dワークスペース


GameOverView.gd スクリプトを更新する

ゲームオーバー画面のノードの追加と編集ができたので、ここからはスクリプトでゲームオーバー画面に切り替わる際のデータの読み込みを実装していく。ちなみに、ゲームオーバー画面なので、データの保存機能は不要だ。

それでは「GameOverView.gd」スクリプトを開いて編集していこう。


必要な変数を追加する

データの読み込みとその読み込んだデータを画面に反映させるために、まずは変数をいくつか追加しておく。

const SCORE_FILE_PATH = "user://score_record.save"

onready var high_score = $VBox/ResultsContainer/HighScore
onready var last_score = $VBox/ResultsContainer/LastScore
onready var high_level = $VBox/ResultsContainer/HighLevel
onready var last_level = $VBox/ResultsContainer/LastLevel

#(後略)

constキーワードで定数SCORE_FILE_PATHを定義した。値はデータが保存されいているファイルのパスだ。「Game.gd」スクリプトでも同じ定数を定義したところである。

次にonreadyキーワード付きの変数をhigh_scoreをはじめとして 4 つ定義しているが、これらは今回追加した「Label」クラスのそれぞれのノードを指している。


読み込み用のメソッドを追加する

ほどんど「Game.gd」スクリプトで実装した内容と同じになるが、「GameOverView.gd」スクリプトの方でも、データ読み込み用のメソッドを追加する必要がある。

func load_data():
	var file = File.new()
	if file.file_exists(SCORE_FILE_PATH):
		file.open(SCORE_FILE_PATH, File.READ)
		var data = parse_json(file.get_line())
		if data != null:
			high_score.text = "High Score: " + str(data["high_score"])
			last_score.text = "Last Score: " + str(data["last_score"])
			high_level.text = "High Level: " + str(data["high_level"])
			last_level.text = "Last Level: " + str(data["last_level"])
		file.close()

laod_dataという名前で読み込み用メソッドを定義した。if data != null:の行までは「Game.gd」スクリプトと全く同じだが、そのifブロックの中のコードがゲームオーバー画面仕様になっている。

このメソッドのコードを要約すると、データ保存先のファイルがあればファイルを開き、そのあとデータが空っぽでなければ、単純に「HighScore」、「LastScore」、「HighLevel」、「LastLevel」それぞれのノードの「Text」プロパティに、ファイルから取得したJSONデータを最終的にstring型に変換して反映する。もしファイルがない、またはデータがなければ、「Text」プロパティの値はデフォルトの設定で表示される。

なお、繰り返しになるが、最後のファイルを閉じるcloseメソッドは忘れてはいけない。


追加したメソッドを適切なタイミングで実行させる

読み込み用メソッドが定義できたので、これを_readyメソッド内で実行させる。コードは至ってシンプルだ。

func _ready():
	load_data()

これだけで、ゲームオーバー画面に遷移した時にファイルの読み込みに失敗さえしなければ、きちんと画面上にデータを表示してくれるはずだ。今はまだデータファイルが作成されていないので、シーンを実行するとデフォルトの値で表示される。
GameOverViewシーンを実行



最終確認

それでは最後にデータの保存とデータの読み込みがうまくいくかプロジェクトを実行して確認してみよう。

GameOverViewシーンを実行

  • 初回プレイ時、ライフが 0 になったタイミングでデータがファイルに保存され、ゲームオーバー画面に遷移した時にそのファイルのデータが読み込まれ画面上に結果が表示された。
  • 2回目、3回目プレイ時は、最初からファイルのデータが読み込まれ HUD 中央のハイスコアに反映された。
  • 2回目プレイで初回より高いスコアを獲得してからゲームオーバーになった時、画面には更新されたハイスコアが表示された。
  • 3回目プレイで2回目プレイより低いスコアでゲームオーバーになった時、画面には2回目プレイ時のハイスコアが表示された。
  • 1 ~ 3回目のプレイでのゲームオーバー画面でその時々のスコアが「Last Score」に反映された。

以上のことが確認できたので、今回のデータの保存と読み込みは問題なく実装できたと判断できる。なお、レベルの方の確認はカットするが、こちらも問題ないはずだ。

ところで、実際にデバッグ作業を行う中で、作成されたデータファイルを削除したい場合も発生するだろう。その場合は、ご利用のOSのファイルマネージャー(Windows なら Explorer、macOS なら Finder)から保存先のフォルダパスへアクセスして削除してほしい。なお、データの保存先は以下の通りだ。

  • Windows: %APPDATA%¥Godot¥app_userdata¥[ProjectName]¥
  • macOS: ~/Library/Application Support/Godot/app_userdata/[ProjectName]/
  • Linux: ~/.local/share/godot/app_userdata/[ProjectName]/

Memo:
詳細は公式ドキュメントの File paths in Godot projects の項目をご参照ください。


最後に今回編集した「Game.gd」と「GameOverView.gd」のスクリプト全体のコードもここに公開しておく。

「Game.gd」スクリプト全体を見る
extends Node2D

const POINT = 100
const MAX_LIFE = 5 # Added @ P11
const SCORE_FILE_PATH = "user://score_record.save" # Added @ P13

export var drop_rate = 0.2

var level_num: int = 1
var high_level_num: int = 0 # Added @ P13
var score: int = 0
var high_score: int = 0 # Added @ P13
var bonus_rate = 1.0
var life = 3
var is_playing = true
var is_multiple_on = false # Added @ P11
var is_laser_on = false # Added @ P11

#onready var level = $Level1 # Removed @ P11
onready var next_screen = $NextScreen
onready var next_screen_level = $NextScreen/VBox/Level
onready var next_screen_score = $NextScreen/VBox/Score
onready var next_screen_life = $NextScreen/VBox/HBox/Life
onready var hud_level = $HUD/LeftBox/Level
onready var hud_high_level = $HUD/MidBox/HighLevel # Added @ P13
onready var hud_score = $HUD/LeftBox/Score
onready var hud_high_score = $HUD/MidBox/HighScore # Added @ P13
onready var hud_rightbox = $HUD/RightBox
onready var paddle = $Paddle
#onready var ball = $Ball # Removed @ P11
onready var pause_screen = $PauseScreen
onready var life_down_sound = $LifeDownSound
onready var slow_collide_sound = $SlowCollideSound
onready var expand_collide_sound = $ExpandCollideSound
onready var multiple_collide_sound = $MultipleCollideSound
onready var laser_collide_sound = $LaserCollideSound
onready var life_collide_sound = $LifeCollideSound
onready var play_bgm = $PlayBGM

onready var paddle_position = paddle.position
onready var paddle_scale = paddle.scale # Added @ P11
#onready var ball_position = ball.position # Removed @ P11
onready var ball = preload("res://scene/Ball.tscn") # Added @ P11
onready var laser = preload("res://scene/Laser.tscn") # Added @ P11
onready var powerup = preload("res://scene/Powerup.tscn") # Added @ P10
onready var level = null # Updated @ P11

func _ready():
	randomize() # Added @ P10
	add_new_level() # Added @ P11
	add_new_ball() # Added @ P11
	update_hud_life()
	load_data() # Added @ P13
	# For debug
	#leave_one_brick(43) # Moved @ P11
	#ball.connect("tree_exited", self, "_on_Ball_tree_exited") # Removed @ P11
	#for brick in level.get_children(): # Removed @ P11
		#brick.connect("tree_exited", self, "_on_Brick_tree_exited", [brick.global_position]) # Updated @ P10


func _process(_delta): # Added @ P11
	if is_multiple_on and Input.is_action_just_pressed("launch_ball"):
		add_new_ball()
	if is_laser_on and Input.is_action_just_pressed("ui_up"):
		fire_laser()


# For debug
func leave_one_brick(brick_num: int):
	for child in level.get_children():
		if child.get_name() == "Brick" + str(brick_num):
			continue
		child.queue_free()

# Method receiving Ball signal
func _on_Ball_tree_exited():
	print("_on_Ball_tree_exited() called")
	var no_ball = true # Added @ P11
	for child in get_children(): # Added @ P11
		if child.is_in_group("Balls"):
			print("found ball")
			no_ball = false
			break
	
	if no_ball: # Added and Edited @ P11
		if is_playing:
			life -= 1
			if life <= 0:
				if high_score < score: # Added @ P13
					high_score = score
				if high_level_num < level_num: # Added @ P13
					high_level_num = level_num
				save_data() # Added @ P13
				get_tree().change_scene("res://scene/GameOverView.tscn")
			else:
				update_hud_life()
				life_down_sound.play()
		else:
			is_playing = true
		# Clear powerup items
		for child in get_children(): # Added @ P10
			if child.is_in_group("PowerupItems"):
				child.queue_free()
		# Set Paddle and Balls as default
		is_multiple_on = false # Added @ P11
		is_laser_on = false # Added @ P11
		paddle.position = paddle_position
		paddle.scale = paddle_scale # Added @ P11
		add_new_ball() # Added @ P11
		#ball = load("res://scene/Ball.tscn").instance() # Removed @ P11
		#call_deferred("add_child", ball) # Removed @ P11
		#call_deferred("move_child", ball, 3) # Removed @ P11
		#ball.connect("tree_exited", self, "_on_Ball_tree_exited") # Removed @ P11


# Set life nodes shown and hidden as life variable
func update_hud_life():
	var count = 0
	for child in hud_rightbox.get_children():
		count += 1
		if count <= life:
			child.show()
		else:
			child.hide()


# Add a new ball
func add_new_ball(): # Added @ P11
	print("add_new_ball() called")
	var instance = ball.instance()
	instance.position = Vector2(paddle.position.x, paddle.position.y - 10) # Added @ P11
	call_deferred("add_child", instance)
	call_deferred("move_child", instance, 4)
	instance.connect("tree_exited", self, "_on_Ball_tree_exited")	
	#instance.mode = 3 # Removed @ P11


# Method receiving Brick signal
func _on_Brick_tree_exited(brick_position):
	# Update Score
	score += POINT * bonus_rate
	bonus_rate += 0.1
	hud_score.text = "Score: " + str(score)

	# Exit current Level node
	if level.get_child_count() <= 0:
		set_next_level()
	else: # Added @ P11
		# Drop powerup item
		drop_powerup(brick_position) # Added @ P10


func drop_powerup(brick_position: Vector2): # Added @ P10
	if randf() <= drop_rate:
		var powerup_instance = powerup.instance()
		powerup_instance.position = brick_position
		call_deferred("add_child", powerup_instance)
		call_deferred("move_child", powerup_instance, 5) # Added @ P 11
		powerup_instance.connect("item_collided", self, "_on_Powerup_item_collided") 

# Action when powerup item collided
func _on_Powerup_item_collided(item): # Updated @ P11
	match item:
		0: # SLOW
			slow_balls()
		1: # EXPAND
			expand_paddle()
		2: # MULTIPLE
			enable_multiple_balls()
		3: # LASER
			enable_laser()
		4: # LIFE
			add_life()

# slow balls
func slow_balls(): # Added @ P11
	slow_collide_sound.play() # Added @ P12
	for child in get_children():
		if child.is_in_group("Balls"):
			child.ball_speed = child.first_speed

# Stretch paddle
func expand_paddle(): # Added @ P11
	expand_collide_sound.play() # Added @ P12
	if paddle.scale <= paddle_scale:
		paddle.scale.x *= 2
		yield(get_tree().create_timer(10), "timeout")
		paddle.scale = paddle_scale

# enable powerup Multiple
func enable_multiple_balls(): # Added @ P11
	multiple_collide_sound.play() # Added @ P12
	if not is_multiple_on:
		is_multiple_on = true
		yield(get_tree().create_timer(3), "timeout")
		is_multiple_on = false

# enable powerup Laser
func enable_laser(): # Added @ P11
	laser_collide_sound.play() # Added @ P12
	if not is_laser_on:
		is_laser_on = true
		yield(get_tree().create_timer(3), "timeout")
		is_laser_on = false

# fire laser beam
func fire_laser():
	var instance = laser.instance()
	call_deferred("add_child", instance)
	call_deferred("move_child", instance, 6)
	instance.position.x = paddle.position.x
	instance.position.y = paddle.position.y - 16

# Add a life if less than 5
func add_life(): # Added @ P11
	life_collide_sound.play() # Added @ P12
	if life < MAX_LIFE:
		life += 1
		update_hud_life()

# set next level
func set_next_level():
	print("set_next_level() called")
	# Change status
	is_playing = false
	is_multiple_on = false # Added @ P11
	is_laser_on = false # Added @ P11
	
	# Clear left objects
	level.queue_free()
	for child in get_children():
		if child.is_in_group("Balls") or child.is_in_group("Lasers"): # 追加
			child.queue_free()
	
	# Increment level number
	level_num += 1
	# Stop PauseScreen node
	pause_screen.pause_mode = 1
	# Show NextScreen node
	next_screen.pause_mode = 2
	next_screen_level.text = "Level: " + str(level_num)
	next_screen_score.text = "Score: " + str(score)
	next_screen_life.text = "x " + str(life)
	next_screen.show()
	# Set Level of HUD the next level
	hud_level.text = "Level: " + str(level_num)
	# Set Paddle and Ball the first position
	#paddle.position = paddle_position # Removed @ P11
	#paddle.scale = paddle_scale # Removed @ P11
	#ball.position = ball_position # Removed @ P11
	#ball.mode = 3 # Removed @ P11
	# Set next Level node
	add_new_level() # Added @ P11
	#level = load("res://scene/Level" + str(level_num) + ".tscn").instance() # Removed @ P11
	#add_child(level) # Removed @ P11
	#move_child(level, 5) # Removed @ P11
	#for child in level.get_children(): # Removed @ P11
		#child.connect("tree_exited", self, "_on_Brick_tree_exited") # Removed @ P11
	# Pause game until NextScreen is hidden
	get_tree().paused = true

# Add new level
func add_new_level(): # Added @ P11
	level = load("res://scene/Level" + str(level_num) + ".tscn").instance()
	add_child(level)
	move_child(level, 3) # Changed from 5 to 3 @ P11
	for child in level.get_children():
		child.connect("tree_exited", self, "_on_Brick_tree_exited", [child.global_position]) # Updated to add th 4th arg @ P11 


func _on_PauseScreen_visibility_changed():
	play_bgm.stream_paused = not play_bgm.stream_paused


# Save data
func save_data(): # Added @ P13
	var data = {
	"last_level": level_num,
	"high_level": high_level_num,
	"last_score": score,
	"high_score": high_score,
	}
	var file = File.new()
	file.open(SCORE_FILE_PATH, File.WRITE)
	file.store_line(to_json(data))
	file.close()


# Load data
func load_data(): # Added @ P13
	var file = File.new()
	if file.file_exists(SCORE_FILE_PATH):
		file.open(SCORE_FILE_PATH, File.READ)
		var data = parse_json(file.get_line())
		if data != null:
			high_score = data["high_score"]
			high_level_num = data["high_level"]
		file.close()
	hud_high_score.text = "H Scr: " + str(high_score)
	hud_high_level.text = "H Lvl: " + str(high_level_num)
  

「GameOverView.gd」スクリプト全体を見る
extends Control


const SCORE_FILE_PATH = "user://score_record.save" # Added @ P13

onready var high_score = $VBox/ResultsContainer/HighScore # Added @ P13
onready var last_score = $VBox/ResultsContainer/LastScore # Added @ P13
onready var high_level = $VBox/ResultsContainer/HighLevel # Added @ P13
onready var last_level = $VBox/ResultsContainer/LastLevel # Added @ P13
onready var sound = $KeySound


func _ready(): # Added @ P13
	load_data()
	

func _input(event):
	if event is InputEventKey:
		print("Input at Game Over: ", event.as_text())
		if event.is_action_released("Quit"):
			sound.play()
			yield(sound, "finished")
			get_tree().quit()
		elif event.is_action_released("ui_accept"):
			sound.play()
			yield(sound, "finished")
			get_tree().change_scene("res://scene/GameStartView.tscn")


func load_data(): # Added @ P13
	var file = File.new()
	if file.file_exists(SCORE_FILE_PATH):
		file.open(SCORE_FILE_PATH, File.READ)
		var data = parse_json(file.get_line())
		if data != null:
			high_score.text = "High Score: " + str(data["high_score"])
			last_score.text = "Last Score: " + str(data["last_score"])
			high_level.text = "High Level: " + str(data["high_level"])
			last_level.text = "Last Level: " + str(data["last_level"])
		file.close()

おわりに

以上で Part 13 は完了だ。今回はデータを保存する機能、保存したデータを読み込む機能を実装した。ゲームによって、保存したいデータは変わり、規模が大きくなるほど、保存するデータも複雑化するだろう。しかし、何事も基本が重要なので、今回のチュートリアルの内容をぜひ今後の開発に役立てていただければと思う。

次回 Part 14 ではブロックの種類(例えば一回の衝突では消えない硬いブロックなど)を増やして、レベルに応じて難易度の幅を広くできるように調整し、複数のレベルをデザインする。