
AI Assisted
この記事は筆者の実装経験をもとに実装コードをベースで執筆し、AIによる校閲・推敲を経て公開しています。
3日目で、ブロック(テトリミノ)を動かせるようになりました。 しかし、今のままではブロックは壁をすり抜け、床を突き破り、奈落の底へ落ちていってしまいます。これではゲームになりません。
今日のテーマは**「衝突判定(Collision)」と「固定(Lock)」**です。 ブロックに「壁には入れない」「床についたら止まる」というルールを与え、テトリスらしく積み上げられるようにします。
今日のゴールはこれです!
Godotには CollisionShape や RigidBody といった便利な物理演算機能があるようで、マリオのようなアクションゲームならそれを使うのが正解らしい。
しかし、テトリスのような「マス目(グリッド)」で管理されるパズルゲームでは、あえて物理エンジンを使いません。 物理エンジンを使うと、ブロックが微妙に斜めになったり、隙間に挟まってガタガタ震えたりして、ピシッとマス目に収まらないからです。
代わりに、以下のシンプルなロジック(先読み法)を使います。
これを実装するために、3つのスクリプトを連携させます。
まず、盤面データを持っている board.gd に、「ここ空いてますか?」と聞かれたら答える窓口を作ります。
# board.gd
# 指定座標が空いているか判定(衝突判定用)
func is_cell_vacant(coords: Vector2i) -> bool:
var x := coords.x
var y := coords.y
# 盤面外(左右の壁、床)は false
if x < 0 or x >= COLS or y >= ROWS:
return false
# 画面上(出現用)は true
if y < 0:
return true
# 既にブロックがあれば false
if grid[y][x] != Cell.EMPTY:
return false
return true
# 指定座標にブロックを固定
func add_block(coords: Vector2i, atlas_coords: Vector2i) -> void:
var x := coords.x
var y := coords.y
# 盤面内のみ処理
if x >= 0 and x < COLS and y >= 0 and y < ROWS:
grid[y][x] = Cell.BLOCK
set_cell(coords, 0, atlas_coords)
これで、盤面は「判定」と「書き込み」ができるようになりました。
次に、ブロック piece.gd が勝手に動かないように、移動前に確認する処理を入れます。
また、下に動けなくなった時に「固定(Lock)」する重要な処理もここに入ります。
# piece.gd
# シグナル定義
signal locked
var board: Node # Mainからセットされる盤面の参照
# 移動と衝突判定
func move_and_collide(dx: int, dy: int) -> void:
var new_position := Vector2(
position.x + dx * TILE_SIZE,
position.y + dy * TILE_SIZE
)
if is_position_valid(new_position, current_cells):
position = new_position
else:
# 下移動で衝突した場合のみロック
if dy > 0:
lock()
# ブロックを盤面に固定
func lock() -> void:
var grid_x := int(position.x / TILE_SIZE)
var grid_y := int(position.y / TILE_SIZE)
var atlas_coords := Vector2i(0, 0)
for cell: Vector2i in current_cells:
# グローバル位置をグリッド座標に変換して登録
var board_coords := Vector2i(grid_x + cell.x, grid_y + cell.y)
board.add_block(board_coords, atlas_coords)
locked.emit() # 固定完了シグナル!
queue_free() # 役目を終えた自分を消す
最後に、親である main.gd で、Piece と Board を引き合わせます。
Piece が生成された瞬間に「君の担当する盤面はこれだよ」と教えてあげる必要があります。
# main.gd
func spawn_piece() -> void:
var piece = piece_scene.instantiate()
piece.position = Vector2(4 * 32, 0)
# ★重要修正:盤面(TileMapLayer)の子として追加する
# Mainの子にすると座標がズレるため注意!
$TileMapLayer.add_child(piece)
# 生まれたてのPieceに盤面を教える
piece.board = $TileMapLayer
# Pieceからの「固定完了」シグナルを待つ
piece.locked.connect(_on_piece_locked)
func _on_piece_locked() -> void:
# 次のブロックを出現させる
spawn_piece()
実装中に「ブロックが壁にめり込む」「右端まで行けない」という現象が発生しました。 原因は、「見た目の壁」と「プログラム上の壁」のズレでした。
プログラム上、テトリスのフィールドは x=0 〜 x=9 です。
つまり、**左の壁は x=-1、右の壁は x=10** にないといけません。
エディタのTileMap描画モードでカーソルを合わせ、画面左下の座標表示を確認しながら、正しい位置 (-1, 0) と (10, 0) に壁を描き直すことで解決しました。
こうした**「論理座標と見た目の不一致」**はゲーム開発でよくある罠なので注意が必要です。

今回初めて signal という機能を使いました。
これは「何かが起きたよ!」と叫ぶ機能です。
locked シグナル発信)」このようにシグナルを使うと、Piece は「次に何をするか(Mainの事情)」を知らなくて済むため、プログラムがごちゃごちゃになりません。Godot開発の最重要テクニックの一つです。
ついに積み上げることができるようになりました。 次回は、テトリスの醍醐味である**「ラインが揃ったら消える」**処理を実装します。いよいよスコアが入るようになります!
スポーツ×ITの会社でバックエンドエンジニア兼マネージャーとして勤務。インテル関連の情報を中心に発信しています。
最終更新: 2026年1月15日
© 2025 nero15.dev. All rights reserved.