Part 13 の今回は、ブロック崩しの HUD にハイスコア、ハイレベル(過去最高クリアレベルのことをこう呼ぶことにする)の要素を追加し、ゲームオーバーになった時点でそのデータが自動的に保存されるようにして、一度ゲームを終了しても記録が消えない仕組みを作っていく。
Memo:
過去のシリーズをまだご覧になっていない方は、そちらを先にご覧いただくことをおすすめします。
Godot で作るブロック崩し
HUD をアップデートする
今回、「ハイスコア」と「ハイレベル」の要素をゲームに追加する。これに伴い、HUD にもそれぞれのノードを追加して、プロパティを編集していく。
では、「Game.tscn」シーンを開いて手順を進めていこう。
ノードを追加する
「HUD」ノードの「LeftBox」ノードの次に、新たに「VBoxContainer」クラスのノードを追加し、名前を「MidBox」に変更しよう。
さらに「MidBox」ノードには「Label」クラスのノードを 2 つ追加し、それぞれの名前を「HighScore」、「HighLevel」に変更しよう。これで「HUD」ノードの構成は以下のようになったはずだ。
追加したノードのプロパティを編集する
「LeftBox」ノードと「MidBox」ノードの位置と枠の大きさを編集する。HUD のレイアウトは、ブロックの配置の都合上、全て画面上端に並べる必要がある。
2D ワークスペースで「MidBox」をドラッグして画面上端中央に配置しても問題ないが、インスペクタドックで数値を調整するならそれぞれのノードで「Margin」プロパティが以下の値になるようにしよう。
- 「LeftBox」ノード
- 「MidBox」ノード
これで、画面上端中央に「MidBox」ノードが配置された。
次にインスペクタドックで、「MidBox」ノードの子ノードである「HighScore」ノードと「HighLevel」ノードの「Text」プロパティを設定する。
- 「HighScore」ノードの「Text」プロパティ:「H Scr: 0」
- 「HighLevel」ノードの「Text」プロパティ:「H Lvl: 1」
この時点で、ゲーム画面は以下のようになったはずだ。
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_num
、high_level_num
、score
、high_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
の引数が書き込むデータになるが、ここでは先に定義した辞書型の変数data
をto_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」シーンを開いて編集していこう。
ノードを追加して、プロパティを編集する
以下の手順で、シーンドックおよびインスペクタドックにて、必要なノードを追加し、プロパティを編集していこう。
シーンドックで「VBox」ノードに「VBoxContainer」クラスのノードを一つ追加して、名前を「ResultsContainer」に変更する。
シーンドックで「ResultsContainer」ノードの順番を「GameOverLavel」ノードの下、「Message」ノードの前に移動する。
インスペクタドックで「ResultsContainer」ノードの「Custom Constants」>「Separation」プロパティを
10
にして有効にする。シーンドックで「ResultsContainer」ノードに「Label」クラスのノードを 1 つ追加し、名前を「HighScore」に変更する。
インスペクタドックで「HighScore」ノードの「Align」プロパティを「Center」に変更する。
インスペクタドックで「HighScore」ノードの「Uppercase」プロパティを「オン」にする。
インスペクタドックで既存の「Message」ノードの「Custom Fonts」>「Font」をコピーする。
インスペクタドックで「HighScore」ノードの「Custom Fonts」>「Font」に貼り付けする。
さらに「HighScore」ノードの「Custom Fonts」>「Font」をユニーク化する。
さらに「HighScore」ノードの「Custom Fonts」>「Settings」>「Size」プロパティを
16
にする。シーンドックで「HighScore」ノードを3つ複製し、それらの名前を上から順番に「LastScore」、「HighLevel」、「LastLevel」に変更する。この時点でシーンドックは以下のようになる。
インスペクタドックで「HighScore」ノードの「Text」プロパティを「High Score: 0」にする。
同様に「LastScore」ノードの「Text」プロパティを「Last Score: 0」にする。
同様に「HighLevel」ノードの「Text」プロパティを「High Level: 0」にする。
同様に「LastLevel」ノードの「Text」プロパティを「Last Level: 0」にする。
ここまでの手順ができたら、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()
これだけで、ゲームオーバー画面に遷移した時にファイルの読み込みに失敗さえしなければ、きちんと画面上にデータを表示してくれるはずだ。今はまだデータファイルが作成されていないので、シーンを実行するとデフォルトの値で表示される。
最終確認
それでは最後にデータの保存とデータの読み込みがうまくいくかプロジェクトを実行して確認してみよう。
- 初回プレイ時、ライフが 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 ではブロックの種類(例えば一回の衝突では消えない硬いブロックなど)を増やして、レベルに応じて難易度の幅を広くできるように調整し、複数のレベルをデザインする。