「PC98」の昔、「QuickBASIC」や「TurboC」のグラフィックスライブラリで遊んでました。Windowsになってからは中々手が出ませんでした。最近出版された、次の3冊
日経ソフトウエア 2024年1月号
日経ソフトウエア 2024年3月号
のおかげで何かできそうな気が。前回の迷路ゲームに続き、今回テトリス風ゲーム作成に挑戦してみました。とは言え普段ゲームをしない人間なので、テトリスは名前を聞いたことがあるくらい。あまり詳しくは知りません。早速Web上のそれらしいゲームを少しやり、先人のコードを参考に…。しかし、残念ながら高度なコード(?)ばかりで、私には理解できません。諦めるのも癪なので、わずかな手がかりをもとに自分で組上げてみました。
一応、まともに動くようになったので投稿します。気付く範囲では複数のキーを押し続けるとブロックや固定物が崩れる不具合などがあります。まあ、ここまでできて自分としては満足してます。本物のテトリスを知らないヒトが作ったものなので、使い勝手の悪さなどはご容赦を。
開発環境やDXライブラリについては、前掲の本とこのサイト
などを参考にしてください。長くなりますが、以下にコードのみを示します。
(2024/04/27修正)
#include "DxLib.h"
#include <time.h>
#include <Windows.h>
#define WIDTH 600 // ウィンドウの幅と高さのピクセル数
#define HEIGHT 480
#define Y_H 24 // フィールドの高さ(上3段と最下段の壁を含む)
#define X_W 12 // フィールドの幅(両側の壁を含む)
#define Vars 32 // ブロックの種類(回転で得られるもの全部を別種とする)
#define PI 20 // ブロック一個のピクセル数
#define Y0 1 // フィールド表示位置
#define X0 170
int field[Y_H][X_W];// ブロックなどのデータを設定
int aline[Y_H]; // 横一列の固定ブロック数
int bXL = 0, bYU = 0, bW = 0, bH = 0, bXR = 0, bYD = 0; // 現ブロックのデータの代入先
enum { TITLE, PLAY, OVER, RESET }; // 各シーンを定める定数
int scene = TITLE; // どのシーンの処理を行うか
int nextB5[5];
unsigned int W = GetColor(255, 255, 255);
unsigned int R = GetColor(255, 0, 0);
unsigned int G = GetColor(0, 255, 0);
unsigned int K = GetColor(0, 0, 0);
unsigned int D = GetColor(80, 80, 80);
unsigned int Y = GetColor(200, 200, 0);
int bSizeX[Vars] = { 4,1,4,1, // 各ブロックの横サイズ
3,2,3,2,
3,2,3,2,
3,2,3,2,
3,2,3,2,
2,2,2,2,
3,2,3,2,
3,2,3,2
};
int bSizeY[Vars] = { 1,4,1,4, // 各ブロックの縦サイズ
2,3,2,3,
2,3,2,3,
2,3,2,3,
2,3,2,3,
2,2,2,2,
2,3,2,3,
2,3,2,3
};
int blocks[Vars][4][4] = // variety, y, x
{ {
{ 1,1,1,1 },// I
{ 0,0,0,0 },
{ 0,0,0,0 },
{ 0,0,0,0 }
},
{
{ 1,0,0,0 },
{ 1,0,0,0 },
{ 1,0,0,0 },
{ 1,0,0,0 }
},
{
{ 1,1,1,1 },
{ 0,0,0,0 },
{ 0,0,0,0 },
{ 0,0,0,0 }
},
{
{ 1,0,0,0 },
{ 1,0,0,0 },
{ 1,0,0,0 },
{ 1,0,0,0 }
},
{
{ 1,1,1,0 },// 逆L
{ 0,0,1,0 },
{ 0,0,0,0 },
{ 0,0,0,0 }
},
{
{ 0,1,0,0 },
{ 0,1,0,0 },
{ 1,1,0,0 },
{ 0,0,0,0 }
},
{
{ 1,0,0,0 },
{ 1,1,1,0 },
{ 0,0,0,0 },
{ 0,0,0,0 }
},
{
{ 1,1,0,0 },
{ 1,0,0,0 },
{ 1,0,0,0 },
{ 0,0,0,0 }
},
{
{ 0,0,1,0 },// L
{ 1,1,1,0 },
{ 0,0,0,0 },
{ 0,0,0,0 }
},
{
{ 1,0,0,0 },
{ 1,0,0,0 },
{ 1,1,0,0 },
{ 0,0,0,0 }
},
{
{ 1,1,1,0 },
{ 1,0,0,0 },
{ 0,0,0,0 },
{ 0,0,0,0 }
},
{
{ 1,1,0,0 },
{ 0,1,0,0 },
{ 0,1,0,0 },
{ 0,0,0,0 }
},
{
{ 1,1,1,0 },// T
{ 0,1,0,0 },
{ 0,0,0,0 },
{ 0,0,0,0 }
},
{
{ 0,1,0,0 },
{ 1,1,0,0 },
{ 0,1,0,0 },
{ 0,0,0,0 }
},
{
{ 0,1,0,0 },
{ 1,1,1,0 },
{ 0,0,0,0 },
{ 0,0,0,0 }
},
{
{ 1,0,0,0 },
{ 1,1,0,0 },
{ 1,0,0,0 },
{ 0,0,0,0 }
},
{
{ 0,1,0,0 },// 逆T
{ 1,1,1,0 },
{ 0,0,0,0 },
{ 0,0,0,0 }
},
{
{ 1,0,0,0 },
{ 1,1,0,0 },
{ 1,0,0,0 },
{ 0,0,0,0 }
},
{
{ 1,1,1,0 },
{ 0,1,0,0 },
{ 0,0,0,0 },
{ 0,0,0,0 }
},
{
{ 0,1,0,0 },
{ 1,1,0,0 },
{ 0,1,0,0 },
{ 0,0,0,0 }
},
{
{ 1,1,0,0 },// 正方形
{ 1,1,0,0 },
{ 0,0,0,0 },
{ 0,0,0,0 }
},
{
{ 1,1,0,0 },
{ 1,1,0,0 },
{ 0,0,0,0 },
{ 0,0,0,0 }
},
{
{ 1,1,0,0 },
{ 1,1,0,0 },
{ 0,0,0,0 },
{ 0,0,0,0 }
},
{
{ 1,1,0,0 },
{ 1,1,0,0 },
{ 0,0,0,0 },
{ 0,0,0,0 }
},
{
{ 0,1,1,0 },// 稲妻
{ 1,1,0,0 },
{ 0,0,0,0 },
{ 0,0,0,0 }
},
{
{ 1,0,0,0 },
{ 1,1,0,0 },
{ 0,1,0,0 },
{ 0,0,0,0 }
},
{
{ 0,1,1,0 },
{ 1,1,0,0 },
{ 0,0,0,0 },
{ 0,0,0,0 }
},
{
{ 1,0,0,0 },
{ 1,1,0,0 },
{ 0,1,0,0 },
{ 0,0,0,0 }
},
{
{ 1,1,0,0 },// 逆稲妻
{ 0,1,1,0 },
{ 0,0,0,0 },
{ 0,0,0,0 }
},
{
{ 0,1,0,0 },
{ 1,1,0,0 },
{ 1,0,0,0 },
{ 0,0,0,0 }
},
{
{ 1,1,0,0 },
{ 0,1,1,0 },
{ 0,0,0,0 },
{ 0,0,0,0 }
},
{
{ 0,1,0,0 },
{ 1,1,0,0 },
{ 1,0,0,0 },
{ 0,0,0,0 }
}
};
void initField() {
for (int i = 0; i < Y_H - 1; i++) {
for (int j = 0; j < X_W; j++) {
if (i > 2 && (j == 0 || j == X_W - 1)) field[i][j] = 9; // 0,1,2は壁なし
else field[i][j] = 0; // 何も無い所は0(黒)
}
aline[i] = 0;
}
for (int j = 0; j < X_W; j++) field[Y_H - 1][j] = 9; // 壁は9(濃い灰色)
}
void drawField(void) {
for (int i = 0; i < Y_H; i++) {
for (int j = 0; j < X_W; j++) {
if (field[i][j] == 1) // 移動中のブロックは黄
DrawBox(X0 + PI * j, Y0 + PI * i, X0 + PI * j + PI - 2, Y0 + PI * i + PI - 2, Y, TRUE);
if (field[i][j] == 2) // 固定ブロックは白
DrawBox(X0 + PI * j, Y0 + PI * i, X0 + PI * j + PI - 2, Y0 + PI * i + PI - 2, W, TRUE);
if (field[i][j] == 9) // 壁は濃い灰色
DrawBox(X0 + PI * j, Y0 + PI * i, X0 + PI * j + PI - 2, Y0 + PI * i + PI - 2, D, TRUE);
}
if (aline[i] == X_W - 3) DrawBox(X0 + PI * 0, Y0 + PI * i, X0 + PI * 0 + PI - 2, Y0 + PI * i + PI - 2, R, TRUE);
}
}
void bRight(int n) { // 右移動
if (bXR < X_W - 2) {
for (int y = bH - 1; y >= 0; y--) {
for (int x = bW - 1; x >= 0; x--)
if (field[bYU + y][bXL + x + 1] > 1) return; // 固定物に衝突
}
for (int y = bH - 1; y >= 0; y--) {
for (int x = bW - 1; x >= 0; x--) {
if (field[bYU + y][bXL + x] < 2 && field[bYU + y][bXL + x + 1] < 2) // 自分自身のみ移動
field[bYU + y][bXL + x + 1] = field[bYU + y][bXL + x]; // フィールドデータ移動
if (field[bYU + y][bXL + x] >= 2 && field[bYU + y][bXL + x + 1] < 2)
field[bYU + y][bXL + x + 1] = 0;
}
if (field[bYU + y][bXL] < 2) field[bYU + y][bXL] = 0;
}
bXL++;
bXR++;
}
}
void bLeft(int n) { // 左移動
if (bXL > 1) {
for (int y = bH - 1; y >= 0; y--) {
for (int x = 0; x < bW; x++)
if (field[bYU + y][bXL + x - 1] > 1 && field[bYU + y][bXL + x] == 1) return; // 衝突
}
for (int y = bH - 1; y >= 0; y--) {
for (int x = 0; x < bW; x++) {
if (field[bYU + y][bXL + x - 1] < 2 && field[bYU + y][bXL + x] < 2) // 自分自身のみ移動
field[bYU + y][bXL + x - 1] = field[bYU + y][bXL + x];
if (field[bYU + y][bXL + x - 1] < 2 && field[bYU + y][bXL + x] >= 2)
field[bYU + y][bXL + x - 1] = 0;
}
if (field[bYU + y][bXR] < 2) field[bYU + y][bXR] = 0;
}
bXL--;
bXR--;
}
}
int rotCheck(int n0, int bXL, int bYU) {
int l, m, n = n0;
int bW, bH, bXR, bYD;
l = (int)(n / 4) * 4;
m = ((n % 4) + 1) % 4;
n = l + m; // 回転後のブロック番号でチェック
bW = bSizeX[n]; // 各情報更新
bH = bSizeY[n];
bXR = bXL + bW - 1;
bYD = bYU + bH - 1;
if (bXR >= X_W - 1) { // 右壁にぶつかったとき
bXL -= 1;
bXR -= 1;
}
if (bXL < 1) { // 左壁にぶつかったとき
bXL += 1;
bXR += 1;
}
bool flg = true; // Collision Check
for (int y = 0; y < bH; y++) {
for (int x = 0; x < bW; x++) {
if (blocks[n][y][x] == 1 && field[bYU + y][bXL + x] > 1) {
flg = false; // ブロック描画位置に何かある
}
}
}
if (flg == true) return 1;
else return 0;
}
void bRot(int* num) { // 回転(右のみ)
int l, m, n = *num;
if (rotCheck(n, bXL, bYU) == 0) return; // 回転できない
for (int y = 0; y < bH; y++) { // 前ブロックを消す
for (int x = 0; x < bW; x++) {
field[bYU + y][bXL + x] -= blocks[n][y][x];
DrawBox(X0 + PI * (bXL + x), Y0 + PI * (bYU + y), X0 + PI * (bXL + x)+PI - 2, Y0 + PI * (bYU + y)+PI - 2, D,TRUE);
}
}
l = (int)(n / 4) * 4;
m = ((n % 4) + 1) % 4;
n = l + m; // 回転後の新ブロック番号
bW = bSizeX[n]; // 各情報更新
bH = bSizeY[n];
bXR = bXL + bW - 1;
bYD = bYU + bH - 1;
if (bXR >= X_W - 1) { // 右壁にぶつかったとき
bXL -= 1;
bXR -= 1;
}
if (bXL < 1) { // 左壁にぶつかったとき
bXL += 1;
bXR += 1;
}
for (int y = 0; y < bH; y++) { // 回転後のブロック描画
for (int x = 0; x < bW; x++) {
if (field[bYU + y][bXL + x] <= 1) {
if (blocks[n][y][x] == 1) {
field[bYU + y][bXL + x] = 1;
DrawBox(X0 + PI * (bXL + x), Y0 + PI * (bYU + y), X0 + PI * (bXL + x)+PI - 2, Y0 + PI * (bYU + y)+PI - 2, Y, TRUE);
}
else if (blocks[n][y][x] == 0) {
field[bYU + y][bXL + x] = 0;
DrawBox(X0 + PI * (bXL + x), Y0 + PI * (bYU + y), X0 + PI * (bXL + x)+PI - 2, Y0 + PI * (bYU + y)+PI - 2, K,TRUE);
}
}
}
}
*num = n;
}
int rakka(int n) {
bool descentflg = TRUE;
bW = bSizeX[n]; bH = bSizeY[n];
for (int x = 0; x < bW; x++) {
for (int y = bH - 1; y > bH - 4; y--) {
if (blocks[n][y][x] == 1 && field[bYU + y + 1][bXL + x] > 1)descentflg = FALSE; // ブロック落下不可
}
}
int sum;
if (descentflg == FALSE) {
for (int y = 0; y < bH; y++) {
for (int x = 0; x < bW; x++) {
if (blocks[n][y][x] == 1) field[bYU + y][bXL + x] = 2; // ブロック固定
}
}
int TG[Y_H];
for (int i = 0; i < Y_H; i++) { TG[i] = -1; }
sum = 0;
for (int i = Y_H - 2; i >= 0; i--) {// 横一列揃ったか調べる
aline[i] = 0;
for (int j = 1; j < X_W - 1; j++) {
if (field[i][j] == 2) aline[i] = aline[i] + 1;
}
if (aline[i] == X_W - 2) { // == 10 揃った
DrawBox(X0 + PI - 1, Y0 + PI * i - 1, X0 + PI * (X_W - 1) + 1, Y0 + PI * (i + 1), R, TRUE); // 赤で強調
TG[sum] = i;
sum++;
Sleep(2);
}
}
if (aline[3] > 0) return aline[3]; //--- GAME OVER ----------------
if (sum > 0) { // 揃った行を消す
for (int t = 0; t < sum; t++) {
for (int i = TG[t] + t; i > 0; i--) {
DrawBox(X0 + PI - 1, Y0 + PI * i - 1, X0 + PI * (X_W - 1) + 1, Y0 + PI * i + PI + 1, R, TRUE); // 赤で強調
Sleep(2);
for (int j = 1; j < X_W - 1; j++) {
field[i][j] = field[i - 1][j]; // 消去行に上段のデータを移動
if (field[i][j] == 0)
DrawBox(X0 + PI * j, Y0 + PI * i, X0 + PI * j + 18, Y0 + PI * i + 18, K, TRUE);
else // field[i][j] == 2 固定ブロックは描画
DrawBox(X0 + PI * j, Y0 + PI * i, X0 + PI * j + 18, Y0 + PI * i + 18, W, TRUE);
Sleep(2);
}
aline[i] = aline[i - 1]; // 固定ブロック数の行データも移動
}
for (int j = 1; j < X_W - 1; j++) field[0][j] = 0; // 最上段は0で埋める
Sleep(2);
}
}
sum = 0;
descentflg = TRUE;
return -1; // 次の新ブロックの生成落下
}
else { // 落下可能
bYU++; bYD++;
for (int i = Y_H - 2; i >= 0; i--) { // 最上段の0,1,2行もブロックを描画、落下させる
if (i == 0) { for (int j = 1; j < X_W - 1; j++) field[i][j] = 0; } // 最上段は0で埋める
else {
for (int j = 1; j < X_W - 1; j++) {
if (field[i][j] >= 2) { ; } // 壁、固定物があるので移動できない
else if (field[i - 1][j] == 2) { ; } // 固定物は移動しない
else {
field[i][j] = field[i - 1][j]; // 一行下に移動
field[i - 1][j] = 0; // 移動後は空欄
}
}
}
}
int tY = bYU;
bool flg = TRUE;
while (flg) {
tY += 1;
for (int y = 0; y < bH; y++) {
for (int x = 0; x < bW; x++) { // 現状、どこまで落下できるか調べる
if (field[tY + y][bXL + x] > 1 && blocks[n][y][x] == 1) {
flg = FALSE;
}
}
}
}
tY -= 1;
for (int y = 0; y < bH; y++) { // 落下予想図
for (int x = 0; x < bW; x++) {
if (blocks[n][y][x] == 1) {
DrawBox(X0 + PI * (bXL + x), Y0 + PI * (tY + y), X0 + PI * (bXL + x) + 18, Y0 + PI * (tY + y) + 18, R, TRUE);
}
}
}
return 0;
}
}
void nbDisp(int* b) { // 次のブロック5個を表示
int X1 = 450, X2 = 490; // 文字(番号)、ブロック位置
for (int n = 0; n < 5; n++) {
int i = b[n]; // numbers of next 5 blocks
for (int y = 0; y < bSizeY[i]; y++) {
for (int x = 0; x < bSizeX[i]; x++) {
if (blocks[i][y][x] == 1)
DrawBox(X2 + PI * x, Y0 + 100 * n + PI * y, X2 + PI * x + 18, Y0 + 100 * n + PI * y + 18, Y, TRUE);
}
}
SetFontSize(14);
if (n == 0)DrawFormatString(X1, n * 100 + 5, W, "NEXT");
else DrawFormatString(X1 + 15, n * 100 + 5, W, "%d", n);
}
}
void nbSet(int* num, int* b) { // 新ブロックをセット
bW = bSizeX[*num];
bH = bSizeY[*num];
bYU = 4 - bH;
int j = rand() % (X_W - bSizeX[*num] - 1) + 1;
bXL = j;
bXR = bXL + bW - 1;
bYD = bYU + bH - 1;
for (int y = 0; y < bH; y++) {
for (int x = 0; x < bW; x++) {
if (blocks[*num][y][x] == 1) field[bYU + y][bXL + x] = 1;
else field[bYU + y][bXL + x] = 0;
}
}
for (int n = 0; n < 4; n++) b[n] = b[n + 1]; // キューの更新
b[4] = rand() % 32;
}
void gStart(int* t) {
srand((unsigned int)time(NULL));
initField();
for (int i = 0; i < 5; i++) nextB5[i] = rand() % 24; // 5個のブロック生成。初手は稲妻型以外。
scene = PLAY;
*t = -1; // timer = -1 で新ブロック生成、落下が始まる
}
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdLine, int nCmdShow)
{
SetWindowText("Tenoros"), SetGraphMode(WIDTH, HEIGHT, 32), ChangeWindowMode(TRUE);
if (DxLib_Init() == -1) return -1;
SetBackgroundColor(0, 0, 0);
SetDrawScreen(DX_SCREEN_BACK);
int dtimer = 5;
int machi = 200;
int countSP = 0;
int bNum = 0;
int timer = 0;
bool jamp = FALSE;
initField();
for (int i = 0; i < 5; i++) nextB5[i] = rand() % 24; // 5個のブロック生成
while (1) { // メインループ
ClearDrawScreen();
SetFontSize(14);
DrawFormatString(0, 0, W, "%d", timer);
drawField();
switch (scene) { // 画面遷移を行うswitch case
case TITLE:
SetFontSize(50);
DrawString(190, 161, "手のロス", R);
SetFontSize(50);
DrawString(187, 158, "手のロス", W);
SetFontSize(20);
DrawString(194, 240, "りたーんキーで開始", R);
if (CheckHitKey(KEY_INPUT_RETURN)) {
scene = PLAY;
gStart(&timer);
}
break;
case PLAY:
SetFontSize(16);
DrawString(5, 150, "「→」で右移動", Y);
DrawString(5, 180, "「←」で左移動", Y);
DrawString(5, 210, "「↓」で速く落下", Y);
DrawString(5, 240, "「SP」で(右)回転", Y);
DrawString(5, 320, "RキーでRESET", W);
DrawString(5, 350, "ESCで終了", G);
if (CheckHitKey(KEY_INPUT_R)) {
scene = RESET;
timer = -1;
}
if (CheckHitKey(KEY_INPUT_RIGHT)) bRight(bNum);
if (CheckHitKey(KEY_INPUT_LEFT)) bLeft(bNum);
if (CheckHitKey(KEY_INPUT_DOWN)) jamp = TRUE; // DOWNキーで落下速度アップ
if (CheckHitKey(KEY_INPUT_SPACE)) {
if (countSP++ > 2) { // キーのチャタリング対策
bRot(&bNum);
countSP = 0;
}
}
if (timer == 0) {
bNum = nextB5[0];
nbSet(&bNum, nextB5);
}
else if (timer % dtimer == 0) {
int t = rakka(bNum); // ブロック落下処理
if (t == -1) timer = -1;// 新ブロックの生成落下
if (t > 0) scene = OVER;//if (aline[3] > 0)
}
nbDisp(nextB5);
break;
case OVER:
SetFontSize(50);
DrawString(173, 240, "GAME OVER", R);
if (timer > 50) {
scene = PLAY;
gStart(&timer);
}
break;
case RESET:
SetFontSize(50);
DrawString(160, 240, "GAME RESET", R);
if (timer > 25) {
scene = PLAY;
gStart(&timer);
}
break;
default:
break;
}
timer++;
ScreenFlip();
if (jamp == TRUE) {
jamp = FALSE; // 待ち時間なし
//dtimer = 1;
}
else {
//dtimer = 5;
WaitTimer(machi); // 一定時間待つ
}
if (ProcessMessage() == -1) break;
if (CheckHitKey(KEY_INPUT_ESCAPE) == 1) break;
}
DxLib_End();
return 0;
}
最後まで御覧いただきありがとうございます。