♟️

ぷよぷよプログラミングをGodotで実装 07 実践編 :盤面のぷよ

に公開

Godotでぷよぷよを作るチュートリアル

前回に引き続きぷよぷよプログラミングを利用して、Godotでパズルゲームの作り方の基礎を一緒に学んでいきましょう。
YouTubeでもこの記事の内容に沿ってゲームを作っているので、動画を見ながら取り組んでみてください。

前回の記事はこちら
https://zenn.dev/yurinchi/articles/78dd2b4df8b466

今回の動画はこちら
https://www.youtube.com/watch?v=X3UHWVEw3b4

7. 状態管理処理の追加(盤面のぷよ)

ぷよを操作をしていない時の状態管理の処理を実装していきます。

7.1 ぷよの固定処理

プレイヤーが操作していたぷよが接地し、一定時間経過すると盤面に固定されるようにします。

  1. 操作していたぷよを盤面に設定するため、Stage.gdに盤面の情報を保持する配列と必要な変数を設定します。
# ぷよの盤面データを格納する2D配列
# 各セルは null (空) または PuyoNode のインスタンスを格納します
var board: Array[Array] = [] 
# 現在ステージにあるぷよの総数
var puyo_count: int = 0

# ぷよのシーン (インスタンス化して使用する)
const PUYO_SCENE = preload("res://scenes/game/Puyo.tscn")
  1. 盤面を初期化する処理を追加します。
# ステージの盤面を初期化する (JavaScriptのStage.initialize()の盤面部分に対応)
func initialize_stage() -> void:
	board.clear() # 既存の盤面データをクリア
	for r in range(Global.STAGE_ROWS):
		board.append([])
		for c in range(Global.STAGE_COLS):
			board[r].append(null) # 全てのセルを空にする

	puyo_count = 0
  1. _readyで初期化処理を呼び出します。
# シーンがツリーに追加され、準備ができたときに呼ばれる
func _ready() -> void:
	initialize_stage() # 追加

	# スクリプトで盤面のサイズを設定する場合
	var background_node = get_node_or_null("Background")
	background_node.size = Vector2(Global.STAGE_COLS * Global.PUYO_IMG_WIDTH, Global.STAGE_ROWS * Global.PUYO_IMG_HEIGHT)
  1. 盤面の情報と表示を管理する処理を追加します。
# 画面とメモリ両方にぷよをセットする (JavaScriptのStage.setPuyo()に対応)
func set_puyo(x: int, y: int, puyo_type: int) -> void:

	# 既にそのマスにぷよがあれば、古いぷよを削除
	if board[y][x] != null and is_instance_valid(board[y][x]):
		board[y][x].queue_free()

	# Puyoシーンをインスタンス化
	var puyo_node: Node2D = PUYO_SCENE.instantiate()
	add_child(puyo_node) # Gridノードの子として追加
	puyo_node.set_puyo_type(puyo_type) # ぷよのタイプを設定
	
	# ぷよのピクセル位置を設定
	puyo_node.position = Vector2(float(x) * Global.PUYO_IMG_WIDTH, float(y) * Global.PUYO_IMG_HEIGHT)
	
	# メモリ (board配列) にぷよノードへの参照を格納
	board[y][x] = puyo_node
	puyo_count += 1 # ぷよの総数を増やす
  1. 操作が終わったぷよを盤面に登録する処理をPlayer.gdに追加します。
# fix_puyo_logic(): プレイヤー操作中のぷよを盤面(Stage)に固定します。
func fix_puyo_logic() -> void:

	# ぷよノードが有効でない場合は処理しません。
	if not is_instance_valid(center_puyo_node) or not is_instance_valid(movable_puyo_node):
		return
		
	# 最終的なピクセル位置からグリッド座標を計算します。
	var center_x_grid: int = int(round(center_puyo_node.position.x / Global.PUYO_IMG_WIDTH))
	var center_y_grid: int = int(round(center_puyo_node.position.y / Global.PUYO_IMG_HEIGHT))
	var movable_x_grid: int = int(round(movable_puyo_node.position.x / Global.PUYO_IMG_WIDTH))
	var movable_y_grid: int = int(round(movable_puyo_node.position.y / Global.PUYO_IMG_HEIGHT))
	
	# 中心ぷよをグリッドに配置します。
	if center_y_grid >= 0: # 画面外でなければ
		stage_node.set_puyo(center_x_grid, center_y_grid, center_puyo_node.puyo_type)

	# 可動ぷよをグリッドに配置します。
	if movable_y_grid >= 0: # 画面外でなければ
		stage_node.set_puyo(movable_x_grid, movable_y_grid, movable_puyo_node.puyo_type)

	# 操作用として作成したぷよノードをシーンツリーから削除します
	if is_instance_valid(center_puyo_node):
		center_puyo_node.queue_free()
	if is_instance_valid(movable_puyo_node):
		movable_puyo_node.queue_free()

	# 参照をクリアし、次のぷよのために準備します。
	center_puyo_node = null
	movable_puyo_node = null
  1. Main.gd_processメソッド内のGameMode.FIXING_PUYOの処理を更新します。
		GameMode.FIXING_PUYO:
			# ぷよを盤面に固定する処理を実行します。
			player_node.fix_puyo_logic()
			# TODO: 確認のため、ぷよの固定が終わったら次の操作ぷよを生成させます
			current_mode = GameMode.NEW_PUYO

これで、操作中のぷよが接地すると、一定時間後に盤面に固定されるようになります。

7.2 ぷよの接触判定

ここまでの実装で操作ぷよが地面に接地するまで落下をしますが、盤面に設置されたぷよは貫通していまします。
盤面に設置されたぷよも地面同様に接地判定されるように修正を行います。

  1. 盤面の指定された位置にぷよがあるか確認するための処理をStage.gdに追加します。
# 指定された位置のぷよを取得する
func get_puyo(x: int, y: int) -> Node:
	# 座標がステージの範囲外ならnullを返す
	if y < 0 or y >= Global.STAGE_ROWS or x < 0 or x >= Global.STAGE_COLS:
		return null
	return board[y][x]
  1. Player.gd_is_collidingの衝突判定に盤面のぷよを追加する
### グリッド上の衝突チェック
# 指定されたグリッド座標にぷよがあるか、またはグリッド範囲外かをチェック
func _is_colliding(check_x: int, check_y: int) -> bool:
	if check_y >= Global.STAGE_ROWS or \
	   check_x < 0 or check_x >= Global.STAGE_COLS:
		return true # 範囲外は衝突とみなす
	return stage_node.get_puyo(check_x, check_y) != null # ぷよが座標にある

これで盤面のぷよに対しても接触判定が追加され、ぷよの上に設置することや回転や横移動の衝突にも影響することが確認できます。

7.3 自由落下チェックと落下アニメーション

ぷよが固定された後、盤面上のぷよと地面に隙間ができていれば、上にあるぷよが自由落下する必要があります。

  1. 落下するぷよが存在するか確認する処理をStage.gdに追加します。
# 落下中のぷよを一時的に保持する配列
var falling_puyos: Dictionary = {}
func check_fall() -> bool:
	
	falling_puyos.clear() # 対象リストをクリア

	# 下から2段目から上の行を見ていく (最下段は落ちないため)
	for c in range(Global.STAGE_COLS):
		for r in range(Global.STAGE_ROWS - 2, -1, -1):
			var puyo_node = get_puyo(c, r)
			if puyo_node == null:
				continue

			var landing_point:int = r
			while (landing_point + 1 < Global.STAGE_ROWS and get_puyo(c, landing_point + 1) == null):
				landing_point += 1

			if landing_point == r:
				continue
			# このぷよは下に落ちる
			falling_puyos[puyo_node] = landing_point
			board[landing_point][c] = puyo_node
			board[r][c] = null

	return !falling_puyos.is_empty()
  1. 自由落下アニメーションの処理をStage.gdに追加します。
# 自由落下を実行する (JavaScriptのStage.fall()に対応)
func fall() -> bool:
	var still_falling: bool = false

	for puyo_node in falling_puyos:
		if is_instance_valid(puyo_node): # ノードが有効か確認
			puyo_node.position.y += Global.FREE_FALLING_SPEED # 落下速度分、Y座標を増やす
			still_falling = true # まだ落下中のぷよがある

			# 完全に1マス分落ちたかをチェック
			# 現在のY位置が、次のグリッドセルの上端を「超えた」ら、グリッド位置を更新
			if puyo_node.position.y >= float(falling_puyos[puyo_node]) * Global.PUYO_IMG_HEIGHT:
				## メモリ上の位置を更新 (現在の位置から1つ下のマスへ)
				puyo_node.position.y = float(falling_puyos[puyo_node]) * Global.PUYO_IMG_HEIGHT # 位置を正確にグリッドに合わせる
				falling_puyos.erase(puyo_node)

	return still_falling # 落下中のぷよがいなければfalseを返す
  1. Main.gd_processメソッド内のGameMode.CHECK_FALLの処理を更新します。
		GameMode.CHECK_FALL:
			# 盤面全体で自由落下が必要なぷよがあるかチェックします。
			if stage_node.check_fall():
				current_mode = GameMode.FALL # 落下が必要な場合は落下アニメーションへ遷移
			else:
				# TODO: 確認のため、ぷよの自由落下が終わったら次の操作ぷよを生成させます
				current_mode = GameMode.NEW_PUYO
  1. GameMode.FIXING_PUYOに仮設定した状態遷移をCHECK_FALLに変更します。
		GameMode.FIXING_PUYO:
			# ぷよを盤面に固定する処理を実行します。
			player_node.fix_puyo_logic()
			# 固定後、再度自由落下チェックへ遷移します。
			current_mode = GameMode.CHECK_FALL
  1. Main.gd_processメソッド内のGameMode.FALLの処理を更新します。
		GameMode.FALL:
			# 自由落下アニメーションを実行します。
			if not stage_node.fall():
				# TODO: 確認のため、すべてのぷよが落ちきったら次の操作ぷよを生成させます
				current_mode = GameMode.NEW_PUYO
  1. GameMode.STARTに仮設定していたゲームモードをCHECK_FALLに置き換えます。
		GameMode.START:
			# ゲーム開始時、最初は自由落下チェックからゲームを開始します。
			current_mode = GameMode.CHECK_FALL

これで操作ぷよが固定された際に地面やぷよに接していないぷよが落下するようになりました。

7.4 ぷよの消去チェックとアニメーション

自由落下が完了したら、同じ色のぷよが4つ以上連結しているかチェックし、消去処理を行います。

  1. 消去できるぷよが存在するか確認する処理をStage.gdに追加します。
# 消去アニメーション中のぷよを一時的に保持する配列
var erasing_puyos: Array[Node2D] = [] 
var erase_start_frame: int = 0
# 消せるぷよがあるかどうかを判定する (JavaScriptのStage.checkErase()に対応)
func check_erase(frame: int) -> Dictionary:
	var erase_info: Dictionary = { "piece": 0, "color": 0 } # 消したぷよ数と色数を格納
	erasing_puyos.clear() # 消去対象リストをクリア

	# 訪問済みのセルを記録する2D配列 (再帰処理での無限ループ防止)
	var visited: Array[Array] = []
	for r in range(Global.STAGE_ROWS):
		visited.append([])
		for c in range(Global.STAGE_COLS):
			visited[r].append(false)

	var erased_puyo_types_found: Dictionary = {} # 消去対象となったぷよのタイプを記録

	# 全てのセルを走査
	for r in range(Global.STAGE_ROWS):
		for c in range(Global.STAGE_COLS):
			var current_puyo: Node2D = get_puyo(c, r)

			# ぷよが存在しない、または既に訪問済みであればスキップして次のセルへ
			if current_puyo == null:
				continue
			if visited[r][c]:
				continue

			var current_group: Array[Array] = [] # 同じ色で連結しているぷよの座標リスト
			var q: Array[Array] = [[c, r]] # BFSのためのキュー
			visited[r][c] = true # 現在のセルを訪問済みとしてマーク

			while q.size() > 0:
				var pos: Array = q.pop_front() # キューから座標を取り出す
				var current_col: int = pos[0]
				var current_row: int = pos[1]
				current_group.append([current_col, current_row]) # グループに追加

				# 上下左右4方向をチェック
				var directions: Array[Vector2] = [Vector2(0, 1), Vector2(0, -1), Vector2(1, 0), Vector2(-1, 0)]
				for dir_vec in directions:
					var next_col: int = current_col + int(dir_vec.x)
					var next_row: int = current_row + int(dir_vec.y)

					# 次の座標がステージ範囲外であればスキップ
					if next_row < 0 or next_row >= Global.STAGE_ROWS or \
					   next_col < 0 or next_col >= Global.STAGE_COLS:
						continue
					# 既に訪問済みであればスキップ
					if visited[next_row][next_col]:
						continue

					var neighbor_puyo: Node2D = get_puyo(next_col, next_row)

					# 隣接するぷよが存在し、同じタイプであればキューに追加
					if neighbor_puyo != null and neighbor_puyo.puyo_type == current_puyo.puyo_type:
						visited[next_row][next_col] = true # 訪問済みとしてマーク
						q.append([next_col, next_row])

			# 連結したぷよの数が消去条件を満たしていれば
			if current_group.size() >= Global.ERASE_PUYO_COUNT:
				erase_info.piece += current_group.size() # 消したぷよ数を加算
				
				# この色のぷよが初めて消去対象となった場合のみ色数を加算
				if not erased_puyo_types_found.has(str(current_puyo.puyo_type)):
					erase_info.color += 1
					erased_puyo_types_found[str(current_puyo.puyo_type)] = true
				
				# 消去対象となるぷよをerasing_puyosリストに追加し、boardからは一時的にnullにする
				for pos in current_group:
					var col: int = pos[0]
					var row: int = pos[1]
					var puyo_to_erase: Node2D = board[row][col]
					if is_instance_valid(puyo_to_erase):
						erasing_puyos.append(puyo_to_erase)
						board[row][col] = null # 盤面から削除
	erase_start_frame = frame
	return erase_info
  1. 消去処理をStage.gdに追加します。
# 消去アニメーションを実行する (JavaScriptのStage.erasing()に対応)
# 戻り値: アニメーション中ならtrue, 完了したらfalse
func erasing(current_frame: int) -> bool:

	if erasing_puyos.is_empty():
		return false # 消去するぷよがなければアニメーションは終了

	# アニメーションの進行度 (0.0から1.0) を計算
	# アニメーションは一定フレーム数で1サイクルとし、それを繰り返す
	var elapsed_frames_in_cycle: int = (current_frame - erase_start_frame) % Global.ERASE_ANIMATION_DURATION
	var ratio: float = float(elapsed_frames_in_cycle) / Global.ERASE_ANIMATION_DURATION

	# JavaScript版の点滅アニメーション (0.25, 0.5, 0.75の閾値で表示/非表示を切り替え) を再現
	var cell_is_visible: bool = true

	if ratio <= 0.25:
		cell_is_visible = false
	elif ratio <= 0.5:
		cell_is_visible = true
	elif ratio <= 0.75:
		cell_is_visible = false
	else: # ratio > 0.75
		cell_is_visible = true # 最後のフェーズは表示

	for puyo_node in erasing_puyos:
		if is_instance_valid(puyo_node): # ノードが有効か確認
			puyo_node.visible = cell_is_visible

	# アニメーションの1サイクルが完全に終わったら、ぷよを削除
	# Global.ERASE_ANIMATION_DURATIONの倍数フレームごとに削除処理を行う
	# current_frame > 0 は、ゲーム開始直後の0フレーム目での意図しない削除を避けるため
	if elapsed_frames_in_cycle == 0 and current_frame > 0: 
		for puyo_node in erasing_puyos:
			if is_instance_valid(puyo_node):
				puyo_node.queue_free() # ノードを削除 (メモリから解放)
				puyo_count -= 1 # ステージ上のぷよの総数を減らす
		erasing_puyos.clear() # リストをクリア
		return false # アニメーション完了
			
	return true # アニメーション中
  1. Main.gd_processメソッド内のGameMode.ERASINGの処理を更新します。
		GameMode.ERASING:
			# ぷよ消去アニメーションを実行します。
			if not stage_node.erasing(frame):
				# 消去アニメーションが終了したら、再度自由落下チェックへ遷移します(連鎖の可能性)。
				current_mode = GameMode.CHECK_FALL 
  1. 連鎖数を記憶させるための変数を作成します。
var combination_count: int = 0               # 現在の連鎖数(コンボ数)
  1. Main.gd_processメソッド内のGameMode.CHECK_ERASEの処理を更新します。
		GameMode.CHECK_ERASE:
			# 消せるぷよがあるかチェックし、消去情報を取得します。
			var erase_info: Dictionary = stage_node.check_erase(frame)
			if erase_info.piece > 0: # 消せるぷよがあった場合
				current_mode = GameMode.ERASING # 消去アニメーションへ遷移
				combination_count += 1 # 連鎖数を加算
			else:
				combination_count = 0 # 連鎖数をリセット
				current_mode = GameMode.NEW_PUYO # 新しいぷよの生成へ遷移
  1. GameMode.CHECK_FALLGameMode.FALLに仮設定していたゲームモードをCHECK_ERASEに置き換えます。
		GameMode.CHECK_FALL:
			# 盤面全体で自由落下が必要なぷよがあるかチェックします。
			if stage_node.check_fall():
				current_mode = GameMode.FALL # 落下が必要な場合は落下アニメーションへ遷移
			else:
				current_mode = GameMode.CHECK_ERASE # 落下がなければ消去チェックへ遷移

		GameMode.FALL:
			# 自由落下アニメーションを実行します。
			if not stage_node.fall():
				# すべてのぷよが落ちきったら、消去チェックへ遷移します。
				current_mode = GameMode.CHECK_ERASE 

実行して動作を確認しましょう。ぷよを4個繋げるとぷよが消去されることが確認できます。

8. 全消しとゲームオーバーの実装

一連のぷよぷよの動作が実装できたので、全消しとゲームオーバーを実装してよりゲームらしくしていきましょう。

8.1 全消しの実装

ぷよを消して盤面上からぷよが1つもない状態になった時に全消しの画像を表示させ、次のぷよを消去するまで表示させます。

  1. Sprite2Dノードを操作できるようにStage.gdに変数を追加します。
# 全消し表示用のSprite2Dノード (MainシーンのBatankyuSpriteとは別)
var zenkeshi_sprite: Sprite2D = null 
  1. 全消し表示用のSprite2Dを設定するsetup_zenkeshi_spriteを作成します。
# 全消し表示用のSprite2Dを設定する
func setup_zenkeshi_sprite() -> void:
	zenkeshi_sprite = Sprite2D.new()
	add_child(zenkeshi_sprite) # Gridの子ノードとして追加
	zenkeshi_sprite.texture = Global.ZENKESHI_TEXTURE # Globalからテクスチャを設定
	# 位置をステージの中央に設定 (中心点がSprite2Dの原点となる場合)
	# Sprite2DのoffsetをVector2(0,0)に設定している場合、positionは左上を指すので注意
	zenkeshi_sprite.position = Vector2(Global.STAGE_COLS * Global.PUYO_IMG_WIDTH / 2.0, Global.STAGE_ROWS * Global.PUYO_IMG_HEIGHT / 2.0)
	# 幅をステージ幅に合わせるようにスケール調整
	if Global.ZENKESHI_TEXTURE:
		zenkeshi_sprite.scale.x = float(Global.STAGE_COLS * Global.PUYO_IMG_WIDTH) / Global.ZENKESHI_TEXTURE.get_width()
		zenkeshi_sprite.scale.y = zenkeshi_sprite.scale.x # アスペクト比を維持してYも同じスケールにする
	zenkeshi_sprite.hide() # 最初は非表示
  1. _readysetup_zenkeshi_spriteを呼び出して初期設定します。
# シーンがツリーに追加され、準備ができたときに呼ばれる
func _ready() -> void:
	initialize_stage()
	# 全消し表示用のSprite2Dをセットアップ
	setup_zenkeshi_sprite() # 追加

	# スクリプトで盤面のサイズを設定する場合
	var background_node = get_node_or_null("Background")
	background_node.size = Vector2(Global.STAGE_COLS * Global.PUYO_IMG_WIDTH, Global.STAGE_ROWS * Global.PUYO_IMG_HEIGHT)
  1. 全消し画像をステージに表示するためにshow_zenkeshiを作成します。
# 全消し画像をステージに表示する (JavaScriptのStage.showZenkeshi()に対応)
func show_zenkeshi() -> void:
	if !zenkeshi_sprite:
		return

	zenkeshi_sprite.show()
	zenkeshi_sprite.modulate.a = 1.0 # 不透明度を完全に表示
	# 全消しアニメーション (Tweenノードを使用)
	var tween = create_tween()

	# Y位置をステージ下から中間点へ移動させるアニメーション
	# zenkeshi_sprite.positionは中央を指しているため、計算を調整
	var start_y_for_tween = Global.STAGE_ROWS * Global.PUYO_IMG_HEIGHT + zenkeshi_sprite.texture.get_height() / 2.0 # 画面下から出現
	var end_y_for_tween = (Global.STAGE_ROWS * Global.PUYO_IMG_HEIGHT) / 3.0 # ステージ上部寄りの中間点
	tween.tween_property(zenkeshi_sprite, "position:y", end_y_for_tween, Global.ZENKESHI_DURATION).from(start_y_for_tween)
	tween.set_trans(Tween.TRANS_SINE).set_ease(Tween.EASE_OUT)
  1. 全消し画像を非表示にするためにhide_zenkeshiを作成します。
# 全消し画像をステージから隠す (JavaScriptのStage.hideZenkeshi()に対応)
func hide_zenkeshi() -> void:
	if !zenkeshi_sprite:
		return

	var tween = create_tween()
	tween.tween_property(zenkeshi_sprite, "modulate:a", 0.0, Global.ZENKESHI_DURATION) # 透明度を0に
	tween.set_trans(Tween.TRANS_LINEAR).set_ease(Tween.EASE_IN)
	# アニメーション終了時に非表示にする
	tween.finished.connect(func(): zenkeshi_sprite.hide())
  1. 盤面上にぷよがなくなった時にshow_zenkeshiを呼び出すようにMain.gd修正します。
		GameMode.CHECK_ERASE:
			# 消せるぷよがあるかチェックし、消去情報を取得します。
			var erase_info: Dictionary = stage_node.check_erase(frame)
			if erase_info.piece > 0: # 消せるぷよがあった場合
				current_mode = GameMode.ERASING # 消去アニメーションへ遷移
				combination_count += 1 # 連鎖数を加算
			else:
				# 消せるぷよがなかった場合
				if stage_node.puyo_count == 0 and combination_count > 0:
					# 全消しボーナス発生(ぷよが全て消え、かつ連鎖があった場合)
					stage_node.show_zenkeshi() # 全消し表示アニメーションを開始

				combination_count = 0 # 連鎖数をリセット
				current_mode = GameMode.NEW_PUYO # 新しいぷよの生成へ遷移
  1. 全消しの状態から通常の状態に戻らせるようにhide_zenkeshiを追加します。
		GameMode.CHECK_ERASE:
			# 消せるぷよがあるかチェックし、消去情報を取得します。
			var erase_info: Dictionary = stage_node.check_erase(frame)
			if erase_info.piece > 0: # 消せるぷよがあった場合
				current_mode = GameMode.ERASING # 消去アニメーションへ遷移
				combination_count += 1 # 連鎖数を加算
				# 追加
				stage_node.hide_zenkeshi() # もし全消し表示中なら隠します
			else:
				# 消せるぷよがなかった場合
				if stage_node.puyo_count == 0 and combination_count > 0:
					# 全消しボーナス発生(ぷよが全て消え、かつ連鎖があった場合)
					stage_node.show_zenkeshi() # 全消し表示アニメーションを開始

				combination_count = 0 # 連鎖数をリセット
				current_mode = GameMode.NEW_PUYO # 新しいぷよの生成へ遷移

動作を確認してみましょう。ぷよを全消ししたら、全消しの画像が表示され、次のぷよを消した時にぷよが残っていたら全消し画像が非表示になっていることを確認できると思います。

8.2 ゲームオーバー処理

ゲームオーバーになった際の処理と、バタンキューアニメーションの表示を行います。

  1. Main.tscnシーンにバタンキュー用のノードを追加します。
    • Mainノードを選択し、子ノードを追加からSprite2Dを選択し名前をBatankyuSpriteにします。
    • 「インスペクター」パネルでOffsetCenterdのチェックを外します。
    • 「シーン」パネルで目のアイコンをクリックし、非表示にさせます。
  2. Main.gdにBatankyuSpriteへの参照を追加します。
@onready var batankyu_sprite: Sprite2D = $BatankyuSprite # バタンキュー表示用のSprite2Dノードへの参照
  1. Main.gd_ready関数内でBatankyuSpriteの初期設定を行います。
# _ready(): シーンがツリーに追加され、準備ができたときに一度だけ呼ばれます。
# ゲーム全体の初期設定を行います。
func _ready() -> void:
	initialize_game()

	# バタンキューSpriteの初期設定
	if is_instance_valid(batankyu_sprite) and Global.BATANKYU_TEXTURE != null:
		batankyu_sprite.texture = Global.BATANKYU_TEXTURE
		# スプライトのオフセットを調整し、原点が中心になるようにします。
		# これにより、positionプロパティでスプライトの中心位置を指定しやすくなります。
		batankyu_sprite.offset = -batankyu_sprite.texture.get_size() / 2 

		# バタンキュー画像の幅をステージ幅に合わせるようにスケール調整
		var target_width = float(Global.STAGE_COLS * Global.PUYO_IMG_WIDTH)
		if batankyu_sprite.texture.get_width() > 0:
			batankyu_sprite.scale.x = target_width / batankyu_sprite.texture.get_width()
			batankyu_sprite.scale.y = batankyu_sprite.scale.x # アスペクト比を維持してYも同じスケールにする
		
		# 最初は画面外の上部に隠しておく
		# positionはoffsetを考慮した中心座標
		batankyu_sprite.position = Vector2(target_width / 2.0, -batankyu_sprite.texture.get_height() * batankyu_sprite.scale.y / 2.0)
		batankyu_sprite.hide()
  1. バタンキューアニメーションで利用する変数を宣言します
var game_over_frame: int = 0                 # バタンキューアニメーション開始時のフレーム数を記録
  1. Main.gdにゲームオーバー関連の関数を追加します。
# prepare_batankyu_logic(current_frame): バタンキューアニメーションの準備をします。
func prepare_batankyu_logic(current_frame: int) -> void:
	game_over_frame = current_frame # ゲームオーバーが開始されたフレームを記録
	if is_instance_valid(batankyu_sprite):
		batankyu_sprite.show() # バタンキュー画像を画面に表示
		# 初期位置を画面外上部に設定します。
		batankyu_sprite.position = Vector2(Global.STAGE_COLS * Global.PUYO_IMG_WIDTH / 2.0, -batankyu_sprite.texture.get_height() * batankyu_sprite.scale.y / 2.0)

# batankyu_logic(current_frame): バタンキューアニメーションを実行します。
func batankyu_logic(current_frame: int) -> void:
	if not is_instance_valid(batankyu_sprite) or Global.GAME_OVER_FRAME == 0:
		return

	# アニメーションの進行度を計算 (0.0から1.0の範囲にクランプ)
	var ratio: float = float(current_frame - game_over_frame) / Global.GAME_OVER_FRAME
	ratio = clampf(ratio, 0.0, 1.0) 

	# JavaScript版のX座標計算をGDScriptに変換(サインカーブで左右に揺れる動き)
	# Math.cos(Math.PI / 2 + ratio * Math.PI * 2 * 10) * Config.puyoImgWidth
	var x_offset_from_center: float = cos(PI / 2.0 + ratio * PI * 2.0 * 10.0) * Global.PUYO_IMG_WIDTH
	
	# JavaScript版のY座標計算をGDScriptに変換(コサインカーブで上下に揺れながら中央に落下)
	# (Math.cos(Math.PI + ratio * Math.PI * 2) * Config.puyoImgHeight * Global.STAGE_ROWS) / 4 + (Config.puyoImgHeight * Global.STAGE_ROWS) / 2
	var stage_height: float = float(Global.PUYO_IMG_HEIGHT) * Global.STAGE_ROWS
	var y_center_of_stage: float = stage_height / 2.0
	var y_amplitude: float = stage_height / 4.0
	var y_offset_from_center: float = cos(PI + ratio * PI * 2.0) * y_amplitude + y_center_of_stage
	
	# バタンキューSpriteの位置を更新 (ステージ中央を基準にオフセットを加えます)
	batankyu_sprite.position = Vector2(
		Global.STAGE_COLS * Global.PUYO_IMG_WIDTH / 2.0 + x_offset_from_center, # ステージ中央X + 横揺れオフセット
		y_offset_from_center # 縦位置(揺れ込み)
	)
  1. Main.gdGameMode.GAME_OVERGameMode.BATANKYU_ANIMATIONの処理を更新します。
		GameMode.GAME_OVER:
			# ゲームオーバー時の準備処理を行います。
			prepare_batankyu_logic(frame)
			current_mode = GameMode.BATANKYU_ANIMATION # バタンキューアニメーションへ遷移
				
		GameMode.BATANKYU_ANIMATION:
			# バタンキューアニメーションを実行します。
			batankyu_logic(frame)
  1. 新しいぷよを生成時にゲームオーバの判定処理をPlayer.gdに追加します。
# 新しい操作用ぷよを生成する (JavaScriptのPlayer.createNewPuyo()に対応)
func create_new_puyo() -> bool:
	# ゲームオーバー判定: 1番上の段の左から3列目にぷよが既にあったらゲームオーバー
	if stage_node.board[0][2] != null:
		return false # 新しいぷよを置けないため、ゲームオーバー
    【以下は省略】
  1. 確認用においていたGameMode.NEW_PUYOの中のコードを削除しましょう。
		GameMode.NEW_PUYO:
			# 新しい操作用ぷよを生成
			if not player_node.create_new_puyo():
				current_mode = GameMode.GAME_OVER # 生成できなかったらゲームオーバー
			else:
				current_mode = GameMode.PLAYER_PLAYING # プレイヤー操作可能状態へ
			# TODO:確認用
			# 削除: player_node.puyo_status.y = 1
			# 削除: player_node.set_puyo_position()
  1. ゲームオーバー時に上キーを押したらゲームを初めから再開できるようにします。
		GameMode.BATANKYU_ANIMATION:
			# バタンキューアニメーションを実行します。
			batankyu_logic(frame)

			# 上キーが押されたらゲームをリロードして再開
			if player_node.key_status.up:
				get_tree().reload_current_scene()

動作を確認してみましょう。ぷよを上まで積み上げるとゲームオーバーになり、上キーでゲームが再スタートされることが確認できます。

まとめ

今回はぷよの固定処理とゲームオーバの処理を作成しました。
次回は得点処理を書いていきます。いよいよ最後です。お楽しみに!!

Discussion