🎲

ぷよぷよプログラミングをGodotで実装 06 実践編 :状態管理作成

に公開

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

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

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

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

5. 状態管理処理の追加(基礎)

主要なシーンとスクリプトが出来てので、少しずつ機能を追加していきます。

5.1 ゲームの状態管理

ゲームの進行フェーズを管理してきます。

  1. Main.gdに下記のコードを追加します。
# --- ゲームの状態を定義する列挙型 ---
# JavaScript版のmode変数に対応し、ゲームの進行フェーズを明確にします。
enum GameMode {
	START,              # ゲーム開始時(初期設定、一度だけ実行)
	CHECK_FALL,         # 盤面全体で自由落下が必要かチェックするフェーズ
	FALL,               # ぷよが重力によって自由落下するアニメーションフェーズ
	CHECK_ERASE,        # 消せるぷよがあるかチェックするフェーズ
	ERASING,            # ぷよが消去アニメーションを行うフェーズ
	NEW_PUYO,           # 新しい操作用ぷよを生成するフェーズ
	PLAYER_PLAYING,     # プレイヤーがぷよを自由に操作できる通常状態
	PLAYER_MOVING,      # プレイヤーのぷよが横移動アニメーション中の状態
	PLAYER_ROTATING,    # プレイヤーのぷよが回転アニメーション中の状態
	FIXING_PUYO,        # ぷよが盤面に固定される最終処理フェーズ
	GAME_OVER,          # ゲームオーバーが確定し、バタンキューアニメーションを準備するフェーズ
	BATANKYU_ANIMATION  # ゲームオーバー時のバタンキューアニメーション表示中
}

# --- ゲーム状態変数 ---
var current_mode: GameMode = GameMode.START  # 現在のゲームモード(初期値はSTART)
  1. initialize_gameメソッド内で初期状態を設定します。
	current_mode = GameMode.START # ゲームモードを初期状態に設定
  1. 毎フレーム呼び出されるゲームループのメイン処理の作成
# _process(_delta): 毎フレーム呼び出されるゲームループのメイン処理。
func _process(_delta: float) -> void:

	# 現在のゲームモードに応じた処理を実行
	match current_mode:
		GameMode.START:
			# ゲーム開始時、最初は自由落下チェックからゲームを開始します。
			pass

		GameMode.CHECK_FALL:
			# 盤面全体で自由落下が必要なぷよがあるかチェックします。
			pass

		GameMode.FALL:
			# 自由落下アニメーションを実行します。
			pass

		GameMode.CHECK_ERASE:
			# 消せるぷよがあるかチェックし、消去情報を取得します。
			pass

		GameMode.ERASING:
			# ぷよ消去アニメーションを実行します。
			pass

		GameMode.NEW_PUYO:
			# 新しい操作用ぷよを生成
			pass

		GameMode.PLAYER_PLAYING:
			# プレイヤーによるぷよ操作処理(移動、回転、落下)を呼び出します。
			pass
				
		GameMode.PLAYER_MOVING:
			# プレイヤーの移動アニメーションを実行します。
			pass

		GameMode.PLAYER_ROTATING:
			# プレイヤーの回転アニメーションを実行します。
			pass
				
		GameMode.FIXING_PUYO:
			# ぷよを盤面に固定する処理を実行します。
			pass
				
		GameMode.GAME_OVER:
			# ゲームオーバー時の準備処理を行います。
			pass
				
		GameMode.BATANKYU_ANIMATION:
			# バタンキューアニメーションを実行します。
			pass

ここまで対応するとMain.gsは下記になっていると思います。

# res://scripts/game/Main.gd
extends Node2D

# --- @onready 変数: シーンツリーからのノード参照を自動的に割り当て ---
@onready var stage_node: Node2D = $Stage # Stageシーンのインスタンスへの参照
@onready var player_node: Player = $Player # Playerのインスタンスへの参照 (型をPlayerに指定)

# --- ゲームの状態を定義する列挙型 ---
# JavaScript版のmode変数に対応し、ゲームの進行フェーズを明確にします。
enum GameMode {
	START,              # ゲーム開始時(初期設定、一度だけ実行)
	CHECK_FALL,         # 盤面全体で自由落下が必要かチェックするフェーズ
	FALL,               # ぷよが重力によって自由落下するアニメーションフェーズ
	CHECK_ERASE,        # 消せるぷよがあるかチェックするフェーズ
	ERASING,            # ぷよが消去アニメーションを行うフェーズ
	NEW_PUYO,           # 新しい操作用ぷよを生成するフェーズ
	PLAYER_PLAYING,     # プレイヤーがぷよを自由に操作できる通常状態
	PLAYER_MOVING,      # プレイヤーのぷよが横移動アニメーション中の状態
	PLAYER_ROTATING,    # プレイヤーのぷよが回転アニメーション中の状態
	FIXING_PUYO,        # ぷよが盤面に固定される最終処理フェーズ
	GAME_OVER,          # ゲームオーバーが確定し、バタンキューアニメーションを準備するフェーズ
	BATANKYU_ANIMATION  # ゲームオーバー時のバタンキューアニメーション表示中
}

# --- ゲーム状態変数 ---
var current_mode: GameMode = GameMode.START  # 現在のゲームモード(初期値はSTART)

# _ready(): シーンがツリーに追加され、準備ができたときに一度だけ呼ばれます。
# ゲーム全体の初期設定を行います。
func _ready() -> void:
	initialize_game()

# initialize_game(): ゲーム全体の初期化処理をまとめた関数。
func initialize_game() -> void:

	# Playerノードを初期化し、Stageノードへの参照を渡す
	if is_instance_valid(player_node):
		player_node.initialize(stage_node)
		
	current_mode = GameMode.START # ゲームモードを初期状態に設定
	
# _process(_delta): 毎フレーム呼び出されるゲームループのメイン処理。
func _process(_delta: float) -> void:

	# 現在のゲームモードに応じた処理を実行
	match current_mode:
		GameMode.START:
			# ゲーム開始時、最初は自由落下チェックからゲームを開始します。
			pass

		GameMode.CHECK_FALL:
			# 盤面全体で自由落下が必要なぷよがあるかチェックします。
			pass

		GameMode.FALL:
			# 自由落下アニメーションを実行します。
			pass

		GameMode.CHECK_ERASE:
			# 消せるぷよがあるかチェックし、消去情報を取得します。
			pass

		GameMode.ERASING:
			# ぷよ消去アニメーションを実行します。
			pass

		GameMode.NEW_PUYO:
			# 新しい操作用ぷよを生成
			pass

		GameMode.PLAYER_PLAYING:
			# プレイヤーによるぷよ操作処理(移動、回転、落下)を呼び出します。
			pass
				
		GameMode.PLAYER_MOVING:
			# プレイヤーの移動アニメーションを実行します。
			pass

		GameMode.PLAYER_ROTATING:
			# プレイヤーの回転アニメーションを実行します。
			pass
				
		GameMode.FIXING_PUYO:
			# ぷよを盤面に固定する処理を実行します。
			pass
				
		GameMode.GAME_OVER:
			# ゲームオーバー時の準備処理を行います。
			pass
				
		GameMode.BATANKYU_ANIMATION:
			# バタンキューアニメーションを実行します。
			pass

6. 操作するぷよ関連の処理追加

全体の状態管理が出来たので操作するぷよ関連の処理を作成していきます。

6.1 操作用ぷよを生成する

操作用ぷよを生成するフェーズで操作用のぷよを作るようにしてみましょう。

  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 # プレイヤー操作可能状態へ
  1. 確認のための一時的なコードを埋め込んでいきます。
    • GameMode.STARTフェーズでcurrent_mode = GameMode.NEW_PUYOを設定する
    • 操作用のぷよを盤面の見える位置に移動させる
	match current_mode:
		GameMode.START:
			# TODO:確認用
			current_mode = GameMode.NEW_PUYO # ゲームモードを初期状態に設定
			# ゲーム開始時、最初は自由落下チェックからゲームを開始します。
			pass
		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()

実行すると、ランダムな色のぷよが盤面に表示されることが確認できます。

6.2 操作中のぷよの落下

操作中のぷよが自動で落下するようにコードを追加していきましょう。

  1. Player.gdに落下処理を追加します。
# プレイヤー操作中のぷよを自由落下させる (JavaScriptのPlayer.falling()に対応)
func falling_logic() -> bool:

	# 次のフレームでのぷよの潜在的なピクセルY座標を計算
	var falling_speed: float = Global.PLAYER_FALLING_SPEED
	var next_center_pixel_y = center_puyo_node.position.y + falling_speed

	# 次のフレームでのぷよの潜在的なグリッドY座標を計算
	# int()で小数点以下を切り捨てることで意図したグリッドY座標を得ます。
	var next_center_grid_y = int(floor(next_center_pixel_y / Global.PUYO_IMG_HEIGHT))
	
	# 可動ぷよの次のグリッドY座標は、中心ぷよの次グリッドY座標と相対dyで計算
	var next_movable_grid_y = next_center_grid_y + puyo_status.dy

	var x: int = puyo_status.x
	var dx: int = puyo_status.dx

	# ぷよがブロックされない場合、落下を続ける
	center_puyo_node.position.y = next_center_pixel_y
	# 可動ぷよのピクセルY座標も中心ぷよからの相対位置で更新
	movable_puyo_node.position.y = next_center_pixel_y + (puyo_status.dy * Global.PUYO_IMG_HEIGHT)

	# puyo_status.yを次のグリッドYに更新
	puyo_status.y = next_center_grid_y

	# 落下中もしくは、着地しているが、固定されていない状態
	return false
  1. GameMode.PLAYER_MOVINGを下記のコードに書き換えましょう。
		GameMode.PLAYER_MOVING:
			# プレイヤーの移動アニメーションを実行します。
			# TODO:呼び出しの仮置き
			player_node.falling_logic()
			pass
  1. 動作確認のため、ameMode.PLAYER_PLAYINGに下記のコードを追加します。
		GameMode.PLAYER_PLAYING:
			# プレイヤーによるぷよ操作処理(移動、回転、落下)を呼び出します。
			# TODO: 動作確認用
			current_mode = GameMode.PLAYER_MOVING
			pass

実行すると、ぷよが徐々に落下するようになったことが確認できます。

6.3 操作中のぷよの着地

操作中のぷよが落下するようになりましたが、今のままでは地面を突き抜けて落下してしまいます。
地面がある場合は着地するように衝突の判定処理を追加していきます。

  1. Player.gdに衝突判定の処理を追加します。
### グリッド上の衝突チェック
# 指定されたグリッド座標にぷよがあるか、またはグリッド範囲外かをチェック
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 false
  1. 着地判定のフレーム数を管理する変数を用意します。
var ground_frame: int = 0 # ぷよが接地(下に動けない状態)してから経過したフレーム数
  1. falling_logicの中に衝突判定する処理を組み込みます。
# プレイヤー操作中のぷよを自由落下させる (JavaScriptのPlayer.falling()に対応)
func falling_logic() -> bool:

	# 次のフレームでのぷよの潜在的なピクセルY座標を計算
	var falling_speed: float = Global.PLAYER_FALLING_SPEED
	var next_center_pixel_y = center_puyo_node.position.y + falling_speed

	# 次のフレームでのぷよの潜在的なグリッドY座標を計算
	# int()で小数点以下を切り捨てることで意図したグリッドY座標を得ます。
	var next_center_grid_y = int(floor(next_center_pixel_y / Global.PUYO_IMG_HEIGHT))
	
	# 可動ぷよの次のグリッドY座標は、中心ぷよの次グリッドY座標と相対dyで計算
	var next_movable_grid_y = next_center_grid_y + puyo_status.dy

	var x: int = puyo_status.x
	var dx: int = puyo_status.dx

	# 次のグリッド位置でブロックされるかどうかを事前にチェック
	# 中心ぷよ、可動ぷよの真下にぷよが存在するか、最終行かどうかのチェック 
	if _is_colliding(x, next_center_grid_y + 1) or _is_colliding(x + dx, next_movable_grid_y + 1):
		# ぷよが次のグリッド位置でブロックされる場合
		
		# ピクセル位置をブロックされる直前のグリッド境界にスナップ
		# これにより、ぷよが地面にめり込むことなく正確な位置に固定されます。
		center_puyo_node.position.y = float(next_center_grid_y) * Global.PUYO_IMG_HEIGHT
		movable_puyo_node.position.y = float(next_movable_grid_y) * Global.PUYO_IMG_HEIGHT
		
		# puyo_status.yを最新のグリッドYに更新します。
		# これが接地判定の基準となります。
		puyo_status.y = next_center_grid_y
		
		# 接地フレームをインクリメント
		ground_frame += 1

		if ground_frame > Global.PLAYER_GROUND_FRAME:
			return true # 固定可能

	else:
		# ぷよがブロックされない場合、落下を続ける
		center_puyo_node.position.y = next_center_pixel_y
		# 可動ぷよのピクセルY座標も中心ぷよからの相対位置で更新
		movable_puyo_node.position.y = next_center_pixel_y + (puyo_status.dy * Global.PUYO_IMG_HEIGHT)
		# puyo_status.yを次のグリッドYに更新
		puyo_status.y = next_center_grid_y
		
		# 自由落下中は接地フレームをリセット
		ground_frame = 0 
	# 落下中もしくは、着しているが、固定されていない状態
	return false

これでぷよが着地するようになりました。

6.4 操作中のぷよの横移動

操作中のぷよを横に移動できるようにコードを追加していきましょう。

  1. Player.gdに入力状態を管理する変数を設定します。
# --- 入力状態管理 ---
var key_status: Dictionary = {
	"right": false, "left": false # キーボード押下状態
}
  1. 移動時のアニメーション制御用の変数を用意します。
# --- アニメーション関連変数 ---
var action_start_frame: int = 0       # 移動や回転アニメーションを開始したフレーム数
var move_source_x: float = 0.0        # 移動アニメーションの開始Xピクセル座標
var move_destination_x: float = 0.0   # 移動アニメーションの目標Xピクセル座標
  1. _physics_processメソッドを作成し、キー入力に合わせて入力状態を設定します。
func _physics_process(_delta: float) -> void:
	# 入力アクションの処理
	if Input.is_action_pressed("ui_left"):
		key_status.left = true
	else:
		key_status.left = false
	
	if Input.is_action_pressed("ui_right"):
		key_status.right = true
	else:
		key_status.right = false
  1. 移動後の座標データを管理するため_handle_horizontal_movementを作成します。
### 左右移動の処理
func _handle_horizontal_movement(current_frame: int) -> bool:
	var cx: int = 1 if key_status.right else -1 # 移動方向 (1:右, -1:左)
	var x: int = puyo_status.x
	var y: int = puyo_status.y
	var mx: int = x + puyo_status.dx # 可動ぷよのXグリッド座標
	var my: int = y + puyo_status.dy # 可動ぷよのYグリッド座標

	var can_move: bool = true # 移動可能かどうかを示すフラグ

	# 移動先のスペースが利用可能かチェック
	if _is_colliding(x + cx, y) or \
	   _is_colliding(mx + cx, my):
		can_move = false

	# 接地していない場合のみ、さらに1個下のマスとの衝突チェック
	if ground_frame == 0:
		if _is_colliding(x + cx, y + 1) or \
		   _is_colliding(mx + cx, my + 1):
			can_move = false

	if can_move:
		# 移動できるので、アニメーション開始情報を設定し、移動状態へ遷移
		action_start_frame = current_frame
		move_source_x = center_puyo_node.position.x # 移動開始Xピクセル座標を記録
		move_destination_x = float(x + cx) * Global.PUYO_IMG_WIDTH # 移動終了Xピクセル座標を計算
		puyo_status.x += cx # 中心ぷよのグリッドX座標を更新
		return true

	return false # 移動不可の場合
  1. プレイヤーの操作を受け付け、状態を遷移させるためのplaying_logicメソッドを作成します。
# プレイヤーの操作を受け付け、状態を遷移させる (JavaScriptのPlayer.playing()に対応)
# 戻り値: 次のゲーム状態を示す文字列 ("playing", "moving", "rotating", "fix")
func playing_logic(current_frame: int) -> String:
	# 自由落下をチェックします。接地フレーム制限を超えたら固定状態へ遷移します。
	if falling_logic():
		return "fix"

	# 左右移動のチェック
	if key_status.right or key_status.left:
		if _handle_horizontal_movement(current_frame):
			return "moving"
	# TODO:回転のチェック (上キー)
	# return "rotating"

	return "playing" # プレイヤーが何も操作しなかった場合
  1. Main.gdに入力状態に合わせてゲーム状態を設定させます
  • 必要な変数の設定
# --- ゲーム状態変数 ---
var current_mode: GameMode = GameMode.START  # 現在のゲームモード(初期値はSTART)
var frame: int = 0                           # ゲームの現在フレーム数(1/60秒ごとに1増加) * 追加
  • _processの先頭でフレームカウンターを更新
# _process(_delta): 毎フレーム呼び出されるゲームループのメイン処理。
func _process(_delta: float) -> void:
	frame += 1 # フレームカウンターを更新 (JavaScriptのrequestAnimationFrameによるframe増加に相当)
  • GameMode.PLAYER_PLAYING内の処理を書き換えます。
		GameMode.PLAYER_PLAYING:
			# プレイヤーによるぷよ操作処理(移動、回転、落下)を呼び出します。
			var action: String = player_node.playing_logic(frame)
			if action == "fix":
				current_mode = GameMode.FIXING_PUYO # ぷよが固定される状態へ遷移
			elif action == "moving":
				current_mode = GameMode.PLAYER_MOVING # 横移動アニメーションへ遷移
			elif action == "rotating":
				current_mode = GameMode.PLAYER_ROTATING # 回転アニメーションへ遷移
  1. 移動用のメソッドを新しく作成します。
# プレイヤーの移動アニメーションを実行する (JavaScriptのPlayer.moving()に対応)
# 戻り値: アニメーション中ならtrue, 完了したらfalse
func moving_logic(current_frame: int) -> bool:
	
	# アニメーションの進行度 (0.0から1.0) を計算
	var ratio: float = minf(1.0, float(current_frame - action_start_frame) / Global.PLAYER_MOVE_FRAME)
	# 線形補間によりぷよのX座標を更新
	var current_x_pixel: float = lerp(move_source_x, move_destination_x, ratio)
	center_puyo_node.position.x = current_x_pixel
	movable_puyo_node.position.x = center_puyo_node.position.x + (puyo_status.dx * Global.PUYO_IMG_WIDTH)

	if ratio == 1.0: # アニメーションが完了した場合
		# 最終位置を正確にグリッドに合わせます。
		center_puyo_node.position.x = move_destination_x
		movable_puyo_node.position.x = center_puyo_node.position.x + (puyo_status.dx * Global.PUYO_IMG_WIDTH)
		puyo_status.x = int(round(move_destination_x / Global.PUYO_IMG_WIDTH)) # 浮動小数点誤差を考慮して丸めます
		# Player.gd内でキー状態のリセットを行う
		key_status.left = false
		key_status.right = false
		return false # アニメーション終了
	
	return true # アニメーション中
  1. falling_logicmoving_logicから呼び出すようにします。
# プレイヤーの移動アニメーションを実行する (JavaScriptのPlayer.moving()に対応)
# 戻り値: アニメーション中ならtrue, 完了したらfalse
func moving_logic(current_frame: int) -> bool:
	
	falling_logic() # 追加
	
	# アニメーションの進行度 (0.0から1.0) を計算
	var ratio: float = minf(1.0, float(current_frame - action_start_frame) / Global.PLAYER_MOVE_FRAME)
	
	# 線形補間によりぷよのX座標を更新
	var current_x_pixel: float = lerp(move_source_x, move_destination_x, ratio)
	center_puyo_node.position.x = current_x_pixel
	movable_puyo_node.position.x = center_puyo_node.position.x + (puyo_status.dx * Global.PUYO_IMG_WIDTH)

	if ratio == 1.0: # アニメーションが完了した場合
		# 最終位置を正確にグリッドに合わせます。
		center_puyo_node.position.x = move_destination_x
		movable_puyo_node.position.x = center_puyo_node.position.x + (puyo_status.dx * Global.PUYO_IMG_WIDTH)
		puyo_status.x = int(round(move_destination_x / Global.PUYO_IMG_WIDTH)) # 浮動小数点誤差を考慮して丸めます
		# Player.gd内でキー状態のリセットを行う
		key_status.left = false
		key_status.right = false
		return false # アニメーション終了
	
	return true # アニメーション中
  • GameMode.PLAYER_MOVING内の処理を書き換えます。
		GameMode.PLAYER_MOVING:
			# プレイヤーの横移動アニメーションを実行します。
			if not player_node.moving_logic(frame):
				# 移動アニメーションが終了したら、プレイヤー操作可能状態に戻ります。
				current_mode = GameMode.PLAYER_PLAYING 

実行すると、左右のキーを押した時にぷよが左右に移動しながら落下することが確認できます。

6.5 操作中のぷよの加速落下

操作中のぷよを下キーを押している間の落下速度をあげてみましょう。

  1. 入力状態の管理の配列にdownを追加します
# --- 入力状態管理 ---
var key_status: Dictionary = {
	"right": false, "left": false, "down": false # キーボード押下状態
}
  1. _physics_processで下キーの入力処理を追加します。
func _physics_process(_delta: float) -> void:
    {省略}
	if Input.is_action_pressed("ui_down"): # 加速落下キー
		key_status.down = true
	else:
		key_status.down = false
  1. falling_logicに下キーが押されているか判断するための変数を追加します。
    func falling_logic(is_down_pressed: bool) -> bool:
  1. is_down_pressedtrueの時に落下速度を上げる処理を書きます。
	if is_down_pressed:
		falling_speed += Global.PLAYER_DOWN_SPEED

最終的にfalling_logicは下記のようになります。

# プレイヤー操作中のぷよを自由落下させる (JavaScriptのPlayer.falling()に対応)
# is_down_pressed: 下キーが押されているか (加速落下のため)
# 戻り値: ぷよが固定できる状態になったらtrue, 落下中ならfalse
func falling_logic(is_down_pressed: bool) -> bool:

	# 次のフレームでのぷよの潜在的なピクセルY座標を計算
	var falling_speed: float = Global.PLAYER_FALLING_SPEED

	if is_down_pressed:
		falling_speed += Global.PLAYER_DOWN_SPEED

	var next_center_pixel_y = center_puyo_node.position.y + falling_speed

	# 次のフレームでのぷよの潜在的なグリッドY座標を計算
	# int()で小数点以下を切り捨てることで意図したグリッドY座標を得ます。
	var next_center_grid_y = int(floor(next_center_pixel_y / Global.PUYO_IMG_HEIGHT))
	
	# 可動ぷよの次のグリッドY座標は、中心ぷよの次グリッドY座標と相対dyで計算
	var next_movable_grid_y = next_center_grid_y + puyo_status.dy

	var x: int = puyo_status.x
	var dx: int = puyo_status.dx

	# 次のグリッド位置でブロックされるかどうかを事前にチェック
	# 中心ぷよ、可動ぷよの真下にぷよが存在するか、最終行かどうかのチェック 
	if _is_colliding(x, next_center_grid_y + 1) or _is_colliding(x + dx, next_movable_grid_y + 1):
		# ぷよが次のグリッド位置でブロックされる場合
		
		# ピクセル位置をブロックされる直前のグリッド境界にスナップ
		# これにより、ぷよが地面にめり込むことなく正確な位置に固定されます。
		center_puyo_node.position.y = float(next_center_grid_y) * Global.PUYO_IMG_HEIGHT
		movable_puyo_node.position.y = float(next_movable_grid_y) * Global.PUYO_IMG_HEIGHT
		
		# puyo_status.yを最新のグリッドYに更新します。
		# これが接地判定の基準となります。
		puyo_status.y = next_center_grid_y
		
		# 接地フレームをインクリメント
		ground_frame += 1

		if ground_frame > Global.PLAYER_GROUND_FRAME:
			return true # 固定可能

	else:
		# ぷよがブロックされない場合、落下を続ける
		center_puyo_node.position.y = next_center_pixel_y
		# 可動ぷよのピクセルY座標も中心ぷよからの相対位置で更新
		movable_puyo_node.position.y = next_center_pixel_y + (puyo_status.dy * Global.PUYO_IMG_HEIGHT)
		# puyo_status.yを次のグリッドYに更新
		puyo_status.y = next_center_grid_y
		
		# 自由落下中は接地フレームをリセット
		ground_frame = 0 
	# 落下中もしくは、着しているが、固定されていない状態
	return false
  1. 呼び出し元の引数を設定していきます。
func moving_logic(current_frame: int) -> bool:
	falling_logic(key_status.down)
    {コード省略}

func playing_logic(current_frame: int) -> String:
	# 自由落下をチェックします。接地フレーム制限を超えたら固定状態へ遷移します。
	if falling_logic(key_status.down):
		return "fix"
    {コード省略}

ここまでの状態で実行すると、下キーを押した時にぷよ加速して落下していくことが確認できます。

6.6 操作中のぷよの回転

操作中のぷよを回転できるようにコードを追加していきましょう。
回転は基準となるぷよを中心に反時計回りで90度回転させていきます。

  1. 入力状態の管理の配列にupを追加します
# --- 入力状態管理 ---
var key_status: Dictionary = {
	"right": false, "left": false, "up": false, "down": false # キーボード押下状態
}
  1. _physics_processで上キーの入力処理を追加します。
func _physics_process(_delta: float) -> void:
    {省略}
	if Input.is_action_pressed("ui_up"): # 回転キー
		key_status.up = true
	else:
		key_status.up = false
  1. 回転アニメーションに利用する変数を追加します。
var rotate_from_rotation: float = 0.0 # 回転アニメーション開始時のぷよの角度
  1. 回転後の座標データを管理するため_handle_rotationを作成します。
## 回転の処理
func _handle_rotation(current_frame: int) -> bool:
	var current_x: int = puyo_status.x
	var current_y: int = puyo_status.y
	var current_rotation: int = puyo_status.rotation
	var next_rotation: int = int(fmod(current_rotation + 90, 360)) # 次の回転角度 (0, 90, 180, 270)

	# 次の回転角度に応じた可動ぷよの相対位置 (dx, dy) を計算
	var next_dx: int = 0
	var next_dy: int = 0
	match next_rotation:
		0:   next_dx = 1; next_dy = 0   # 右に連結
		90:  next_dx = 0; next_dy = -1  # 上に連結
		180: next_dx = -1; next_dy = 0  # 左に連結
		270: next_dx = 0; next_dy = 1   # 下に連結

	# 回転時の中心ぷよのズレ補正 (壁際や地面との衝突判定を考慮)
	var compensated_cx: int = 0
	var compensated_cy: int = 0
	var needs_y_compensation: bool = false

	# ズレ補正が必要なケースを定義
	if current_rotation == 90: # 現在上向き → 次に右向き
		if _is_colliding(current_x - 1, current_y): # 左隣が衝突
			compensated_cx = 1
	elif current_rotation == 180: # 現在左向き → 次に下向き
		# 下または左下が衝突している場合
		if _is_colliding(current_x, current_y + 1) or _is_colliding(current_x - 1, current_y + 1):
			compensated_cy = -1
			needs_y_compensation = true
	elif current_rotation == 270: # 現在下向き → 次に左向き
		if _is_colliding(current_x + 1, current_y): # 右隣が衝突
			compensated_cx = -1

	# 最終的な回転後の位置に障害物がないか確認
	var final_center_x: int = current_x + compensated_cx
	var final_center_y: int = current_y + compensated_cy
	var final_mobile_x: int = final_center_x + next_dx
	var final_mobile_y: int = final_center_y + next_dy

	if _is_colliding(final_center_x, final_center_y) or \
	   _is_colliding(final_mobile_x, final_mobile_y):
		return false # 回転不可

	# 回転可能
	# 上にずれる必要がある場合、Y座標を即時調整し、接地フレームをリセット
	if needs_y_compensation and ground_frame > 0:
		puyo_status.y -= 1
		ground_frame = 0 # 接地状態をリセット
		center_puyo_node.position.y = float(puyo_status.y) * Global.PUYO_IMG_HEIGHT # ピクセル位置を即時反映

	# 回転アニメーション開始情報を設定
	action_start_frame = current_frame
	move_source_x = center_puyo_node.position.x
	move_destination_x = float(final_center_x) * Global.PUYO_IMG_WIDTH

	rotate_from_rotation = puyo_status.rotation # 回転開始時の角度を記録

	# 次の回転状態を設定 (アニメーション完了時の最終状態)
	puyo_status.x = final_center_x
	puyo_status.rotation = next_rotation
	puyo_status.dx = next_dx
	puyo_status.dy = next_dy

	return true
  1. playing_logicに上キーが押された時の処理を追加します。
	# 左右移動のチェック
	if key_status.right or key_status.left:
		if _handle_horizontal_movement(current_frame):
			return "moving"
	# 回転のチェック (上キー)
	elif key_status.up:
		if _handle_rotation(current_frame):
			return "rotating"
  1. プレイヤーの操作を受け付け、状態を遷移させるためのrotating_logicメソッドを作成します。
func rotating_logic(current_frame: int) -> bool:
	# 修正: 下キーの状態をplayer_falling_logicに渡す
	falling_logic(key_status.down) 
	
	# アニメーションの進行度 (0.0から1.0) を計算
	var ratio: float = minf(1.0, float(current_frame - action_start_frame) / Global.PLAYER_ROTATE_FRAME)
	
	# 中心ぷよのX座標を補間(壁に当たった時の跳ね返り)
	#var current_center_x_pixel: float = lerp(move_source_x, move_destination_x, ratio)
	center_puyo_node.position.x = lerp(move_source_x, move_destination_x, ratio)
	
	# 補間された相対位置に基づいて可動ぷよの絶対位置を更新
	movable_puyo_node.position.x = center_puyo_node.position.x + (cos(deg_to_rad(rotate_from_rotation + (90 * ratio))) * Global.PUYO_IMG_WIDTH)
	movable_puyo_node.position.y = center_puyo_node.position.y - (sin(deg_to_rad(rotate_from_rotation + (90 * ratio))) * Global.PUYO_IMG_HEIGHT)

	if ratio == 1.0: # アニメーションが完了した場合
		# 最終位置と角度を正確に設定します。
		center_puyo_node.position.x = move_destination_x
		puyo_status.x = int(round(move_destination_x / Global.PUYO_IMG_WIDTH))
		puyo_status.rotation = int(fmod(rotate_from_rotation + 90, 360)) # 最終角度を正確に設定
		
		# 可動ぷよの最終位置も確定 (補間ではなく、puyo_statusの最終値に基づいてスナップ)
		movable_puyo_node.position.x = center_puyo_node.position.x + (puyo_status.dx * Global.PUYO_IMG_WIDTH)
		movable_puyo_node.position.y = center_puyo_node.position.y + (puyo_status.dy * Global.PUYO_IMG_HEIGHT)
		
		return false # アニメーション終了
	
	return true # アニメーション中
  1. GameMode.PLAYER_PLAYING内の処理を書き換えます。
		GameMode.PLAYER_ROTATING:
			# プレイヤーの回転アニメーションを実行します。
			if not player_node.rotating_logic(frame):
				# 回転アニメーションが終了したら、プレイヤー操作可能状態に戻ります。
				current_mode = GameMode.PLAYER_PLAYING 

ここまでの状態で実行すると、上キーを押した時にぷよが回転することが確認できます。

まとめ

今回は状態管理処理の大枠と操作まわりを作成しました。
次回は状態管理処理の続きから書いていきます。

Discussion