C++でつくったテトリスの完成品
今回はTetrisクラスの実装を行ないます。
Contents
ヘッダーファイル
Tetris.hを示します。privateとかpublicを繰り返して見た目は最悪ですが、このような場合はどうすればいいのでしょうか? publicならpublicでまとめる、メンバ変数とメンバ関数をわけて見やすくするなどの考え方がありますが、機能でわけることにしました。そのためprivateとpublicが繰り返し出現するという美しくない書き方になってしまいました。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
#pragma once // 10列20行 #define COLUMN_MAX 10 #define ROW_MAX 20 #define BLOCK_SIZE 20 class Tetris { // 初期化 public: Tetris(); void Init(); private: void ResetTimer(); // 落下中のテトロミノの位置・状態を保存する変数 private: int m_TetriminoPositionX = 0; int m_TetriminoPositionY = 0; TetriminoTypes m_CurTetriminoType = TetriminoTypes::I; TetriminoAngle m_TetriminoAngle = TetriminoAngle::Angle0; // 落下中のテトロミノの位置の取得関連 private: void GetTetriminosPosition(TetriminoPosition tetri[], int size); void GetInitBlocksPositionI(TetriminoPosition tetri[], int size); void GetInitBlocksPositionJ(TetriminoPosition tetri[], int size); void GetInitBlocksPositionL(TetriminoPosition tetri[], int size); void GetInitBlocksPositionO(TetriminoPosition tetri[], int size); void GetInitBlocksPositionS(TetriminoPosition tetri[], int size); void GetInitBlocksPositionZ(TetriminoPosition tetri[], int size); void GetInitBlocksPositionT(TetriminoPosition tetri[], int size); void GetInitBlocksRotate33(TetriminoPosition tetri[], int size, TetriminoAngle angle); void GetInitBlocksRotate44(TetriminoPosition tetri[], int size, TetriminoAngle angle); // 表示色取得関連 private: COLORREF GetColor(Color color); Color GetTetriminoColor(); // 描画関連 public: POINT LeftTopBlockPosition; void Draw(HDC hdc); void GetRect(RECT* pRect); private: void DrawBlock(HDC hdc, int column, int row, Color color); void DrawOutsideBlocks(HDC hdc); void DrawTetrimino(HDC hdc); void DrawFixedBlock(HDC hdc); // 移動関連 public: BOOL m_IsSoftDrop = FALSE; BOOL MoveLeft(); BOOL MoveRight(); BOOL MoveUp(); BOOL MoveDown(); private: BOOL CanMove(int x, int y); // 回転関連 public: BOOL RotateRight(); BOOL RotateLeft(); private: BOOL CanRotate(BOOL isRight, int x, int y); BOOL RotateRight(int x, int y); BOOL RotateRight3(); BOOL RotateRightI(); BOOL RotateLeft(int x, int y); BOOL RotateLeft3(); BOOL RotateLeftI(); // ラインの消去関連 public: void DropLines(); private: Block* m_FixedBlocks[ROW_MAX][COLUMN_MAX]; int m_DeleteLineNumbers[4]; BOOL CheckDeletingLines(); void FixBlocks(); void CheckLine(); BOOL DeleteLines(int lineNumbers[], int size); void NewTetrimino(); void SetNewBlockType(); }; |
初期化の処理
コンストラクタを示します。
LeftTopBlockPositionに座標をセットしていますが、これはウィンドウのなかのブロックが表示される部分の左上の座標を(0, 0)ではなく余白を持たせたかったのでこのようにしました。
そのあとInit関数を呼び出してゲームの初期化をしています。
Tetris.cpp
1 2 3 4 5 6 7 8 9 10 |
#include "all.h" #include <time.h> Tetris::Tetris() { LeftTopBlockPosition.x = 150; LeftTopBlockPosition.y = 70; Init(); } |
初期化の処理です。ここでやっていることは着地して固定されているブロックの位置を格納する二次元配列をNULLで埋めています。ゲームをしおわってもう一度始めたい場合は前のゲームのデータが残っているのでdeleteしてからNULLで埋めています。
次にm_DeleteLineNumbersですが、ここには横一列にそろった行の番号が入ります。テトリスであれば最大4行を同時に消すことができます。最初は4つとも-1で初期化します。ここに0以上の整数が格納されている場合はラインが消去されたことを意味しています。この場合はタイマーで時間になってもテトリミノを落下させる処理はしません。
実はこんなことをしなくてもラインが消えたらすぐに上の段にあるブロックを下げて空間を詰める処理をしてしまえばいいのですが、ラインが消えたあと一瞬ブロックが浮かんでいるような状態を作りたかったのでこのようにしています。
最後に乱数の初期化をおこないます。これは次におちてくるテトリミノの種類をランダムで決めるための処理です。これをやらないと毎回同じ順番で前回実行時と同じテトリミノが落ちてきて、ゲームが単調になってしまいます。
Tetris.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void Tetris::Init() { for (int row = 0; row < 20; row++) { for (int column = 0; column < 10; column++) { delete m_FixedBlocks[row][column]; m_FixedBlocks[row][column] = NULL; } } for (int i = 0; i < 4; i++) m_DeleteLineNumbers[i] = -1; unsigned int now = (unsigned int)time(0); srand(now); } |
ResetTimer関数はタイマーをリセットするためのものです。テトリスではテトリミノを移動させたり回転させると一時的にその段で止めることができます。これを無制限にできるようにしてしまうとゲームとしての難易度が下がってしまうので制限値があるのですが、ここでは無制限にできるようにしています。この点はあとで考えます。まずは完成させることを目指します。
1 2 3 4 5 6 |
void Tetris::ResetTimer() { // タイマーをいったん殺してすぐにセットする KillTimer(hMainWnd, TIMER_FREE_FALL); SetTimer(hMainWnd, TIMER_FREE_FALL, 1000, NULL); } |
描画処理
描画処理をする前に描画対象をどの位置に描画するのかを決めなければなりません。また表示するときに使う色も指定しなければなりません。
まずは表示色取得関連の関数を定義します。
表示色を取得する
テトリミノの種類で表示色は決まっています。そこでガイドラインで定められているとおりの色を指定します。Color::Aquaってどんな色なのかはそのあとの関数で取得します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
Color Tetris::GetTetriminoColor() { if (m_CurTetriminoType == TetriminoTypes::I) return Color::Aqua; else if (m_CurTetriminoType == TetriminoTypes::J) return Color::Blue; else if (m_CurTetriminoType == TetriminoTypes::L) return Color::Orange; else if (m_CurTetriminoType == TetriminoTypes::O) return Color::Yellow; else if (m_CurTetriminoType == TetriminoTypes::S) return Color::Green; else if (m_CurTetriminoType == TetriminoTypes::T) return Color::Violet; else if (m_CurTetriminoType == TetriminoTypes::Z) return Color::Red; else return Color::Gray; } |
GetColor関数はColor列挙体からCOLORREF型でデータを取得する処理をおこないます。CSSで赤だと#FF0000と書きますが、COLORREFの場合は赤が一番右側です。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
COLORREF Tetris::GetColor(Color color) { if (color == Color::Aqua) return 0x00FFFF00; else if (color == Color::Blue) return 0x00FF0000; else if (color == Color::Green) return 0x0000FF00; else if (color == Color::Orange) return 0x000045FF; else if (color == Color::Red) return 0x000000FF; else if (color == Color::Violet) return 0x00800080; else if (color == Color::Yellow) return 0x0000FFFF; else if (color == Color::Gray) return 0x00808080; else return 0x00AAAAAA; } |
落下中のテトロミノの位置を取得する
以下は落下中のテトロミノの位置・状態を保存するメンバ変数です。
1 2 3 4 5 |
// Tetrisクラスのメンバ変数 int m_TetriminoPositionX = 0; int m_TetriminoPositionY = 0; TetriminoTypes m_CurTetriminoType = TetriminoTypes::I; TetriminoAngle m_TetriminoAngle = TetriminoAngle::Angle0; |
では落下中のテトロミノの位置を取得するための関数を定義していきます。
GetTetriminosPosition関数は現在落下中のテトロミノの種類と向きからTetriminoPositionの配列を取得します。IミノであればGetInitBlocksPositionI関数を呼び出してテトリミノの左上の行と列が0,0である場合の4つのブロックの位置を取得します。そのあと回転処理をおこない、現在のm_TetriminoPositionXとm_TetriminoPositionYへ平行移動させることでTetriminoPositionの配列を取得しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 |
void Tetris::GetTetriminosPosition(TetriminoPosition pos[], int size) { // m_TetriminoPositionXとm_TetriminoPositionYが0のときのTetriminoPositionの配列を得る if (m_CurTetriminoType == TetriminoTypes::I) GetInitBlocksPositionI(pos, size); else if (m_CurTetriminoType == TetriminoTypes::J) GetInitBlocksPositionJ(pos, size); else if (m_CurTetriminoType == TetriminoTypes::L) GetInitBlocksPositionL(pos, size); else if (m_CurTetriminoType == TetriminoTypes::O) GetInitBlocksPositionO(pos, size); else if (m_CurTetriminoType == TetriminoTypes::S) GetInitBlocksPositionS(pos, size); else if (m_CurTetriminoType == TetriminoTypes::T) GetInitBlocksPositionT(pos, size); else if (m_CurTetriminoType == TetriminoTypes::Z) GetInitBlocksPositionZ(pos, size); // 回転させる。Oミノは回転しない。Iミノ以外は3×3、Iミノは4×4なので分けて処理をする if (m_CurTetriminoType != TetriminoTypes::I) GetInitBlocksRotate33(pos, size, m_TetriminoAngle); else if (m_CurTetriminoType == TetriminoTypes::I) GetInitBlocksRotate44(pos, size, m_TetriminoAngle); // m_TetriminoPositionXとm_TetriminoPositionYが現在の値の場合のTetriminoPositionの配列を得る for (int i = 0; i < size; i++) { pos[i].m_column += m_TetriminoPositionX; pos[i].m_row += m_TetriminoPositionY; } } |
各テトリミノの初期のTetriminoPositionの配列を取得する処理を示します。テトリミノは4つのブロックで構成されているので第二引数は常に4です。念のため4であることをチェックして、そうでない場合は何もしません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void Tetris::GetInitBlocksPositionI(TetriminoPosition pos[], int size) { if (size != 4) return; pos[0].m_column = 0; pos[0].m_row = 1; pos[1].m_column = 1; pos[1].m_row = 1; pos[2].m_column = 2; pos[2].m_row = 1; pos[3].m_column = 3; pos[3].m_row = 1; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void Tetris::GetInitBlocksPositionJ(TetriminoPosition pos[], int size) { if (size != 4) return; pos[0].m_column = 0; pos[0].m_row = 0; pos[1].m_column = 0; pos[1].m_row = 1; pos[2].m_column = 1; pos[2].m_row = 1; pos[3].m_column = 2; pos[3].m_row = 1; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void Tetris::GetInitBlocksPositionL(TetriminoPosition pos[], int size) { if (size != 4) return; pos[0].m_column = 2; pos[0].m_row = 0; pos[1].m_column = 0; pos[1].m_row = 1; pos[2].m_column = 1; pos[2].m_row = 1; pos[3].m_column = 2; pos[3].m_row = 1; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void Tetris::GetInitBlocksPositionO(TetriminoPosition pos[], int size) { if (size != 4) return; pos[0].m_column = 1; pos[0].m_row = 0; pos[1].m_column = 1; pos[1].m_row = 1; pos[2].m_column = 2; pos[2].m_row = 0; pos[3].m_column = 2; pos[3].m_row = 1; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void Tetris::GetInitBlocksPositionS(TetriminoPosition pos[], int size) { if (size != 4) return; pos[0].m_column = 1; pos[0].m_row = 0; pos[1].m_column = 2; pos[1].m_row = 0; pos[2].m_column = 0; pos[2].m_row = 1; pos[3].m_column = 1; pos[3].m_row = 1; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void Tetris::GetInitBlocksPositionT(TetriminoPosition pos[], int size) { if (size != 4) return; pos[0].m_column = 1; pos[0].m_row = 0; pos[1].m_column = 0; pos[1].m_row = 1; pos[2].m_column = 1; pos[2].m_row = 1; pos[3].m_column = 2; pos[3].m_row = 1; } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
void Tetris::GetInitBlocksPositionZ(TetriminoPosition pos[], int size) { if (size != 4) return; pos[0].m_column = 0; pos[0].m_row = 0; pos[1].m_column = 1; pos[1].m_row = 0; pos[2].m_column = 1; pos[2].m_row = 1; pos[3].m_column = 2; pos[3].m_row = 1; } |
GetInitBlocksPositionX関数でXミノの初期のTetriminoPosition配列を得ることができたらこれに回転処理を加えます。前述のとおり、テトリミノには3×3タイプと4×4タイプがあるので分けて考えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
void Tetris::GetInitBlocksRotate33(TetriminoPosition pos[], int size, TetriminoAngle angle) { if (size != 4) return; if (angle == TetriminoAngle::Angle90) { for (int i = 0; i < size; i++) { int x = pos[i].m_column; int y = pos[i].m_row; pos[i].m_column = 3 - y - 1; pos[i].m_row = x; } } else if (angle == TetriminoAngle::Angle180) { for (int i = 0; i < size; i++) { int x = pos[i].m_column; int y = pos[i].m_row; pos[i].m_column = 3 - x - 1; pos[i].m_row = 3 - y - 1; } } else if (angle == TetriminoAngle::Angle270) { for (int i = 0; i < size; i++) { int x = pos[i].m_column; int y = pos[i].m_row; pos[i].m_column = y; pos[i].m_row = 3 - x - 1; } } } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 |
void Tetris::GetInitBlocksRotate44(TetriminoPosition pos[], int size, TetriminoAngle angle) { if (size != 4) return; if (angle == TetriminoAngle::Angle90) { for (int i = 0; i < size; i++) { int x = pos[i].m_column; int y = pos[i].m_row; pos[i].m_column = 4 - y - 1; pos[i].m_row = x; } } else if (angle == TetriminoAngle::Angle180) { for (int i = 0; i < size; i++) { int x = pos[i].m_column; int y = pos[i].m_row; pos[i].m_column = 4 - x - 1; pos[i].m_row = 4 - y - 1; } } else if (angle == TetriminoAngle::Angle270) { for (int i = 0; i < size; i++) { int x = pos[i].m_column; int y = pos[i].m_row; pos[i].m_column = y; pos[i].m_row = 4 - x - 1; } } } |
実際に描画する
描画する位置であるTetriminoPosition配列を取得できたら描画の処理をおこないます。
描画の対象は周囲の灰色のブロックと落下中のテトロミノ、そしてすでに着地して固定されているブロックです。本当はスコアとか次に落ちてくるテトリミノも表示させたいのですが、まずは完成を急ぎます。
1 2 3 4 5 6 |
void Tetris::Draw(HDC hdc) { DrawOutsideBlocks(hdc); DrawTetrimino(hdc); DrawFixedBlock(hdc); } |
DrawBlock関数は列と行と色を指定してブロックを描画するためのものです。輪郭は黒で内部を引数で渡された色で塗りつぶします。メンバ関数 GetColorをつかってCOLORREFを取得しています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
void Tetris::DrawBlock(HDC hdc, int column, int row, Color color) { COLORREF colorref = GetColor(color); // ペンとブラシを生成・選択 HPEN hPen = CreatePen(PS_SOLID, 1, RGB(0, 0, 0)); HPEN hOldPen = (HPEN)SelectObject(hdc, hPen); HBRUSH hBrush = CreateSolidBrush(colorref); HBRUSH hOldBrush = (HBRUSH)SelectObject(hdc, hBrush); // 0行0列の位置を少しウィンドウの内側に移動させる int x = column * BLOCK_SIZE + LeftTopBlockPosition.x; int y = row * BLOCK_SIZE + LeftTopBlockPosition.y; ::Rectangle(hdc, x, y, x + BLOCK_SIZE, y + BLOCK_SIZE); // ペンとブラシをもとに戻す SelectObject(hdc, hOldPen); SelectObject(hdc, hOldBrush); // 自分で作成したペンとブラシを削除する DeleteObject(hPen); DeleteObject(hBrush); } |
DrawOutsideBlocks関数は周囲の灰色のブロックを描画する処理をおこないます。
1 2 3 4 5 6 7 8 9 10 |
void Tetris::DrawOutsideBlocks(HDC hdc) { for (int row = 0; row < ROW_MAX + 1; row++) { DrawBlock(hdc, -1, row, Color::Gray); DrawBlock(hdc, COLUMN_MAX, row, Color::Gray); } for (int colum = -1; colum < COLUMN_MAX + 1; colum++) DrawBlock(hdc, colum, ROW_MAX, Color::Gray); } |
DrawTetrimino関数は落下中のテトロミノを描画する処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 |
void Tetris::DrawTetrimino(HDC hdc) { TetriminoPosition position[4]; // 現在のテトロミノの位置と色を取得する GetTetriminosPosition(position, 4); Color color = GetTetriminoColor(); // 取得した位置をDrawBlock関数にわたして描画する for (int i = 0; i < 4; i++) DrawBlock(hdc, position[i].m_column, position[i].m_row, color); } |
DrawFixedBlock関数は着地し固定されているブロックを描画する処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
void Tetris::DrawFixedBlock(HDC hdc) { for(int row = 0; row< ROW_MAX; row++) { for (int column = 0; column < COLUMN_MAX; column++) { Block *pBlock = m_FixedBlocks[row][column]; // NULLでなければブロックが存在するということなので描画する if(pBlock != NULL) DrawBlock(hdc, column, row, pBlock->color); } } } |
GetRect関数は再描画の対象になる矩形を取得します。ここでは単純に現在落下中のテトリミノの周辺を切り取ってそのまま返しています。ラインが消えるときでなければ再描画の対象になるのは現在落下中のテトリミノの周辺だけです。
1 2 3 4 5 6 7 8 9 10 11 |
void Tetris::GetRect(RECT *pRect) { // スーパーローテーションシステムを採用するため // (m_TetriminoPositionX - 2, m_TetriminoPositionY - 2)を起点に8×8を取得している int x = LeftTopBlockPosition.x + BLOCK_SIZE * (m_TetriminoPositionX - 2); int y = LeftTopBlockPosition.y + BLOCK_SIZE * (m_TetriminoPositionY - 2); pRect->left = x; pRect->top = y; pRect->right = x + BLOCK_SIZE * 8; pRect->bottom = y + BLOCK_SIZE * 8; } |
移動関連の処理
けっこうな字数になってきたので次回に回したいのですが、もうちょっと頑張ります。移動関連の処理をおこないます。
移動できるのか確認する
移動させるときはm_TetriminoPositionX と m_TetriminoPositionYを増減させればいいのですが、そのまえに本当に移動させることができるのかどうか確認しなければなりません。
CanMove関数は引数分XY方向に移動させることができるのかどうかを調べます。移動させたら外枠に重なってしまったりすでに固定されているブロックと重なってしまう場合は移動できません。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
BOOL Tetris::CanMove(int x, int y) { // 移動先のTetriminoPositionの配列を取得する TetriminoPosition tetri[4]; GetTetriminosPosition(tetri, 4); // 外に出たり固定されているブロックとぶつかってしまう場合は移動不可とする for (int i = 0; i < 4; i++) { if (tetri[i].m_column + x > COLUMN_MAX - 1) return FALSE; if (tetri[i].m_column + x < 0) return FALSE; if (tetri[i].m_row + y > ROW_MAX - 1) return FALSE; if(m_FixedBlocks[tetri[i].m_row + y][tetri[i].m_column + x] != NULL) return FALSE; } return TRUE; } |
左右の移動
左右に移動させる処理を示します。移動できる場合はm_TetriminoPositionXとm_TetriminoPositionYを増減させるとともにタイマーをリセットします。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 |
BOOL Tetris::MoveRight() { if (CanMove(1, 0)) { m_TetriminoPositionX += 1; m_TetriminoPositionY += 0; ResetTimer(); return TRUE; } return FALSE; } BOOL Tetris::MoveLeft() { if(CanMove(-1, 0)) { m_TetriminoPositionX += -1; m_TetriminoPositionY += 0; ResetTimer(); return TRUE; } return FALSE; } |
下に移動させる
下に移動させる場合、これ以上下に移動できない場合があります。その場合はテトロミノを落下中の状態から固定された状態に変更します。
それから他にもすることがあって固定されたときに横一列にそろっているかどうかの確認作業、横一列にそろっている場合はラインを消去して上に積まれているブロックを下に1段落とさなければなりません。そして落下中のテトロミノを固定した場合は次のテトリミノを上から落とさなければなりません。
テトリミノを固定する処理はFixBlocks関数、ラインが消去されるかどうかはCheckLine関数でおこない、新しいテトリミノをつくる処理はNewTetrimino関数でおこないます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
BOOL Tetris::MoveDown() { if(CheckDeletingLines()) return FALSE; ResetTimer(); if (CanMove(0, 1)) { m_TetriminoPositionX += 0; m_TetriminoPositionY += 1; } else { FixBlocks(); CheckLine(); NewTetrimino(); } return FALSE; } |
FixBlocks関数はテトリミノを固定する処理をおこないます。
1 2 3 4 5 6 7 8 9 10 11 |
void Tetris::FixBlocks() { TetriminoPosition tetri[4]; GetTetriminosPosition(tetri, 4); for (int i = 0; i < 4; i++) { Block* pBlock = new Block(); pBlock->color = GetTetriminoColor(); m_FixedBlocks[tetri[i].m_row][tetri[i].m_column] = pBlock; } } |
NewTetrimino関数は新しいテトリミノをつくる処理をおこないます。新しく作るというよりはメンバ変数をリセットするという表現のほうが正しいかもしれません。Positionを(0, 0)に戻して新しいタイプのテトリミノをセットします。
1 2 3 4 5 6 7 |
void Tetris::NewTetrimino() { m_TetriminoAngle = TetriminoAngle::Angle0; m_TetriminoPositionX = 0; m_TetriminoPositionY = 0; SetNewBlockType(); } |
SetNewBlockType関数は新しいタイプのテトリミノを乱数で決定してm_CurTetriminoTypeにセットします。この単に乱数で決めるというやり方はテトリスのガイドラインに則していません。いまはちょっとスルーさせてください。あとで修正します。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void Tetris::SetNewBlockType() { int type =rand() % 7; if(type == 0) m_CurTetriminoType = TetriminoTypes::I; if (type == 1) m_CurTetriminoType = TetriminoTypes::J; if (type == 2) m_CurTetriminoType = TetriminoTypes::L; if (type == 3) m_CurTetriminoType = TetriminoTypes::O; if (type == 4) m_CurTetriminoType = TetriminoTypes::S; if (type == 5) m_CurTetriminoType = TetriminoTypes::T; if (type == 6) m_CurTetriminoType = TetriminoTypes::Z; } |
ブロックが消えたときの処理
CheckLine関数はブロックが横一列にそろったかを調べます。そしてそろっている場合はブロックを消去して上の段にあるブロックを下に下げます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 |
void Tetris::CheckLine() { int index = 0; // チェックする前に配列m_DeleteLineNumbersを-1で初期化する for(int i = 0; i < 4; i++) m_DeleteLineNumbers[i] = -1; for(int row = 0; row < ROW_MAX; row++) { BOOL isComplete= TRUE; for (int column = 0; column < COLUMN_MAX; column++) { // 同じ行にひとつでもNULLがあればその部分が空いている // すなわちそろっていない if(m_FixedBlocks[row][column] == NULL) { isComplete = FALSE; break; } } // ループから抜けたときisComplete == TRUEであればその段はブロックがそろっているといえる // その場合は配列にその行の番号を格納する if (isComplete) { m_DeleteLineNumbers[index] = row; index++; } } // ブロックを削除する。実際に削除の処理がおこなわれたらDeleteLines関数はTRUEを返す BOOL ret = DeleteLines(m_DeleteLineNumbers, 4); if(ret) { // DeleteLines関数が実行されたらウィンドウ全体を再描画する // 同時にタイマーをセットして0.1秒後に上の段にあるブロックを下に移動させる SetTimer(hMainWnd, TIMER_DROP_LINES, 100, NULL); InvalidateRect(hMainWnd, NULL, TRUE); } } |
DeleteLines関数は引数で渡された配列のなかに-1でない値があった場合、その行にあるブロックを削除します。このあと上記の関数のなかでInvalidateRect関数が呼び出されるので、この部分は約0.1秒間、空白となり、これより上にあるブロックが宙に浮いているように見えます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
BOOL Tetris::DeleteLines(int lineNumbers[], int size) { BOOL done = FALSE; for(int i=0; i < size; i++) { // 要素が-1のときはなにもしない // なにかしたときはこの関数はTRUEを返す int lineNumber = lineNumbers[i]; if(lineNumber == -1) continue; done = TRUE; for(int x=0; x < COLUMN_MAX; x++) { // deleteしたあとNULLを代入。ここにはブロックはないと認識される delete m_FixedBlocks[lineNumber][x]; m_FixedBlocks[lineNumber][x] = NULL; } } return done; } |
DeleteLines関数でセットされたタイマーによってWM_TIMERメッセージが送られてきた場合はMainWindowクラスのWinProc関数内でOnTimer関数が実行され、これによってDropLines関数が呼び出されます。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 |
void Tetris::DropLines() { for (int i = 0; i < 4; i++) { // 配列 m_DeleteLineNumbersのなかの-1以外の値がある場合、 // その行は上にあるブロックで詰められる int lineNumber = m_DeleteLineNumbers[i]; if (lineNumber == -1) continue; for (int x = 0; x < COLUMN_MAX; x++) { // その行をひとつ上にあるブロックで埋める for (int row = lineNumber; row >= 1; row--) { m_FixedBlocks[row][x] = m_FixedBlocks[row - 1][x]; } // 一番上の行はNULLを代入する。必ず一番上の行が空く m_FixedBlocks[0][x] = NULL; } m_DeleteLineNumbers[i] = -1; } KillTimer(hMainWnd, TIMER_DROP_LINES); } |
MoveDown関数が実行されたときにライン消去の処理がおこなわれている場合、ライン消去の処理を優先しMoveDown関数としては何もしません。ライン消去の処理がおこなわれているかどうかを調べる関数を示します。
1 2 3 4 5 6 7 8 9 10 |
BOOL Tetris::CheckDeletingLines() { for (int i = 0; i < 4; i++) { int lineNumber = m_DeleteLineNumbers[i]; if (lineNumber != -1) return TRUE; } return FALSE; } |