
AI Assisted
この記事は筆者の実装経験をもとに実装コードをベースで執筆し、AIによる校閲・推敲を経て公開しています。
4日目で、ブロックを積み上げることができるようになりました。 しかし、このままでは画面がいっぱいになって終わりです。テトリスと言えば、横一列が揃った瞬間にブロックが消える あの爽快感が欠かせません。
今日のゴールはこれです!
盤面を管理している board.gd に、「ライン消去機能」 を追加します。
この処理は「判定」「削除」「シフト(移動)」「再描画」という複数の工程が必要になるため、機能を小さな関数に分割して実装しました。
実装したコード:
以下のメソッド群を board.gd に追加します。
# board.gd
# 揃った行を消去し、消した行数を返す(メイン処理)
func clear_full_rows() -> int:
var cleared_count: int = 0
var y: int = ROWS - 1
# 下から順にチェックしていきます
while y >= 0:
if _is_row_full(y):
_remove_row(y)
cleared_count += 1
# 【重要】行を削除すると上の行が落ちてくるため、
# yを減らさずに、同じ場所をもう一度チェックします
else:
y -= 1
# もし1行でも消えていたら、見た目を更新します
if cleared_count > 0:
_redraw_board()
return cleared_count
# 指定された行(y)がすべて埋まっているか判定するヘルパー関数
func _is_row_full(y: int) -> bool:
for x in range(COLS):
if grid[y][x] == Cell.EMPTY:
return false
return true
# 指定行を削除し、それより上の行を全て1段下へずらす関数
func _remove_row(target_y: int) -> void:
# target_y から一番上(0)まで、上から順にコピーしてきます
for y in range(target_y, 0, -1):
# ★重要:duplicate()でコピーを作る(後述)
grid[y] = grid[y - 1].duplicate()
# 一番上の行(0行目)は空っぽの行で埋めます
var empty_row: Array = []
for x in range(COLS):
empty_row.append(Cell.EMPTY)
grid[0] = empty_row
# gridの内容に基づいてTileMapLayerを再描画する関数
func _redraw_board() -> void:
# 一旦すべてのタイルを消去
clear()
# データ(grid)を見て、ブロックがある場所だけタイルを置き直す
for y in range(ROWS):
for x in range(COLS):
if grid[y][x] == Cell.BLOCK:
# ここでは atlas_coords(0,0) の白いブロックを使用
set_cell(Vector2i(x, y), 0, Vector2i(0, 0))
上記のコードにある grid[y] = grid[y - 1].duplicate() という部分。
「なぜわざわざ .duplicate() をつけるの?」と思うかもしれません。
実は Godot (GDScript) の配列は 「参照渡し」 です。
もし .duplicate() をつけずに grid[y] = grid[y - 1] と書くと、データのコピーではなく**「データの置き場所(住所)」**だけが渡されてしまいます。
すると、「19行目と18行目が実体として同じメモリを見る」ことになり、後で18行目を変更した瞬間に19行目も勝手に書き換わってしまいます。
配列をコピーするときは、必ず .duplicate() を使う。これはバグを防ぐための鉄則です。
機能はできましたが、いつ呼び出すべきでしょうか? それは 「ブロックが固定された直後」 です。
piece.gd の lock() 関数の中に数行追加するだけでOKです。
# piece.gd の 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)
# Boardにブロック情報を書き込む
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)
# ★★★ 追加ここから ★★★
# 固定した直後に、ラインが揃ったかチェックして消す!
board.clear_full_rows()
# ★★★ 追加ここまで ★★★
locked.emit()
queue_free()
実装して実行してみると、ラインは消えましたが...周りの壁まで一緒に消えてしまいました。

原因は _redraw_board() 内の clear() 関数です。
これが TileMapLayer上のすべてのタイル(手描きした壁含む) を消去してしまうため、データ(grid)に含まれていない壁が復元されなかったのです。
そこで、「壁を描く専用の関数」を作成し、再描画のたびに呼び出すように修正しました。
修正後の board.gd(追加・変更分のみ):
# board.gd
# ★追加:壁と床をプログラムで描画する関数
func _draw_walls() -> void:
var atlas_coords := Vector2i(0, 0)
# 左右の壁を描く (x = -1 と x = COLS)
for y in range(ROWS + 1): # +1 は床の高さまで描くため
set_cell(Vector2i(-1, y), 0, atlas_coords)
set_cell(Vector2i(COLS, y), 0, atlas_coords)
# 床を描く (y = ROWS)
for x in range(-1, COLS + 1):
set_cell(Vector2i(x, ROWS), 0, atlas_coords)
# ★変更:_redraw_board の中で壁も描くように修正
func _redraw_board() -> void:
clear() # 一旦すべて消す
_draw_walls() # ★ここで壁と床を描き直す関数を呼ぶ!
# その後、ブロックデータを描画(既存の処理)
for y in range(ROWS):
for x in range(COLS):
if grid[y][x] == Cell.BLOCK:
set_cell(Vector2i(x, y), 0, Vector2i(0, 0))
# ★変更:ゲーム開始時にも壁を描く
func _ready() -> void:
_draw_walls()
これで、エディタ上で手描きした壁は不要になります(コードが描いてくれるため)。 実行してみると、ライン消去後も壁が維持されるようになりました。

さっそく実行してみましょう。 わざと隙間なく横一列を埋めてみます。最後の1ピースがハマった瞬間...
シュッ!
揃ったラインが消えて、上のブロックが落ちてきました!
これで永遠に遊び続けられる「テトリス」の基本ループが完成しました。
ゲームのループはできましたが、まだ終わりがありません。上まで積み上がってもエラーが出るだけです。 次回は 「ゲームオーバー判定」 と、モチベーションを高める 「スコア機能」 を実装します!
スポーツ×ITの会社でバックエンドエンジニア兼マネージャーとして勤務。インテル関連の情報を中心に発信しています。
最終更新: 2026年1月16日
© 2025 nero15.dev. All rights reserved.
