ぷよぷよの作成 - C言語とelで様々なゲームを作ろう
目次
- C言語とelで様々なゲームを作ろう
- Visual C++ .NET での設定
- テンプレートファイルの解説
- シューティングゲームの作成(チュートリアル)
- パックマン的ゲームの作成(チュートリアル)
- ブロック崩しの作成
- 15パズルの作成
- 横スクロールジャンピングゲームの作成
- オセロの作成
- 神経衰弱の作成
- 7ならべの作成
- テトリスの作成
- ぷよぷよの作成
ソース・プロジェクトファイル
ソースファイルプロジェクトファイル(Visual C++ 6.0)
ゲームの説明
いわゆるぷよぷよです。カーソルキーの左右でぷよを動かし、スペースキーで回転、カーソルキーの下で落下します。同色のぷよを4つ以上繋げると消すことができます。だんだん落下速度が速くなって難しくなります。
プログラムの解説
グローバル変数
DDOBJ puyo;
各色のぷよを表わすスプライトオブジェクトです。ひとつのぷよは 32 × 32 ピクセルで、それが5色と、ぷよを消したときに一時的に表示される灰色のぷよが1つ並んだ画像です。
int field[15][8];
画面の状態を表わす配列です。中に入る値として、-1が空白、0~4が緑・黄・青・赤・紫のぷよ、5が壁を表わします。配列のどの値が画面のどこを表わすかを以下に示します。壁は赤色の部分で、画面上には表示されません。
int cmb[15][8];
ぷよの結合チェックに使う配列です。
bool elist[30];
ぷよが結合されているかを一時的に保存しておくリストです。ウインドウ生成関数
puyo = elDraw::LoadObject("puyo.bmp");
ぷよの画像をスプライトオブジェクトに読み込んでいる部分です。
for (int y = 0; y < 15; y++) {
for (int x = 0; x < 8; x++) {
if (x == 0 || x == 7 || y == 14) field[y][x] = 5;
else field[y][x] = -1;
}
}
画面データ配列 field を初期化しています。画面の左右端と下端は見えない壁 (5) とし、その他は空白 (-1) とします。ぷよ結合チェック関数 CheckCombine
int CheckCombine(int x, int y, int pno, int cno)
画面上の (x, y) の位置に pno (0 ~ 4) の種類のぷよが存在するか調べ、さらにそのぷよに繋がったぷよを調べていき、繋がったぷよの合計を戻値として返す関数です。cno は結合番号で、既に結合チェック済みのぷよを再度調べないために使用し、ひとつ調べるたびにインクリメントされます。
if (field[y][x] != pno || cmb[y][x] != 0) return 0;
まず、調べようとする位置のぷよの種類が違う、またはすでにチェック済みならば結合数 0 を戻値として返します。結合チェック配列 cmb には引数として渡された結合番号 cno が入ります。0 ならば未チェックです。
int ret = 1;
ret に戻値として返すためのぷよ結合数が入ります。
cmb[y][x] = cno;
現在の結合番号を結合チェック配列にセットします。
if (y > 0) ret += CheckCombine(x, y - 1, pno, cno);
ret += CheckCombine(x + 1, y, pno, cno);
ret += CheckCombine(x, y + 1, pno, cno);
ret += CheckCombine(x - 1, y, pno, cno);
ぷよの繋がりを調べるために、再帰関数として自分自身を呼び出します。現在の座標の上下左右の座標をセットして呼び出すことによって、再帰的に全ての繋がりを調べることができます。上部以外は壁があるのでそこでチェックが止まります。メイン画面 変数
static int pnext1, pnext2;
次に出てくるぷよ(ネクストぷよ)の種類です。5種類のぷよがあるので値は0~4を取り、一度に2つのぷよが出るので変数が2つあります。
static int pno1, pno2;
現在画面上に出ているぷよの種類です。5種類のぷよがあるので値は0~4を取り、一度に2つのぷよが出るので変数が2つあります。
static int px1, py1, px2, py2;
現在画面上に出ているぷよの座標です。これも2つ分あります。
static int keyLeft = FREE_KEY;
static int keyRight = FREE_KEY;
static int keyDown = FREE_KEY;
static int keySpace = FREE_KEY;
カーソルキー左・右・下・スペースキーの状態を保持する変数です。
static bool overFlag = false;
ゲームオーバーフラグです。これが true になるとゲームオーバーです。
static int score = 0;
得点です。
static DWORD rpt = 0;
ぷよを動かすときに連続で動くと速すぎるので、どのくらいの速さで動くかを表わすキーリピート時間を保持します。
static DWORD downTime = 1000;
ぷよが強制的に一段落下する時間をミリ秒で表わします。ゲームが進むにつれてこの時間が短くなります。
static DWORD downCount;
ぷよが強制的に落下する時間を計測するカウンタです。
static enum {
NEXT,
NORMAL,
FALL,
ERASE1,
ERASE2,
} status = NEXT;
現在の状態を表わす列挙型変数です。それぞれの値は次のような意味です。- NEXT : ネクストぷよが出現する瞬間です。ゲーム開始時や、ぷよが一番下まで落ちて固まったときにこの状態になります。
- NORMAL : 通常のぷよが自由に動く状態です。
- FALL : ぷよが落下中の状態です。ひとつのぷよが着地し、もうひとつのぷよが落下する時や、連結ぷよが消えて、その上のぷよが落下するときにこの状態になります。
- ERASE1 : ぷよが全部落下し、結合チェックをするときにこの状態になります。
- ERASE2 : ぷよが消えている最中は、灰色の消しぷよを表示するのですが、そのときにこの状態になります。
メイン画面 初期処理
if (elChangeScreen()) {pnext1 = rand() % 5; pnext2 = rand() % 5;}
elChangeScreen 関数はゲーム開始時に一度だけ true になります。ここでは、ゲーム開始時にネクストぷよを2つランダムで選んでいます。
DWORD nowTime = timeGetTime();
メイン処理を行なう前に、現在時刻を nowTime に代入しておきます。メイン画面 ネクストぷよ出現
メイン処理では、状態変数 status の値に応じてそれぞれの処理を行ないます。
px1 = 3; py1 = 1; px2 = 3; py2 = 0;
pno1 = pnext1; pno2 = pnext2;
pnext1 = rand() % 5; pnext2 = rand() % 5;
ぷよの各変数を初期化している部分です。ぷよの座標(2つ)を画面最上部の中央にセットし、ぷよの種類をネクストぷよの値にし、ネクストぷよをランダムで2つ選びます。
downCount = timeGetTime();
強制落下用のカウンタを現在時刻にします。
if (downTime > 100) downTime -= 10;
else downTime--;
if (downTime < 10) downTime = 10;
ネクストぷよが出るたびに、強制落下時間を減らします。強制落下時間が 100 ミリ秒より多ければ 10 ミリ秒ずつ減らし、100 ミリ秒以下なら 1 ミリ秒ずつ減らしています。さらに強制落下時間が 10 ミリ秒未満にならないようにします。
status = NORMAL;
最後に status を通常の状態にします。メイン画面 通常処理
kx1 = px1; ky1 = py1;
kx2 = px2; ky2 = py2;
まず仮の座標にぷよの座標を入れておきます。
elSystem::GetKey(VK_LEFT, &keyLeft);
elSystem::GetKey(VK_RIGHT, &keyRight);
elSystem::GetKey(VK_DOWN, &keyDown);
elSystem::GetKey(VK_SPACE, &keySpace);
カーソルキーとスペースキーの値を読み取り変数に格納します。
flag = false;
カーソルキーの下を押したときや、時間による強制落下などで、ぷよが落下中なら true になるフラグです。
if (keySpace == PUSH_KEY) {
if (kx2 > kx1) {kx2 = kx1; ky2 = ky1 + 1;}
else if (kx2 < kx1) {kx2 = kx1; ky2 = ky1 - 1;}
else if (ky2 > ky1) {ky2 = ky1; kx2 = kx1 - 1;}
else {ky2 = ky1; kx2 = kx1 + 1;}
スペースキーが押されている場合は、仮のぷよ座標を回転します。GetKey 関数はキーが押された瞬間に PUSH_KEY、押し続けている間は HOLD_KEY の値を取ります。つまり、続けて回転するためには一度キーを離さないといけません。
} else if (keyDown == PUSH_KEY || keyDown == HOLD_KEY && nowTime - rpt > 10) {
ky1++; ky2++; flag = true; rpt = nowTime;
カーソルキーの下が押されたときは、ぷよの y 座標を +1 し、落下フラグをセットします。押した瞬間 (PUSH_KEY) はすぐに落下、押し続けているなら (HOLD_KEY) キーリピート時間 10 ミリ秒を過ぎたときだけ落下します。
} else if (keyLeft == PUSH_KEY || keyLeft == HOLD_KEY && nowTime - rpt > 150) {
kx1--; kx2--; rpt = nowTime;
} else if (keyRight == PUSH_KEY || keyRight == HOLD_KEY && nowTime - rpt > 150) {
kx1++; kx2++; rpt = nowTime;
カーソルキーの左右を押したときも同じくぷよの座標を左右に動かします。キーリピート時間は 150 ミリ秒にします。
} else if (nowTime - downCount > downTime) {
ky1++; ky2++; flag = true; downCount = nowTime;
}
どのキーも押されていないとき、強制落下時間を過ぎていればぷよの y 座標を +1 し、落下フラグをセットします。
if (field[ky1][kx1] == -1 && field[ky2][kx2] == -1) {
px1 = kx1; py1 = ky1;
px2 = kx2; py2 = ky2;
もしぷよを動かした先が空ならば、実際の座標に仮の座標を入れます。
} else if (flag) {
field[py1][px1] = pno1;
field[py2][px2] = pno2;
status = FALL;
}
もしぷよを動かしたさきが空でなく、つまり壁か他のぷよに当たったときで、なおかつ落下中ならば、現在のぷよを画面配列に入れ、状態変数をぷよ落下中にします。メイン画面 ぷよ落下
Sleep(50);
毎回処理を行なうと一瞬で落下してしまうので、まずここで 50 ミリ秒時間待ちをします。
flag = false;
ぷよがひとつでも落下中ならば、このフラグが true になります。
for (y = 12; y >= 0; y--) {
for (x = 1; x < 7; x++) {
if (field[y][x] != -1 && field[y + 1][x] == -1) {
field[y + 1][x] = field[y][x];
field[y][x] = -1;
flag = true;
}
}
}
画面の一番下から上へ順次調べて行き、現在調べている座標にぷよがあり、さらにそのひとつ下が空間ならば、落下処理を行ないます。ぷよをひとつ下に移動し、落下中フラグをセットします。
if (flag == false) status = ERASE1;
ぷよがひとつも落下中でなければ、状態をぷよ消しチェックにします。メイン画面 ぷよ消しチェック
flag = false;
ひとつでも連結ぷよ(4つ以上繋がったぷよ)があればこのフラグが true になります。
for (y = 0; y < 15; y++) for (x = 0; x < 8; x++) cmb[y][x] = 0;
for (i = 0; i < 30; i++) elist[i] = false;
結合チェック配列と結合リストを全てクリアしておきます。
for (y = 13, i = 0; y >= 0; y--) {
for (x = 1; x < 7; x++) {
画面の一番下から上へ、左から右へ順次結合チェックして行きます。
if (cmb[y][x] == 0 && field[y][x] != -1) {
まだ結合チェックを行なっていなくて、なおかつそこが空間でなければ以下の処理を行ないます。
i++;
i は結合番号です。チェック配列 cmb にセットされる値です。
int ret = CheckCombine(x, y, field[y][x], i);
CheckCombine 関数を呼び出し、現在の座標にあるぷよから繋がっているぷよの数を ret に代入します。
if (ret >= 4) {
flag = true;
elist[i] = true;
score += ret * 10;
}
繋がっているぷよの数が4以上ならば連結フラグをセットし、結合リストをセットします。さらに得点を繋がった数× 10 点追加します。
if (flag) {
for (y = 13; y >= 0; y--) {
for (x = 1; x < 7; x++) {
if (elist[cmb[y][x]]) field[y][x] = 5;
}
}
status = ERASE2;
結合ぷよが存在する場合、結合ぷよを灰色の消しぷよに入れ替え、状態をぷよ消し中にします。
} else {
for (y = 0; y < 2; y++) {
for (x = 1; x < 7; x++) {
if (field[y][x] != -1) overFlag = true;
}
}
status = NEXT;
結合ぷよが存在しない場合、上部2段のどこかにぷよが存在するか調べ、あったならばゲームオーバーフラグをセットします。さらに状態をネクストぷよ出現にします。メイン画面 ぷよ消し中
Sleep(500);
for (y = 13; y >= 0; y--) {
for (x = 1; x < 7; x++) {
if (field[y][x] == 5) field[y][x] = -1;
}
}
status = FALL;
灰色の消しぷよが表示されている状態です。まず 500 ミリ秒待ち、その後消しぷよを空間にし、状態をぷよ落下中にします。メイン画面 画面表示処理
elDraw::Box(192, 0, 256, 448, RGB(150, 150, 150), RGB(150, 150, 150), 0);
elDraw::Line(0, 64, 192, 64, RGB(255, 255, 255), 1);
画面右側の灰色の四角と、これより上にぷよが置かれたらゲームオーバーになる線を書きます。
for (y = 0; y < 14; y++) {
for (x = 1; x < 7; x++) {
if (field[y][x] != -1) {
elDraw::Layer((x - 1) * 32, y * 32, puyo,
field[y][x] * 32, 0, field[y][x] * 32 + 32, 32);
}
}
}
画面配列を見て、ぷよを描いている部分です。
if (status == NORMAL) {
elDraw::Layer((px1 - 1) * 32, py1 * 32,
puyo, pno1 * 32, 0, pno1 * 32 + 32, 32);
elDraw::Layer((px2 - 1) * 32, py2 * 32,
puyo, pno2 * 32, 0, pno2 * 32 + 32, 32);
}
現在のぷよを表示しています。状態が通常のときだけ表示します。
elDraw::Layer(208, 144, puyo, pnext2 * 32, 0, pnext2 * 32 + 32, 32);
elDraw::Layer(208, 144 + 32, puyo, pnext1 * 32, 0, pnext1 * 32 + 32, 32);
ネクストぷよを表示している部分です。
if (overFlag) {
elSystem::Message("ゲームオーバー");
elDraw::Exit();
}
ゲームオーバーフラグが立っていた場合、メッセージを表示してゲームを終了します。