👾

ぷよぷよプログラミングをGodotで実装 05 実践編 :基礎作成

に公開

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

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

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

動画はこちら
https://youtu.be/9PhiqwiwpT8

1. はじめに

前回まではぷよぷよプログラミングのJavaScriptの中身を見ていきましたが、今回からはいよいよGodot でぷよぷよを作っていきます。
このチュートリアルを終えることで、Godot Engineでの2Dゲーム開発の基礎、シングルトン(オートロード)の活用、シーン構造の設計、アニメーションの実装、そしてゲームロジックの管理について理解を深めることができます。

学習目標

  • Godotプロジェクトのセットアップ
  • ゲームアセットの準備とインポート
  • ゲームの主要なシーンとノード構造の設計
  • GDScriptを使用したぷよの動き、落下、消去、連鎖、スコア計算、ゲーム状態管理の実装
  • ビューポートとウィンドウサイズの動的な調整

2. 準備するもの

  • Godot Engine 4.x: 最新版のGodot Engineを公式サイトからダウンロードしてインストールしてください。
  • Monacaアカウント登録: 元になる教材「ぷよぷよプログラミング」を利用するために必要です。
  • ゲームアセット:
    • ぷよの画像(puyo_1.png ~ puyo_5.png)
    • スコア表示用の数字画像(0.png ~ 9.png)
    • ゲームオーバー画面用の画像(batankyu.png)
    • 全消しボーナス表示用の画像(zenkeshi.png)

アセットの準備

ぷよぷよプログラミングで学習用に用意されている画像を利用していきます。
これらの画像は、プロジェクト内の res://assets/images/ ディレクトリに配置することを想定しています。

3. プロジェクトのセットアップ

3.1 新規Godotプロジェクトの作成

  1. Godot Engineを起動します。
  2. +作成をクリックします。
  3. プロジェクト名をPuyoPuyoGameのように入力し、プロジェクトパスを設定します。
  4. レンダラーForward+またはモバイルのいずれかを選択します(2Dゲームでは通常どちらでも問題ありませんが、モバイルの方が軽量です)。
  5. 作成して編集をクリックします。

3.2 フォルダ構造の作成

プロジェクトが作成されたら、res://ディレクトリ直下に以下のフォルダを作成します。

  • res://assets/ (画像などのアセットを格納)
    • res://assets/images/ (上記で準備した画像ファイルをここに入れます)
  • res://scenes/ (Godotのシーンファイルを格納)
    • res://scenes/game/ (ゲーム本体のシーンを格納)
  • res://scripts/ (GDScriptファイルを格納)
    • res://scripts/autoload/ (シングルトンスクリプトを格納)
    • res://scripts/game/ (ゲームロジックのスクリプトを格納)

3.3 アセットのインポート

作成したres://assets/imagesフォルダの中に画像ファイルをドラッグ&ドロップしてください。

  • res://assets/images/puyo_1.png
  • res://assets/images/puyo_2.png
  • res://assets/images/puyo_3.png
  • res://assets/images/puyo_4.png
  • res://assets/images/puyo_5.png
  • res://assets/images/0.png
  • res://assets/images/1.png
  • ...
  • res://assets/images/9.png
  • res://assets/images/batankyu.png
  • res://assets/images/zenkeshi.png

3.4 プロジェクト設定の構成

プロジェクトを快適に動作させるために、いくつか設定を変更します。

  1. Godotエディタのメニューでプロジェクト > プロジェクト設定 を選択します。
  2. 表示 (Display) > ウィンドウ (Window) に移動します。
    • ビューポートの幅: 240 (6カラム * 40ピクセル/ぷよ)
    • ビューポートの高さ: 500 (12行 * 40ピクセル/ぷよ + スコアの高さ20ピクセル)
    • ストレッチのモード: Canvas Items (2Dゲームに適したスケーリングモード)
    • アスペクト (Aspect): Keep (アスペクト比を維持し、黒帯で埋める)
  3. 閉じるをクリックして設定を保存します。

3.5 オートロード(シングルトン)の設定

ゲーム全体で共有されるデータや機能を持つスクリプトは、オートロード(シングルトン)として設定します。これにより、シーンツリーのどこからでもグローバルにアクセスできるようになります。

  1. res://scripts/autoload/フォルダ内に、新しいGDScriptファイルを作成し、それぞれ Global.gd と Score.gd と名前を付けます。
  2. プロジェクト > プロジェクト設定 > グローバル タブに移動します。
  3. パスの右にあるフォルダアイコンをクリックし、res://scripts/autoload/Global.gdを選択します。
  4. ノード名Globalとなっていることを確認し、追加をクリックします。
  5. 同様に、res://scripts/autoload/Score.gdを追加し、ノード名をScoreとします。

これで、スクリプト内でGlobal.任意のプロパティScore.任意のメソッドのようにアクセスできるようになります。

4. シーンとスクリプトの作成

このセクションでは、ゲームを構成する主要なシーンと、それらにアタッチするスクリプトを作成していきます。

4.1 Globalスクリプトの作成 (Global.gd)

ゲーム全体で使用する定数やリソース、シングルトン間の参照を管理するスクリプトです。

  1. res://scripts/autoload/Global.gdを開きます。
  2. 内容を以下に記述します。
# res://scripts/autoload/Global.gd
# このスクリプトはオートロード(シングルトン)として設定され、
# プロジェクト内のどこからでも Global.PropertyName でアクセスできます。
extends Node

# --- ゲーム全体の設定定数 ---

# ステージのサイズ設定
const STAGE_COLS: int = 6           # ステージの列数(横のぷよの数)
const STAGE_ROWS: int = 12          # ステージの行数(縦のぷよの数)
const PUYO_IMG_WIDTH: int = 40      # ぷよ1つあたりの画像の幅(ピクセル)
const PUYO_IMG_HEIGHT: int = 40     # ぷよ1つあたりの画像の高さ(ピクセル)
const PUYO_COLORS: int = 5          # ぷよの色の種類数 (1から5)
const FREE_FALLING_SPEED: float = 1.5

# プレイヤー操作ぷよの動作設定
const PLAYER_FALLING_SPEED: float = 0.5 # プレイヤーが操作中のぷよの基本落下速度(ピクセル/フレーム)
const PLAYER_DOWN_SPEED: float = 2.5    # 下キー押下時の追加落下速度(ピクセル/フレーム)
const PLAYER_GROUND_FRAME: int = 30     # ぷよが接地してから盤面に固定されるまでの猶予フレーム数
const PLAYER_MOVE_FRAME: int = 10       # ぷよの横移動アニメーションにかけるフレーム数
const PLAYER_ROTATE_FRAME: int = 15     # ぷよの回転アニメーションにかけるフレーム数

# ぷよ消去と連鎖の設定
const ERASE_PUYO_COUNT: int = 4          # ぷよを消去するために必要な連結数
const ERASE_ANIMATION_DURATION: int = 30 # 消去アニメーションが1サイクルにかけるフレーム数

# ゲームオーバーと特殊アニメーションの設定
const GAME_OVER_FRAME: int = 3000     # ゲームオーバー時のバタンキューアニメーションフレーム数
const ZENKESHI_DURATION: float = 0.15 # 全消し(全消しボーナス)アニメーションの表示時間(秒)

# 全消しの得点
const ZENKESHI_SCORE: int = 3600

# ぷよのテクスチャ配列(色の種類に対応)
# ぷよの種類1はPUYO_TEXTURES[0]、ぷよの種類2はPUYO_TEXTURES[1]... と対応させる
const PUYO_TEXTURES: Array[Texture2D] = [
	preload("res://assets/images/puyo_1.png"),
	preload("res://assets/images/puyo_2.png"),
	preload("res://assets/images/puyo_3.png"),
	preload("res://assets/images/puyo_4.png"),
	preload("res://assets/images/puyo_5.png")
]
# バタンキュー(ゲームオーバー)表示用のテクスチャ
const BATANKYU_TEXTURE: Texture2D = preload("res://assets/images/batankyu.png")
# 全消し表示用のテクスチャ
const ZENKESHI_TEXTURE: Texture2D = preload("res://assets/images/zenkeshi.png")

# --- シングルトン間の参照保持 ---
# Scoreシングルトンへの参照を保持するための変数。
# Score.gdの_ready()関数内でこの変数が設定されます。
# ここでは型ヒントとしてNodeを設定していますが、実際はScore.gdのインスタンスが設定されます。
var Score: Node = null # Score.gdのインスタンス(Node継承クラス)がここに代入される

4.2 Stageシーンの作成と構成 (Stage.tscn, Stage.gd)

盤面の管理と、落ちてくるぷよや消えるぷよの描画を担当します。
まずは盤面の基礎部分から作っていきましょう。

  1. res://scenes/game/フォルダ内に新しいシーンを作成します。
  2. ルートノードとしてNode2Dを選択し、名前をStageとします。
  3. Stageシーンに以下のノードを追加します。
    • Background (ColorRect): 盤面の背景として使用します。
      • Stageの子としてColorRectノードを追加し、名前をBackgroundとします。
      • 「インスペクター」パネルで Colorを好きな色(例:#2E2E2Eのような濃い灰色)に設定します。
      • サイズはスクリプト(Stage.gdの_ready())で動的に設定していきます。
  4. Stage ノードに新しいGDScriptをアタッチし、res://scripts/game/Stage.gd と名前を付けます。
  5. Stage.gd の内容を以下に記述します。
# res://scripts/game/Stage.gd
extends Node2D

# シーンがツリーに追加され、準備ができたときに呼ばれる
func _ready() -> void:

	# スクリプトで盤面のサイズを設定する場合
	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)

ここでStage.tscnを実行してみましょう。
ステージの列数、行数とぷよぷよのサイズにを考慮した範囲に盤面の背景が反映されていることがわかります。

4.3 Mainシーンの作成と構成 (Main.tscn)

ゲーム全体の司令塔です。ぷよぷよゲームを構成する他の重要なシーンのインスタンスを作成し、それらを組み合わせてゲーム全体を機能させます。

  1. res://scenes/game/フォルダ内に新しいシーンを作成します。
  2. ルートノードとしてNode2Dを選択し、名前をMainとします。
  3. Stageシーンのインスタンス化
    • Mainノードを選択し、ツールバーの「子シーンをインスタンス化」ボタン(鎖アイコン)をクリックします。
    • res://scenes/game/Stage.tscnを選択し、Mainの子として配置します。ノード名がStageになっていることを確認します。

ここでMain.tscnを実行してみましょう。
Stageシーンで作成した盤面が反映されていることが確認できます。

4.4 Puyoシーンの作成 (Puyo.tscn, Puyo.gd)

個々のぷよの見た目を定義します。

  1. res://scenes/game/フォルダ内に新しいシーンを作成します。
  2. ルートノードとしてSprite2Dを選択し、名前をPuyoとします。
  3. Puyoノードを選択した状態で、インスペクターパネルのテクスチャ (Texture)プロパティに、res://assets/images/puyo_1.pngなど、いずれかのぷよ画像をドラッグ&ドロップして設定します。(スクリプトで動的に変更されます)
  4. 画像基準が中心になっているので、左上になるようにインスペクターパネルのOffsetCenteredのチェックボックを外します。
  5. Puyoノードに新しいGDScriptをアタッチし、res://scripts/game/Puyo.gdと名前を付けます。
  6. Puyo.gd の内容を以下に記述します。
# res://scripts/game/Puyo.gd
# このスクリプトは個々のぷよの動作と外観を定義します。
# 各ぷよはSprite2Dとして描画され、そのタイプ(色)に応じてテクスチャが設定されます。
extends Sprite2D

# --- 変数 ---
var puyo_type: int = 0 # このぷよのタイプ(色を表すID)。0は空きマスや特殊なぷよに使うことが多い。


# --- カスタム関数 ---

# set_puyo_type(type_id): ぷよのタイプを設定し、対応するテクスチャをロードして表示します。
# type_id: 設定するぷよのタイプ(色ID)。Global.PUYO_TEXTURESのインデックスに対応します。
func set_puyo_type(type_id: int) -> void:
	puyo_type = type_id # ぷよのタイプを保存

	# Globalから対応するテクスチャをロードします。
	# type_id は 1から始まるため、配列のインデックスに合わせるために -1 します。
	if type_id >= 1 and type_id <= Global.PUYO_TEXTURES.size():
		texture = Global.PUYO_TEXTURES[type_id - 1]
		
		# ぷよのテクスチャサイズがConfig.PUYO_IMG_WIDTH/HEIGHTと異なる場合、スケールを調整します。
		if texture and (texture.get_width() != Global.PUYO_IMG_WIDTH or texture.get_height() != Global.PUYO_IMG_HEIGHT):
			scale.x = float(Global.PUYO_IMG_WIDTH) / texture.get_width()
			scale.y = float(Global.PUYO_IMG_HEIGHT) / texture.get_height()

	else:
		# 無効なタイプIDの場合は警告を出力し、テクスチャをクリアします。
		push_warning("Invalid puyo type_id: %d. Clearing texture." % type_id)
		texture = null # テクスチャをクリア

4.5 Playerクラスの作成 (Player.gd)

プレイヤーが操作する「落ちてくるぷよ(2つ1組)」の見た目と挙動を定義するクラスです。

  1. res://script/game/フォルダ内に新しいスクリプトを作成し、名前をPlayerとします。
  2. Player.gd の内容を以下に記述します。
# res://scripts/game/Player.gd
# このスクリプトは、プレイヤーが操作するぷよの挙動を管理します。
# 具体的には、落下、左右移動、回転、固定などのロジックを含みます。
class_name Player
extends Node2D

# --- @onready 変数: シーンツリーからのノード参照を自動的に割り当て ---
# Main.gdからこのノードの子としてStageが渡されることを想定
@onready var stage_node: Node2D = null # Stageシーンのインスタンスへの参照 (Main.gdから設定される)

# ぷよのシーン (インスタンス化して使用する)
const PUYO_SCENE = preload("res://scenes/game/Puyo.tscn")

# --- プレイヤー操作中のぷよ情報 ---
var center_puyo_node: Node2D = null # 操作する2つのぷよのうち、基準となる中心ぷよのノード
var movable_puyo_node: Node2D = null # 中心ぷよと連結している可動ぷよのノード
var puyo_status: Dictionary = {
	"x": 0, "y": 0,      # 中心ぷよのグリッド座標(x:列, y:行)
	"dx": 0, "dy": 0,    # 可動ぷよの中心ぷよからの相対グリッド座標(dx:相対列, dy:相対行)
	"rotation": 90       # 可動ぷよの相対回転角度 (0:右, 90:上, 180:左, 270:下)
}


# initialize(): Playerクラスのインスタンスが作成された際に初期化を行う
func initialize(stage: Node2D) -> void:
	stage_node = stage # Stageノードへの参照を設定
	# プレイヤーぷよ関連の変数をリセット
	center_puyo_node = null
	movable_puyo_node = null
	puyo_status = {
		"x": 0, "y": 0,
		"dx": 0, "dy": 0,
		"rotation": 90
	}

# 新しい操作用ぷよを生成する (JavaScriptのPlayer.createNewPuyo()に対応)
func create_new_puyo() -> bool:

	# 新しいぷよの色をランダムに決定します。
	# Global.PUYO_COLORS で設定された色数の範囲内でランダムに選択します。
	var puyo_colors_count: int = maxi(1, mini(5, Global.PUYO_COLORS))
	var center_puyo_type: int = randi_range(1, puyo_colors_count)
	var movable_puyo_type: int = randi_range(1, puyo_colors_count)
	
	# ぷよシーンをインスタンス化します。
	center_puyo_node = PUYO_SCENE.instantiate()
	movable_puyo_node = PUYO_SCENE.instantiate()
	
	# 生成したぷよのタイプ(色)を設定します。
	center_puyo_node.set_puyo_type(center_puyo_type)
	movable_puyo_node.set_puyo_type(movable_puyo_type)
	
	# Mainシーンの子としてぷよノードを追加し、画面に表示されるようにします。
	add_child(center_puyo_node)
	add_child(movable_puyo_node)
	
	# ぷよの初期配置をグリッド座標と可動ぷよの相対位置、回転角度で設定します。
	puyo_status.x = 2    # 中心ぷよの初期グリッドX座標 (0-indexed: 左から3列目)
	puyo_status.y = -1   # 画面上部ギリギリから出てくる(最初は表示されないが落下する)
	puyo_status.dx = 0   # 可動ぷよの初期相対X座標(中心ぷよの真上に連結)
	puyo_status.dy = -1  # 可動ぷよの初期相対Y座標(中心ぷよの真上に連結)
	puyo_status.rotation = 90 # 可動ぷよの初期回転状態(90度: 真上)
	
	set_puyo_position() # 初期位置にぷよを描画(ピクセル位置を設定)
	
	return true

# プレイヤー操作中のぷよの画面上の位置を設定する (JavaScriptのPlayer.setPuyoPosition()に対応)
func set_puyo_position() -> void:
	# ぷよノードが有効でない場合は処理しない
	if not is_instance_valid(center_puyo_node) or not is_instance_valid(movable_puyo_node):
		return

	# 中心ぷよのピクセル位置を計算して設定
	var center_puyo_pixel_pos_x: float = float(puyo_status.x) * Global.PUYO_IMG_WIDTH
	var center_puyo_pixel_pos_y: float = float(puyo_status.y) * Global.PUYO_IMG_HEIGHT
	center_puyo_node.position = Vector2(center_puyo_pixel_pos_x, center_puyo_pixel_pos_y)

	# 可動ぷよのピクセル位置を計算して設定 (中心ぷよからの相対位置)
	var movable_pixel_offset_x: float = float(puyo_status.dx) * Global.PUYO_IMG_WIDTH
	var movable_pixel_offset_y: float = float(puyo_status.dy) * Global.PUYO_IMG_HEIGHT
	
	movable_puyo_node.position = Vector2(
		center_puyo_pixel_pos_x + movable_pixel_offset_x,
		center_puyo_pixel_pos_y + movable_pixel_offset_y
	)

4.6 Mainシーンへの組み込み(Stage、Player)

Mainシーンに戻りPlayerをインスタンス化して、Mainシーンから利用出来るようにしましょう。

  1. Playerのインスタンス化
    • Mainノードを選択し、子ノードを追加をクリックします。
    • Node2Dを選択し、名前をPlayerにします。
    • Playerノードを選択して、スクリプトをアタッチからres://scripts/game/Player.gdを選択します。
  2. Main.gdの作成
    • Mainノードを選択し、スクリプトをアタッチからres://scripts/game/Main.gdを選択します。
  3. 動作の確認のため、Main.gd の内容を以下に記述します。
# res://scripts/game/Main.gd
extends Node2D

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

# _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)
	
	# TODO:確認用
	player_node.create_new_puyo()    # 操作用のぷよを生成
	player_node.puyo_status.y = 1    # 盤面の見える位置を設定
	player_node.set_puyo_position()  # 描写

実行すると、ランダムな色のぷよが盤面に表示されることが確認できます。
確認が終わったら確認用のコードを削除しておきます。

	# TODO:確認用
	player_node.create_new_puyo()
	player_node.puyo_status.y = 1
	player_node.set_puyo_position()

まとめ

今回は基礎部分の作成まで完了しました。
次回は状態管理処理の実装から続きを書いていきます。

Discussion