プログラミングの学習において、ゲーム開発は非常に優れたアウトプットの場となります。

特に、世界的に有名な「テトリス」をC言語で作成することは、アルゴリズムやデータ構造の基礎を深く理解するために最適です。

この記事では、C言語を使ってコンソール上で動作するテトリスを開発するための手順を詳しく解説します。

プログラムの全体像から、ブロックの移動や回転、衝突判定といった重要なロジックまで、ステップバイステップで学んでいきましょう。

初心者の方でも理解できるように、一つひとつの工程を丁寧に紐解いていきます。

C言語でのゲーム開発とテトリスの魅力

C言語は、コンピュータのメモリ操作や処理の最適化を直接制御できるパワフルな言語です。

現代では高機能なゲームエンジンが多く存在しますが、あえてC言語でゼロからゲームを作ることで、コンピュータがどのように命令を処理しているかを肌で感じることができます。

テトリスというゲームは、2次元配列の操作、ループ処理、条件分岐といった、プログラミングの本質的な要素が凝縮された題材です。

このプロジェクトを完成させることで、ポインタや配列の扱いに関するスキルが飛躍的に向上するはずです。

また、自分で作ったゲームが実際に動く喜びは、プログラミング学習の大きなモチベーションに繋がります。

テトリスの基本構造を設計する

実装に入る前に、テトリスのシステムをどのようにデータとして表現するかを設計しましょう。

フィールドの表現方法(2次元配列)

テトリスの舞台となる「フィールド」は、一般的に横10マス、縦20マスの矩形エリアです。

C言語でこれを表現する場合、int型の2次元配列を使用するのが最もシンプルで効率的です。

例えば、field[20][10]という配列を用意し、何も無い場所を「0」、ブロックが固定されている場所を「1」、壁を「2」といった数値で管理します。

このように数値で状態を保持することで、衝突判定や描画処理が格段にスムーズになります。

テトリミノ(ブロック)のデータ構造

落下するブロック(テトリミノ)は、全部で7つの種類が存在します。

それぞれのテトリミノは4つの正方形で構成されており、これを4×4の小規模な2次元配列として定義するのが一般的です。

例えば、T字型のブロックであれば、4×4の配列の中に特定の位置に「1」を配置することで表現できます。

このテトリミノの形状データを構造体(struct)にまとめ、現在位置の座標(x, y)や回転の状態を持たせると管理がしやすくなります。

実装に必要なアルゴリズム

テトリスの核となるのは、ブロックの動きを制御するロジックです。

ブロックの移動と衝突判定

プレイヤーが矢印キーを押した際、ブロックは左右または下に移動します。

このとき、移動先のマスがすでに埋まっていないか、あるいはフィールドの境界を越えていないかを確認しなければなりません。

これが「衝突判定」と呼ばれる処理です。

具体的には、移動後の座標において、テトリミノの「1」の部分がフィールド配列の「0」以外の場所と重ならないかをループでチェックします。

もし重なるようであれば移動をキャンセルし、下方向への移動で衝突した場合は、そのブロックをフィールドに固定する処理へ移行します。

ブロックの回転処理

テトリスの醍醐味であるブロックの回転は、数学的な行列の回転として処理できます。

4×4の配列内で要素を90度入れ替える計算式を適用することで、回転を実現します。

ただし、壁際で回転させた際にフィールド外にはみ出してしまう可能性があるため、回転後も衝突判定を行うことが不可欠です。

もし回転後の位置が不正であれば、回転を無効にする、あるいは「壁蹴り」と呼ばれる補正処理を入れることで、自然な操作感を実現できます。

行の消去判定とスコア計算

ブロックが固定された後、フィールドの横一行がすべて埋まっているかを確認します。

2次元配列の特定の行について、すべての要素が「0」以外であれば、その行を「消去」の対象とします。

行を消去した後は、その上にあるすべての行を一段下にずらす処理が必要です。

この際、一度に多くの行を消すほど高いスコアが入るような仕組みを実装すると、ゲーム性が高まります。

プログラミングの実装手順

それでは、具体的なコーディングの準備を進めていきましょう。

ライブラリの準備と初期設定

標準的なC言語の機能(stdio.h, stdlib.h, time.h)に加えて、リアルタイムなキー入力を取得するための工夫が必要です。

Windows環境であればconio.hkbhit()関数を使用するのが手軽ですが、Linux環境であればncursesライブラリの使用が推奨されます。

今回は基本的なロジックに集中するため、標準的な入出力に基づいた設計を考えます。

メインループと入力処理

ゲームの基本は「メインループ」と呼ばれる無限ループの中で進行します。

このループ内では、1:入力の受付、2:状態の更新(落下や移動)、3:描画、4:ウェイト処理、というサイクルを高速に繰り返します。

一定時間が経過するごとにブロックを自動で一段落下させる仕組みをタイマーやカウンタで制御するのがポイントです。

サンプルコード:コンソール版テトリス

以下に、C言語でテトリスの骨組みを実装するための簡略化したサンプルコードを示します。

このコードは概念を理解するためのものであり、Windows環境(VC++やMinGW)での動作を想定しています。

C言語
#include <stdio.h>
#include <stdlib.h>
#include <conio.h>
#include <time.h>
#include <windows.h>

#define FIELD_WIDTH 10
#define FIELD_HEIGHT 20
#define BLOCK_SIZE 4

// フィールドとブロックのデータ
int field[FIELD_HEIGHT][FIELD_WIDTH] = {0};
int current_x = 3, current_y = 0;
int block[BLOCK_SIZE][BLOCK_SIZE] = {
    {0, 1, 0, 0},
    {0, 1, 0, 0},
    {0, 1, 0, 0},
    {0, 1, 0, 0}
};

// 画面描画関数
void draw_field() {
    system("cls"); // 画面をクリア
    for (int y = 0; y < FIELD_HEIGHT; y++) {
        for (int x = 0; x < FIELD_WIDTH; x++) {
            // 現在落下中のブロックを表示
            int is_block = 0;
            if (y >= current_y && y < current_y + BLOCK_SIZE && 
                x >= current_x && x < current_x + BLOCK_SIZE) {
                if (block[y - current_y][x - current_x]) {
                    printf("■");
                    is_block = 1;
                }
            }
            
            if (!is_block) {
                if (field[y][x] == 0) printf("・");
                else printf("□");
            }
        }
        printf("\n");
    }
}

// 衝突判定関数
int check_collision(int next_x, int next_y) {
    for (int y = 0; y < BLOCK_SIZE; y++) {
        for (int x = 0; x < BLOCK_SIZE; x++) {
            if (block[y][x]) {
                int target_x = next_x + x;
                int target_y = next_y + y;
                
                if (target_x < 0 || target_x >= FIELD_WIDTH || target_y >= FIELD_HEIGHT) return 1;
                if (field[target_y][target_x]) return 1;
            }
        }
    }
    return 0;
}

int main() {
    srand((unsigned int)time(NULL));
    
    while (1) {
        if (_kbhit()) {
            char key = _getch();
            if (key == 'a' && !check_collision(current_x - 1, current_y)) current_x--;
            if (key == 'd' && !check_collision(current_x + 1, current_y)) current_x++;
            if (key == 's' && !check_collision(current_x, current_y + 1)) current_y++;
        }
        
        // 簡易的な重力処理
        static int count = 0;
        if (count++ > 10) {
            if (!check_collision(current_x, current_y + 1)) {
                current_y++;
            } else {
                // フィールドに固定
                for (int y = 0; y < BLOCK_SIZE; y++) {
                    for (int x = 0; x < BLOCK_SIZE; x++) {
                        if (block[y][x]) field[current_y + y][current_x + x] = 1;
                    }
                }
                current_y = 0;
                current_x = 3;
            }
            count = 0;
        }
        
        draw_field();
        Sleep(50);
    }
    return 0;
}

上記のプログラムを実行すると、コンソール上に「・」のフィールドが表示され、棒状のブロックが落下してくる様子が確認できます。

実行結果
・ ・ ・ ■ ・ ・ ・ ・ ・ ・ 
・ ・ ・ ■ ・ ・ ・ ・ ・ ・ 
・ ・ ・ ■ ・ ・ ・ ・ ・ ・ 
・ ・ ・ ■ ・ ・ ・ ・ ・ ・ 
・ ・ ・ ・ ・ ・ ・ ・ ・ ・ 
(以下フィールドが続く)

実装のポイントとさらなる拡張

基本的な動きが完成したら、ゲームをより完成度の高いものへとブラッシュアップしていきましょう。

まず、「7種類のテトリミノ」をすべて実装することが次のステップです。

各テトリミノの初期形状を3次元配列などで保持しておき、出現時にランダムで選択するようにします。

次に、回転機能を追加します。

回転は配列のインデックスを new_y = x, new_x = 3 - y のように入れ替えることで実現可能です。

また、コンソール画面のチラつきを抑えるために、画面全体をクリアするのではなく、変更があった場所だけを上書きするテクニックや、ダブルバッファリングのような考え方を取り入れると、より滑らかな描画になります。

さらに、次に落ちてくるブロックを表示する「Next表示」や、スコア、レベル、ゲームオーバーの判定を追加すれば、立派なゲーム作品として完成します。

まとめ

C言語でのテトリス作成は、プログラミングの基礎知識をフル活用する非常にエキサイティングな挑戦です。

2次元配列によるフィールド管理、構造体を用いたデータのカプセル化、そしてループと条件分岐による動的な状態更新など、学べる要素は多岐にわたります。

最初はシンプルな移動と描画から始め、徐々に回転や消去といった複雑な機能を追加していくのが、挫折せずに完成させるコツです。

この記事で紹介した基本ロジックをベースに、ぜひあなただけのオリジナルテトリスを完成させてください。

コードを一行ずつ書き進めるごとに、C言語への理解が深まっていくことを実感できるはずです。