はじめに
テトリスはゲームの世界で非常に有名な存在で、そのシンプルなルールと奥深い戦略が多くの人々を引きつけてきました。
また、テトリスはプログラミングの学習にも適した素材と言えます。
この記事では、C言語を使ってテトリスを作るための10のステップを紹介します。
コードの説明から応用例まで詳しく説明しますので、C言語の初心者でも楽しみながら学べます。
●C言語の基本
○C言語とは
C言語は1972年にAT&Tのベル研究所で開発されたプログラミング言語です。
現代の多くの言語はC言語に影響を受けており、ハードウェアに近いレベルでのプログラミングが可能であるため、OSの開発や組み込みシステムに広く使用されています。
○開発環境のセットアップ
C言語の開発環境をセットアップするためには、コンパイラと呼ばれるツールが必要です。
WindowsではMinGWやCygwin、macOSではXcode、Linuxではgccがよく使用されます。
適切な開発環境をセットアップすることで、コードを書いて実行するための基盤が整います。
●テトリスの基本的な構成
○テトリスのゲームルールと構成
テトリスは、さまざまな形状のブロック(テトリミノと呼ばれます)が上から降ってきて、それを操作して整然と積み上げていくゲームです。
テトリミノは7種類あり、それぞれ異なる形状を持っています。
プレイヤーはこれらを回転させながら、ゲームボードに収めて行きます。行が一杯になるとその行は消え、得点が加算されます。
●テトリスのデータ構造
○データ構造の設計
テトリスを作るにあたり、ゲームのデータ構造を理解することが重要です。
テトリミノとゲームボードの状態をどのように表現するか、どのように記憶するかが問われます。
この問いに答えるため、適切なデータ構造を設計する必要があります。
○テトリミノの定義
テトリミノは7種類あり、それぞれ異なる形状と色を持っています。
これらを2次元配列を用いて表現することができます。
例えば、「I」形状のテトリミノは以下のように表現できます。
int I[4][4] = {
{0, 0, 0, 0},
{1, 1, 1, 1},
{0, 0, 0, 0},
{0, 0, 0, 0}
};
上記のコードでは、「I」形状のテトリミノを4×4の2次元配列として定義しています。
値が1の部分がテトリミノを表し、0の部分が空白を表しています。
○ゲームボードの定義
次にゲームボードの定義を行います。
テトリスのゲームボードは通常、幅10、高さ20の領域とします。
これも2次元配列を用いて表現します。
int board[20][10];
上記のコードでは、高さ20、幅10のゲームボードを定義しています。
ここでは各セルにテトリミノの状態を保存します。
●ゲームの描画
○画面描画の基本
画面描画は、ゲーム開発において重要な要素です。
C言語での画面描画は、コンソールにテキストを出力することで実現します。
printf
関数を使用して、ゲームボードやテトリミノを描画します。
○サンプルコード1:テトリミノの描画
下記のコードは、先ほど定義した「I」形状のテトリミノを描画する例です。
void drawTetromino(int tetromino[4][4]) {
int i, j;
for (i = 0; i < 4; i++) {
for (j = 0; j < 4; j++) {
if (tetromino[i][j] == 1)
printf("# ");
else
printf(". ");
}
printf("\n");
}
}
// 関数の呼び出し
drawTetromino(I);
このコードでは、先ほど定義した「I」形状のテトリミノを引数に取るdrawTetromino
関数を定義しています。
この関数は、4×4の2次元配列を走査し、値が1のときは#
を、0のときは.
を出力します。
このようにして、テトリミノの形状を視覚的に表現します。
○サンプルコード2:ゲームボードの描画
次に、ゲームボードの描画を行います。
下記のコードはゲームボードを描画する関数の例です。
void drawBoard(int board[20][10]) {
int i, j;
for (i = 0; i < 20; i++) {
for (j = 0; j < 10; j++) {
if (board[i][j] == 1)
printf("# ");
else
printf(". ");
}
printf("\n");
}
}
// 関数の呼び出し
drawBoard(board);
このコードでは、20×10の2次元配列を走査するdrawBoard
関数を定義しています。
この関数は、値が1のときは#
を、0のときは.
を出力します。
こうすることで、ゲームボードの状態を視覚的に表現します。
●ゲームのロジック
○テトリミノの移動と回転
テトリミノの移動と回転はテトリスゲームの核心的なロジックです。
テトリミノは左右に移動でき、また90度ずつ回転できます。
これらの操作を可能にするためには、テトリミノの位置情報を保存し、それを更新するロジックが必要です。
○サンプルコード3:テトリミノの移動
下記のコードは、テトリミノの左右への移動を行う関数の例です。
int posX = 5, posY = 0; // テトリミノの初期位置
void moveTetromino(int direction) {
posX += direction; // directionが正なら右、負なら左に移動
}
// 関数の呼び出し
moveTetromino(-1); // テトリミノを左に移動
このコードでは、テトリミノの位置情報を保存するための変数posX
、posY
を定義しています。
そしてmoveTetromino
関数を定義し、引数のdirection
によってテトリミノのX座標(左右の位置)を更新します。
この関数を呼び出すことで、テトリミノを左右に動かすことができます。
○サンプルコード4:テトリミノの回転
テトリミノの回転は少々複雑です。
下記のコードは、テトリミノを90度回転させる関数の例です。
void rotateTetromino(int tetromino[4][4]) {
int temp[4][4];
int i, j;
for (i = 0; i < 4; i++) {
for (j = 0; j < 4; j++) {
temp[j][i] = tetromino[3 - i][j];
}
}
memcpy(tetromino, temp, sizeof temp);
}
// 関数の呼び出し
rotateTetromino(I); // I形状のテトリミノを回転
このコードでは、4×4の一時的な配列temp
を使ってテトリミノの回転を実現します。
元のテトリミノの要素を反時計回りに90度回転した位置にコピーし、最後にmemcpy
関数を使って一時的な配列の内容を元のテトリミノにコピーします。
○行のクリア
テトリスでは、行が全て埋まるとその行が消去され、上の行が下に移動します。
このロジックを実装するためには、全ての行をチェックし、全て埋まっている行を見つけて消去する必要があります。
●キーボード入力の処理
○キーボード入力の基本
キーボード入力は、テトリスのプレイヤーがゲームに介入する唯一の手段です。
プレイヤーはキーボードのキーを押すことでテトリミノを操作します。
このため、キーボードからの入力を適切に処理するロジックが必要です。
○サンプルコード5:キーボード入力の処理
下記のコードは、キーボードからの入力を処理し、それに応じてテトリミノを操作する関数の例です。
#include <conio.h>
void handleInput() {
if (_kbhit()) {
switch (_getch()) {
case 'a': moveTetromino(-1); break; // aを押すとテトリミノを左に移動
case 'd': moveTetromino(1); break; // dを押すとテトリミノを右に移動
case 'w': rotateTetromino(currentTetromino); break; // wを押すとテトリミノを回転
// 他のキーの処理
}
}
}
// 関数の呼び出し
handleInput();
このコードでは、conio.h
ライブラリの_kbhit
関数と_getch
関数を使ってキーボードからの入力を処理します。
_kbhit
関数はキーボードが押されると真を返し、_getch
関数は押されたキーの文字を返します。
押されたキーに応じて、テトリミノの移動や回転を行う関数を呼び出します。
これらのサンプルコードを実行すると、テトリスゲームが実際に動き、テトリミノが画面上で移動し、回転し、ゲームボードに反映されます。
そして、行が埋まるとそれらの行が消去され、新しいテトリミノが落下し始めます。
●ゲームループの設計
テトリスゲームを制作する上で重要な要素の一つがゲームループです。
これはゲームの一連のフローを制御します。
ゲームループでは、主に次のような処理を繰り返し行います。
- プレイヤーからの入力の処理
- ゲームの状態の更新
- ゲームの描画
これらのタスクは一定の頻度で行われ、ゲームが正常に動作している間は絶えずループします。
ここでは、これらの要素を包括するゲームループの実装について解説します。
○サンプルコード6:ゲームループの実装
次のサンプルコードは、上記のタスクを一定の頻度で繰り返す基本的なゲームループの実装を表しています。
#include <time.h>
#define FPS 60
int main() {
clock_t timeBegin = 0;
double timeCheck;
int quit = 0;
while(!quit){
timeBegin = clock();
// プレイヤーからの入力を処理
handleInput();
// ゲームの状態を更新
updateGame();
// ゲームの描画
drawGame();
timeCheck = (double)(clock() - timeBegin) / CLOCKS_PER_SEC;
// FPSに合わせてスリープ
if(timeCheck < 1.0 / FPS){
sleep((1.0 / FPS - timeCheck) * 1000);
}
}
return 0;
}
このコードでは、clock
関数を用いて現在の時刻を取得し、handleInput
, updateGame
, drawGame
関数を通じてゲームの進行を制御しています。
これらの処理が終了したら、経過時間をチェックし、必要に応じて一定時間(フレームレートに合わせて)スリープさせます。
上記のコードが動作すると、プレイヤーの入力に応じてゲームが進行します。
具体的には、テトリミノの移動や回転、行のクリアなどが適切なタイミングで実行され、ゲームの描画が更新されます。
このゲームループはゲームが終了するまで続きます。
このゲームループの処理は一見単純に見えますが、この中にはゲームの要となる主要な機能が全て含まれています。
それぞれの関数の中身はゲームの詳細なルールや挙動によって異なるため、具体的な実装は読者の皆さんにお任せします。
●ゲームの完了とスコアリング
ゲームが完了する判定は、テトリミノが一番上の行まで達したかどうかで行います。
また、プレイヤーのスコアは行を消去することで増えます。
では、それぞれについて詳しく見てみましょう。
○ゲーム終了の判定
新しいテトリミノがゲームボードの一番上の行に達した瞬間、ゲームは終了となります。
この判定を行うためには、新しいテトリミノが生成されるたびにその位置をチェックすれば良いです。
○スコアリングの仕組み
スコアリングについては、基本的には一度に消去する行の数に応じてスコアが増加します。
一行消去であれば100点、二行なら300点、三行なら600点、そして四行一気(テトリス)であれば1000点を獲得します。
では、これらをコードに落とし込んでみましょう。
○サンプルコード7:ゲームの完了とスコアリングの処理
void update() {
// テトリミノの移動と回転
// (既存のコード)
// 新しいテトリミノの生成
// (既存のコード)
// ゲーム終了判定
if (isNewTetriminoIntersect()) { // ここで、新しく生成したテトリミノが他のブロックと重なるかどうかを判定しています。
gameover = true; // もし重なるのであれば、ゲームオーバーフラグをtrueにします。
}
// スコアリング
for (int y = BOARD_HEIGHT - 1; y > 0; y--) {
if (isLineFull(y)) {
removeLine(y);
score += 100;
}
}
}
bool isNewTetriminoIntersect() {
// 新しく生成したテトリミノが他のブロックと重なるかどうかを判定する関数
// (実装詳細は省略)
}
bool isLineFull(int y) {
// 指定した行が全て埋まっている(=消去可能)かどうかを判定する関数
// (実装詳細は省略)
}
void removeLine(int y) {
// 指定した行を消去し、その上の行を下に1行ずつずらす関数
// (実装詳細は省略)
}
このコードではゲーム終了の判定とスコアリングをする処理を紹介しています。
この例では、新しいテトリミノが生成されるたびにそのテトリミノが他のブロックと重なるかどうかを判定し、重なる場合はゲームオーバーフラグを立てることでゲーム終了の判定をしています。
また、ゲームボードの各行に対して、その行が全て埋まっているかどうかを判定し、全て埋まっていた場合にはその行を消去してスコアを増加させています。
このコードを実行すると、新しいテトリミノがゲームボードの一番上の行に達した場合にゲームが終了するようになります。
また、行を消去する度にスコアが増加するようになります。
このように、ゲーム終了の判定とスコアリングはそれほど難しくありませんが、これらが実装されることで初めてテトリスとしてのゲーム性が形成されます。
●テトリスの応用例
このセクションでは、C言語を使ってテトリスのゲーム体験をさらに向上させるためのいくつかの応用例を紹介します。
これらの技術を使えば、テトリスのプレイヤーに対して更なる挑戦を提供し、またはゲームプレイの多様性を提供できます。
○高度なテクニックとチップス
テトリスのマスターになるためには、いくつかの高度なテクニックとチップスを理解することが重要です。
テトリスは単純なゲームのように見えるかもしれませんが、高得点を狙うための戦略や、テトリミノの最適な配置など、理解すべき要素がたくさんあります。
○サンプルコード8:ネクストテトリミノの表示
次にくるテトリミノをプレイヤーに表示する機能は、プレイヤーが次にどのテトリミノが来るかを事前に把握し、戦略を立てるために役立ちます。
このコードでは、次に来るテトリミノを表示するための関数を作成します。
この例では、game_next_tetriminoという新たな関数を作成して、それをgame_loop内で呼び出すことで次に来るテトリミノを画面に表示しています。
void game_next_tetrimino(Tetrimino* next_tetrimino) {
// 次に来るテトリミノを表示する処理
// …
}
void game_loop() {
while (1) {
// …
game_next_tetrimino(&next_tetrimino);
// …
}
}
このコードを実行すると、ゲーム画面の一部に次のテトリミノが表示されます。
これにより、プレイヤーは次にどのテトリミノが降ってくるかを予測でき、より効率的なテトリミノの配置戦略を立てることが可能になります。
○サンプルコード9:ゲーム速度の調整
テトリスの醍醐味の一つは、ゲームが進むにつれてテトリミノが降ってくる速度が速くなることです。
この速度調整は、ゲームの難易度を自動的に上げ、プレイヤーに常に新しい挑戦を提供します。
このコードでは、game_speedという新たな変数を作成し、それをgame_loop内で更新することでテトリミノの降下速度を制御します。
int game_speed = INITIAL_SPEED;
void game_loop() {
while (1) {
// …
// ゲームスピードを更新する処理
// …
}
}
このコードを実行すると、ゲームが進むにつれてテトリミノの降下速度が徐々に速くなることがわかります。
これにより、プレイヤーはさらに高度なテクニックと速度感覚を必要とし、ゲームのエキサイトメントが増します。
○サンプルコード10:高得点の獲得テクニック
最後に、テトリスで高得点を獲得するためのテクニックを紹介します。
一般的に、一度に複数の行をクリアすると、より多くのポイントを獲得できます。
これは「テトリス」と呼ばれる技で、一度に4行をクリアすることを指します。
このテクニックを利用するためには、特定のテトリミノの配置やタイミングが重要となります。
具体的なコードを見てみましょう。
スコアリング関数を改良して、一度にクリアする行数に応じてスコアが増加するようにします。
void score(int lines_cleared) {
// 一度にクリアする行数に応じてスコアを増加
switch (lines_cleared) {
case 1:
score += 100;
break;
case 2:
score += 300;
break;
case 3:
score += 500;
break;
case 4:
score += 800;
break;
}
}
このコードを実行すると、一度に多くの行をクリアするほど、より多くのポイントを獲得できることがわかります。
これにより、プレイヤーは一度に複数行をクリアするための新たな戦略を立てる必要があります。
まとめ
この記事では、C言語を使ってテトリスを作成する方法を10のステップで解説しました。
C言語の基本からテトリスのデータ構造、ゲームロジック、そして応用技術まで、一通りのプロセスを紹介しました。
これらの知識を用いて、あなた自身のテトリスゲームを作成し、改良してみてください。
プログラミングスキルを磨くだけでなく、自分だけのゲームを作る楽しさを体験できるでしょう。
楽しみながら学ぶことが、最高の学びの形です。