Godot4 ビッグカツブロック崩し29 最速タイムのセーブ・ロード・更新

※この連載の全ての記事は、タグ「ビッグカツ」の検索一覧から探すことができます。
※この連載で作ったゲームは「BigBreakOut(ゲームの作り方の記事付き) | フリーゲーム投稿サイト GodotPlayer」でプレイできます。

昔から人気の駄菓子「ビッグカツ」フリー素材画像が公開されたので、無料・軽快な 2D / 3D 用のゲームエンジン Godot Engine 4 を使って、ビッグカツ画像を使ったブロック崩しを作成します。

「ビッグカツブロック崩し」作成の第29回では、ゲームクリア時の最速のプレイ時間記録(セーブ)しておき、ゲーム起動時読み込み(ロード)を行い、ラベルで表示し、より速いタイムでゲームクリアした場合は記録を更新するスクリプト例とテスト結果を紹介します。

※ GodotEngine 4.3 を使用しています。.NET 版ではありません。
※スクリプトは自己責任でご使用ください。

前回の記事

前回は、設定した音量やゲームクリアの最速タイムのデータをセーブデータとしてセーブ・ロード・初期化・更新するスクリプト例を紹介しました。

今回は、このセーブデータを管理するグローバルノードを利用して、最速タイムの表示・セーブ・ロード・更新を行います。

最速タイムを表示するラベルの作成

最速タイム(ベストタイム)のラベル( Label )を、ステージのシーンの左上に表示します。

新規作成するとフォントの設定などが面倒なので、以前に作成した画面右上の経過時間を表示する ElapsedTime ラベルノードを右クリックして表示されるメニュー「複製」を選んで、複製を変更して作成します。

経過時間を表示する ElapsedTime ノードの作成については、以下の記事を参照してください。

Godot4 ビッグカツブロック崩し BestTimeノードの作成1

複製されたノードを BestTime にリネームして、2D ワークスペースで周囲のオレンジ色の丸いハンドルをマウスドラッグして位置とサイズを調整します。
※リネームは、シーンドックでノードを右クリックして表示されるメニュー「名前を変更」などから行います。

インスペクタードックの Text プロパティ表示される文字列を設定して、 Horizontal Alignment / Vertical Arignment で水平方向・垂直方向での寄せ方(例では Left, Top) を設定します。
Control クラスの Theme Overrides セクションを開いて Font Sizes の Font Size プロパティで、ラベルの枠に合うようにフォントサイズを調整します。

Godot4 ビッグカツブロック崩し BestTimeノードの作成2

スクリプトの切り替え

複製すると、複製元に割り当てられているスクリプトが、複製されたノードにも割り当てられます。

新しいスクリプトを割り当てるために、複製直後に割り当てられているスクリプトをはずし新規作成したスクリプトを割り当てます。

Godot4 ビッグカツブロック崩し BestTimeノードの作成3

シーンドックで複製したノードを選択してから、シーンドックの右上の – ボタンのついた「選択したノードのスクリプトをでタッチする」ボタンを押してスクリプトをはずします。

Godot4 ビッグカツブロック崩し BestTimeノードの作成4

シーンドックの右上のアイコンが + のマークに変わるので「選択したノードに新規または既存のスクリプトをアタッチする」ボタンを押します。

Godot4 ビッグカツブロック崩し BestTimeノードの作成5

「ノードにスクリプトをアタッチする」ダイアログでファイル名などのパスを確認・編集(例では res://best_time.gd )して「作成」ボタンを押して、最速タイムを表示する BestTime ノード用のスクリプトを割り当てます。

Godot4 ビッグカツブロック崩し BestTimeノードの作成6

ElapsedTime ノードと BestTime ノードは、一番手前で経過時間とベストタイムを表示させるので、動的に後から追加されるブロックで隠されないように、インスペクタードックで CanvasItem クラスの Ordering セクションの Z Index プロパティを 0 から 1 にして、シーンドックのツリーの順番(下側が手前に表示されます)にかかわらず手前にくるように設定します。

Godot4 ビッグカツブロック崩し BestTimeノードの作成7

最速タイムの読み込みと保存

最速タイムは前回作成した save_data.gd を持つグローバルノード SaveDataGlobal で管理されています。
save_data.gd については前回の記事を参照してください。

BestTime のラベルがシーンに配置されたら _ready イベント関数内で、 update_label 関数を呼び出して、 SaveDataGlobal ノードがすでに読み込んでいる best_time 変数から表示を更新します。

extends Label
class_name BestTime
## ベストタイム(最速クリア時のタイム)を表示するスクリプトです。
## 以前に作成した TapTheTakarabako の記事も参照にしてください。
## https://compota-soft.work/archives/25627

# Called when the node enters the scene tree for the first time.
func _ready():
	# シーンに配置されたらセーブデータの値を読み込み、最速タイム(BestTime)の表示を更新します。
	update_label()
	return

# Called every frame. 'delta' is the elapsed time since the previous frame.
func _process(delta):
	pass

## ベストタイムの表示を更新します。
## シーンが切り替わったり、ベストタイムが変わった際などに必要です。
## ベストタイムがない場合は非表示にします。
func update_label():
	#print("BestTime.best_time = ", BestTime.best_time)
	# ベストタイムが有効ならば表示します。(すでに表示している場合も更新します)
	if $"/root/SaveDataGlobal".best_time > 0:
		# ラベルを表示します。
		self.show()
		# 99.9 など小数第一位までの表示形式でラベルの文字列を設定します。
		self.text = "BestTime\n%s" % str("%.1f" % $"/root/SaveDataGlobal".best_time)
	else:
		# 有効な最速タイムがない場合は、ラベルを非表示にします。
		self.hide()
	return

ゲームクリア時には、現在のタイムとベストタイムを比較して、さらに速いタイムだった場合は更新します。
念のため、stage.gd の全文を最後に記載します。

## 全てのレベルをクリアしてゲームをクリアした際に呼び出されます。
func on_game_clear():
	# ゲームの進行状況を更新します。
	current_phase = Phase.GameClear
	print("current_phase = Phase.GameClear")
	# レベルクリアの UI を無効にします。
	$LevelClearUI.hide()
	# ゲームクリアの UI を有効にします。
	$GameClearUI.show()
	# ボールを一時停止します。
	$Ball.is_pause = true
	# ゲームクリア時のタイムを比較してベストタイムを更新します。
	$"/root/SaveDataGlobal".update_best_time_save_data($ElapsedTime.time) # セーブデータの更新
	$BestTime.update_label()	# ラベルの表示を更新します。(変化がなくても更新します)
	return

関連記事

セーブとロード、メインシーンを設定していないと正常に動作しない点などについては、以下の記事を参照してください。

メインシーンの変更

今まで、ステージのシーンをメインにしていましたが、メインシーンはゲームの起動時に読み込まれるシーンなので、タイトルのシーンに変更します。

Godot4 ビッグカツブロック崩し メインシーンをタイトルのシーンに変更

メインシーンの設定については以下の記事も参照してください。

テスト

F5 キーで、メインシーンに設定したタイトルのシーンを実行します。
※他のシーンが実行される場合は、メニュー「プロジェクト」→「プロジェクト設定」ツリー「アプリケーション」→「実行」「メインシーン」項目にタイトルのシーン (title_scene.tscn) を設定してください。

セーブデータがない初回は、有効なベストタイムがない( -1.0 が設定されています)ので、ラベルは表示されません
1回ゲームをクリアすると、そのタイムがベストタイムとしてセーブされ、ラベルに BestTime 7.1 のように表示されます。
さらに2回目以降より速いタイムでゲームをクリアすると、ベストタイム更新されました。

※セーブやロードに関しては、実行環境によって動作が変わる場合があるので、エディタ上でテストをした後は、実際に公開する形式でも確認すると良いでしょう。

stage.gd 全文

効果音を呼び出す処理をフェーズの切り替えの際に追加しました。
新しくレベルをプレイする直前には、ボールの初期の角度と速度を init_speed_and_angle 関数を呼び出して初期化しています。

また、ゲームオーバー時は、画面全体のクリックの処理は無視して、各ボタンがクリックされた際のイベント処理各受信側メソッドの関数 (_on_retry_button_button_up, _on_giveup_button_button_up) で行います。

ゲームオーバー時に、スペースキーが押された際には Retry ボタンが押された際と同じ動作をするために、_process イベント関数で Phase.GameOver のフェーズだけはクリックイベントを除いた if 文の中で処理しました。

extends Node2D
class_name Stage
## ゲーム進行を管理するクラスです。
## [member array_level_scene] へのレベルシーンの設定が必要です。

## ボタンが押されたときの効果音をあらかじめ読み込んで保持します。
@onready var audio_stream_button: AudioStream = preload("res://sound/Button.wav")

## ボールの初期位置です。
## $Ball の最初の位置を初期位置として保存して、各レベルの始まりにその位置にボールを配置します。
var ball_start_position: Vector2

## レベルごとに読み込むシーン (.tscn) を設定する配列です。1面から順番に設定してください。
## 
## 設定するシーンはレベルシーンと呼びます。
## レベルシーンは、 Node クラスのルートノードの下に block.tscn の Block の子ノードを1個以上配置したシーンです。
## ブロックはボールが届く範囲に配置してください。
## 例えば、以下のようなレベルシーンがあります。
## Level1(Node クラス)
##   |
##   +- Block1 (block.tscn)
##   |
##   +- Block2 (block.tscn)
##   |
##   +- Block3 (block.tscn)
@export var array_level_scene: Array[PackedScene]

## 現在のレベルです。[member array_level_scene] の要素番号として用います。
## 初期値は -1 ですが、 next_level 関数内で 1 増やされて 0 番目の [member array_level_scene] 要素が実体化します。
var current_level_index: int = -1

## 現在読み込んでいるレベルのシーンのインスタンス(子ノード)です。
var current_level_scene:Node = null

## 現在のゲームのプレイ段階の種類です。
## タイトルシーンと異なり、ゲームオーバーやゲームクリアはプレイ時の画面の上に UI を表記するので
## シーンを切り替えずに、ラベルやボタンなどを必要に応じて表示するため、同一のシーン内でフェーズを変えて
## 見た目や入力による反応を切り替えます。
enum Phase
{
	## レベルのプレイ開始直前のフェーズです。
	LevelReady,
	## レベルプレイ中のフェーズです。ボールを失うと GameOver に移行します。
	Playing,
	## レベルをクリアした直後のフェーズです。
	## 次の面がなければすぐに GameClear に移行します。
	## 次の面があれば、 Next ボタンをクリック後、次の面があれば StageStart に移行します。
	LevelClear,
	## ゲームオーバーのフェースです。タイトルボタンをクリックすると Title に移行します。
	GameOver,
	## ゲームクリアのフェーズです。タイトルボタンをクリックすると Title に移行します。
	GameClear,
}
## 現在のゲームの進行段階、フェーズを表します。
var current_phase = Phase.LevelReady

## Called when the node enters the scene tree for the first time.
## ノードが初めてシーン ツリーに入るときに呼び出されます。
func _ready():
	# プロパティの設定の確認
	if array_level_scene.size() < 1:
		printerr("Stage シーンの Stage ルートノードのプロパティ ArrayLevelScene にレベルシーンを1つ以上設定してください。")
	# ボールの初期位置を保存します。
	ball_start_position = $Ball.global_position
	# 1 面を読み込みます。
	current_level_index = -1 # next_level 関数内で +1 されて 0 になります。
	next_level() # 最初のレベルに進みます。
	return

## Called every frame. 'delta' is the elapsed time since the previous frame.
## フレームごとに呼び出されます。 delta は、前のフレームからの経過時間です。
## delta を使用しない場合は warning が出ないように _delta とリネームします。
func _process(_delta):
	# プレイ中
	if current_phase == Phase.Playing:
		# レベルシーンの持つ子ノード(ブロック)が全て消えたらレベルクリアに移ります。
		if current_level_scene.get_child_count() < 1:
			on_level_clear()
			
	# Enter / Space キー / マウス左ボタン(クリック) が押された直後の場合
	if Input.is_action_just_pressed("ui_accept") or Input.is_action_just_pressed("ui_click"):
	#if Input.is_action_just_pressed("ui_accept") or Input.is_action_just_pressed("ui_right") or Input.is_action_just_pressed("ui_left"):
		# ゲームの進行状況によって、入力があった際の挙動を変えます。
		match current_phase:
			Phase.LevelReady:
				# 効果音を再生します。
				SCUtil.AudioStreamOneShotPlay(SCUtilGlobal, audio_stream_button)
				# プレイ前の状態で入力されたら、ボールを動かして、プレイを開始します。
				on_playing()
			Phase.Playing:
				# プレイ中はクリック入力に意味はありません。
				pass
			Phase.LevelClear:
				# 効果音を再生します。
				SCUtil.AudioStreamOneShotPlay(SCUtilGlobal, audio_stream_button)
				# 現在のレベルをクリアした状態で入力があった場合は、次のレベルに進みます。
				next_level()
			Phase.GameOver:
				# 画面全体のクリックイベントを無視するため、後の if 文でスペースキーなどだけを処理します。
				pass
			Phase.GameClear:
				# 効果音を再生します。
				SCUtil.AudioStreamOneShotPlay(SCUtilGlobal, audio_stream_button)
				# ゲームクリア状態で、入力があればタイトルに戻ります。
				get_tree().change_scene_to_file("res://title_scene.tscn")
			_:
				# 上記以外
				printerr("想定していない Phase の列挙子の値です。 current_phase = ", str(current_phase))
		
		# ゲームオーバー時は、ボタンをクリックして選択するので、画面全体でのクリックのイベント処理は行いません。
		if Input.is_action_just_pressed("ui_accept"):
			if current_phase == Phase.GameOver:
				# 効果音を再生します。
				SCUtil.AudioStreamOneShotPlay(SCUtilGlobal, audio_stream_button)
				# ゲームオーバー時は、 Retry ボタンまたは GiveUp ボタンで
				# 遷移先を決めます。
				# Space/Enter キーは Retry ボタンと同じ入力として処理します。
				_on_retry_button_button_up()
	return


## 次のレベルに進みます。レベルシーンを実体化して、Phase.StageReady の状態になります。
## 次のレベルシーンがない場合は、ゲームクリアの処理に移ります。
func next_level():
	# 次のレベルの要素番号に更新します。
	current_level_index += 1
	# [member array_level_scene] の要素数(面の数)を超えて、次のレベルがない場合
	if array_level_scene.size() < (current_level_index + 1):
		# ゲームクリア
		on_game_clear()
	else:
		# 直前のレベルシーンを解放します。
		if current_level_scene != null:
			current_level_scene.queue_free()
		# 次の面のレベルシーンをインスタンス化します。
		current_level_scene = array_level_scene[current_level_index].instantiate()
		# Stage ルートノードの子ノードとしてレベルシーンのインスタンスを追加します。
		add_child(current_level_scene)
		# ボールを初期位置に配置して、移動速度と角度を初期化します。
		$Ball.global_position = ball_start_position
		$Ball.init_speed_and_angle()
		# ゲームの進行状況を StageReady にします。
		on_level_ready()
		return

## プレイ開始直前に呼び出されます。
func on_level_ready():
	# ゲームの進行状況を更新します。
	current_phase = Phase.LevelReady
	#print("current_phase = Phase.LevelReady")
	# ゲームオーバーとクリア時の UI を非表示にします。下位のノードも非表示になります。
	# 非表示のボタンはその場所をクリックしても反応しません。
	$GameOverUI.hide()
	$GameClearUI.hide()
	$LevelClearUI.hide()
	# Ready UI を有効にします。
	$LevelReadyUI.show()

## 現在のレベルのプレイ開始時に呼び出されます。
func on_playing():
	# ゲームの進行状況を更新します。
	current_phase = Phase.Playing
	#print("current_phase = Phase.Playing")
	# Ready UI を無効にします。
	$LevelReadyUI.hide()
	# ボールの一時停止を解除します。
	$Ball.is_pause = false
	return

## 現在のレベルをクリアした際に呼び出されます。
func on_level_clear():
	# ゲームの進行状況を更新します。
	current_phase = Phase.LevelClear
	#print("current_phase = Phase.LevelClear")
	# レベルクリアの UI を有効にします。
	$LevelClearUI.show()
	# ボールを一時停止します。
	$Ball.is_pause = true
	return

## 全てのレベルをクリアしてゲームをクリアした際に呼び出されます。
func on_game_clear():
	# ゲームの進行状況を更新します。
	current_phase = Phase.GameClear
	#print("current_phase = Phase.GameClear")
	# レベルクリアの UI を無効にします。
	$LevelClearUI.hide()
	# ゲームクリアの UI を有効にします。
	$GameClearUI.show()
	# ボールを一時停止します。
	$Ball.is_pause = true
	# ゲームクリア時のタイムを比較してベストタイムを更新します。
	$"/root/SaveDataGlobal".update_best_time_save_data($ElapsedTime.time) # セーブデータの更新
	$BestTime.update_label()	# ラベルの表示を更新します。(変化がなくても更新します)
	return

## ゲームオーバーになった際に呼び出されます。
func on_game_over():
	# ゲームの進行状況を更新します。
	current_phase = Phase.GameOver
	#print("current_phase = Phase.GameOver")	
	# ゲームオーバーの UI を有効にします。
	$GameOverUI.show()
	# ボールを一時停止します。
	$Ball.is_pause = true
	return

## Ball ノードが画面外に出た際に発する
## カスタムノイズ ball_screen_exited を受信するメソッドです。
func _on_ball_ball_screen_exited():
	# ゲームオーバー時の処理を呼び出します。
	on_game_over()
	return

## RetryButton が離されたタイミングで呼び出される受信側メソッドです。
## 同じレベルを再び読み込み、リトライを行います。
func _on_retry_button_button_up():
	# 効果音を再生します。
	SCUtil.AudioStreamOneShotPlay(SCUtilGlobal, audio_stream_button)
	# 現在のレベルを読み込みなおします。
	current_level_index -= 1
	next_level()
	return

## GiveupButton が離されたタイミングで呼び出される受信側メソッドです。
## タイトルシーンに戻ります。
func _on_giveup_button_button_up():
	# 効果音を再生します。
	SCUtil.AudioStreamOneShotPlay(SCUtilGlobal, audio_stream_button)
	# タイトルシーンに戻ります。
	get_tree().change_scene_to_file("res://title_scene.tscn")
	return

まとめ

「ビッグカツブロック崩し」作成の第29回では、ゲームクリア時の最速のプレイ時間記録(セーブ)しておき、ゲーム起動時読み込み(ロード)を行い、ラベルで表示し、より速いタイムでゲームクリアした場合は記録を更新するスクリプト例とテスト結果を紹介します。

参照サイト Thank You!

記事一覧 → Compota-Soft-Press

コメント

Ads Blocker Image Powered by Code Help Pro

お願い - Ads Blocker Detected

このサイトは広告を掲載して運営しています。

ポップアップを閉じて閲覧できますが、よろしければ

このサイト内の広告を非表示にする拡張機能をオフにしていただけませんか?

━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━━

We have detected that you are using extensions to block ads. Please support us by disabling these ads blocker.

タイトルとURLをコピーしました