C++でつくったテトリスの完成品
これまでC++で作成してきたテトリスに機能を追加します。テトリスにハードドロップといってキーを押すと一瞬でテトリミノを着地させることができます。またハードドロップさせた場合、どこにブロックが配置されるのかを示すゴーストを表示させるタイプのものもあります。
そこで今回はゴーストとハードドロップの機能を実装することを目指します。
ハードドロップの実装
まずどのキーをおせばハードドロップさせることができるかを決めなければなりません。テトリスのガイドラインではスペースキーが当てられています。ここでもそれに従うことにします。
Tetrisクラスのメンバ変数にHardDrop関数を追加し、スペースキーが押されたときにこれが呼び出されるようにします。
MainWindow.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 |
LRESULT MainWindow::OnKeyDown(WPARAM wp, LPARAM lp) { UINT vk = (UINT)(wp); BOOL ret = FALSE; switch (vk) { // テトリミノを移動・回転させるキーが押されたときの処理は省略 case VK_SPACE: ret = m_Tetris.HardDrop(); break; default: break; } // 以下、省略 return 0L; } |
Tetrisクラスにもメンバ関数を追加しなければなりません。以下の関数を追加します。
Tetris.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
class Tetris { // ゴーストの表示とハードドロップ関連のメンバ関数 // それ以外のメンバは省略 public: BOOL HardDrop(); void DrawGhostBlocks(HDC hdc); private: int GetHardDropCount(); COLORREF GetGhostColor(Color color); void GetGhostBlocks(Block* blocks, int size); void DrawGhostBlock(HDC hdc, int column, int row, Color color); }; |
ではどのような処理をすればよいでしょうか? いまそのままテトリミノを落下できるところまで移動させるとどこまで移動できるのかを考えます。この位置はゴーストを表示すべき位置でもあります。
そこで先にテトリミノをどこまで落下させることができるかを調べる関数をつくります。それがGetHardDropCount関数です。
Tetris.cpp
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 |
int Tetris::GetHardDropCount() { TetriminoPosition blocks[4]; GetTetriminosPosition(blocks, 4); int count = 0; while (true) { BOOL failure = FALSE; for (int i = 0; i < 4; i++) { blocks[i].m_row++; if (0 <= blocks[i].m_row && blocks[i].m_row < ROW_MAX && 0 <= blocks[i].m_column && blocks[i].m_column < COLUMN_MAX) { if (blocks[i].m_row < ROW_MAX && m_FixedBlocks[blocks[i].m_row][blocks[i].m_column] != NULL) failure = TRUE; } else failure = TRUE; if (failure) break; } if (!failure) count++; else break; } return count; } |
スペースキーが押されたらGetHardDropCountで現在のテトリミノは何段落下することができるのかを調べ、その分だけ下に平行移動させます。そしてハードドロップを実行したらそのままテトリミノは固定されてしまうので、FixBlocks関数を実行します。
1 2 3 4 5 6 7 8 |
BOOL Tetris::HardDrop() { int fallCount = GetHardDropCount(); m_TetriminoPositionY += fallCount; InvalidateRect(hMainWnd, NULL, TRUE); FixBlocks(); return TRUE; } |
ゴーストを描画する
次にゴーストを描画するための処理ですが、これはTetris関数のメンバであるDraw関数の最初にゴーストを描画するための処理を追加します。落下中のテトロミノとゴーストが重なってしまう場合もあるので、先にゴーストを描画してそのあと落下中のテトロミノを描画すればトラブルはおきません。
1 2 3 4 5 6 7 8 |
void Tetris::Draw(HDC hdc) { DrawGhostBlocks(hdc); DrawFixedBlock(hdc); DrawOutsideBlocks(hdc); DrawTetrimino(hdc); } |
DrawGhostBlocks関数ではゴーストを描画すべき位置を取得してその場所にゴーストを描画します。ゴーストを描画すべき位置はGetGhostBlocks関数で取得します。
1 2 3 4 5 6 7 |
void Tetris::DrawGhostBlocks(HDC hdc) { Block blocks[4]; GetGhostBlocks(blocks, 4); for (int i = 0; i < 4; i++) DrawGhostBlock(hdc, blocks[i].m_column, blocks[i].m_row, blocks[i].color); } |
GetGhostBlocks関数では現在のテトリミノの位置を取得したあと、GetHardDropCount関数でどこまで落下することができるのかを調べて、そのあとm_rowをそれだけ増やしたBlockの配列を返します。このときテトリミノの色も取得しておきます。ゴーストを表示するとき表示色も必要だからです。
1 2 3 4 5 6 7 8 9 10 11 12 13 |
void Tetris::GetGhostBlocks(Block* blocks, int size) { TetriminoPosition pos[4]; GetTetriminosPosition(pos, 4); int fallCount = GetHardDropCount(); for(int i=0; i<4; i++) { blocks[i].m_column = pos[i].m_column; blocks[i].m_row = pos[i].m_row + fallCount; blocks[i].color = GetTetriminoColor(); // 落下中のテトロミノの色を格納しておく } } |
ゴースト用の色をつくる
ゴーストを表示する場所ともとの色がわかったらあとは表示させるだけです。この処理はDrawGhostBlock関数でおこないます。ゴーストはテトロミノよりも目立たない色で描画したいので新しくGetGhostColor関数を作ります。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
void Tetris::DrawGhostBlock(HDC hdc, int column, int row, Color color) { COLORREF colorref = GetGhostColor(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); 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); } |
GetGhostColor関数ですが、こんな感じになっています。テトリミノの元の色よりも黒に近い色にしています。
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
COLORREF Tetris::GetGhostColor(Color color) { if (color == Color::Aqua) return 0x00404000; else if (color == Color::Blue) return 0x00800000; else if (color == Color::Green) return 0x00004000; else if (color == Color::Orange) return 0x00001040; else if (color == Color::Red) return 0x00000040; else if (color == Color::Violet) return 0x00200020; else if (color == Color::Yellow) return 0x00004040; else return 0x00808080; } |
それからゴーストを描画するということは再描画すべき部分が増えることを意味しています。再描画の処理によってチカチカすると目立つのは外枠のブロックです。そこでフィールド内部のブロックが多少チカチカするのは仕方ないということにして以下のように譲歩します。外枠ではなくその内部だけを再描画の対象としています。これだけでもチカチカをかなり抑えることができます。
1 2 3 4 5 6 7 |
void Tetris::GetRect(RECT *pRect) { pRect->left = LeftTopBlockPosition.x; pRect->top = LeftTopBlockPosition.y; pRect->right = LeftTopBlockPosition.x + BLOCK_SIZE * COLUMN_MAX; pRect->bottom = LeftTopBlockPosition.y + BLOCK_SIZE * ROW_MAX; } |
スコアの表示
それからスコアも表示させたいと思います。
得点ですが、これを参考にしました。
これによると1列だけ消した場合は40点、2列同時に消した場合は100点、3列になると300点、そして4列同時に消した場合は1200点です。1列ずつチマチマ消してもたいした点数になりませんが、Iミノをぶっ込んで4列同時に消すと大量得点をゲットすることができるというわけです。
それからラインを消せない場合でもドロップ得点というものがあります。高いところから落とすとそれだけ大きな点数を得ることができるのです。↓キーを押してドロップする場合とハードドロップでは得点に差をつけているゲームもあります。そこで↓キーを押したままにして急速落下させた場合は1段あたり1点、ハードドロップの場合は高さの2倍の得点にすることにします。
まずスコア表示のためのメンバ変数とメンバ関数を追加します。
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 |
class Tetris { // 既出のメンバ変数とメンバ関数は省略 // スコア表示のためのメンバ変数とメンバ関数 private: int m_Score = 0; void ShowScore(HDC hdc); HFONT SetScoreFont(HDC hdc, int height); }; HFONT Tetris::SetScoreFont(HDC hdc, int height) { HFONT hFont; hFont = CreateFont(height, // フォント高さ 0, 0, 0, // 文字幅、テキストの角度、ベースラインとx軸との角度 FW_BOLD, FALSE, FALSE, FALSE, // フォントの太さ、イタリック体、アンダーライン、打ち消し線 SHIFTJIS_CHARSET, OUT_DEFAULT_PRECIS, CLIP_DEFAULT_PRECIS, //文字セット、出力精度、クリッピング精度 PROOF_QUALITY, FIXED_PITCH | FF_MODERN, // 出力品質、ピッチとファミリー L"MS ゴシック"); //書体名 return hFont; } void Tetris::ShowScore(HDC hdc) { WCHAR scoreText[32]; wsprintf(scoreText, L"SCORE %d", m_Score); HFONT hFont = SetScoreFont(hdc, 20); HFONT hFontOld = (HFONT)SelectObject(hdc, hFont); SetBkColor(hdc, RGB(0, 0, 0)); SetTextColor(hdc, RGB(255, 255, 255)); TextOut(hdc, 20, 20, scoreText, lstrlen(scoreText)); SelectObject(hdc, hFontOld); DeleteObject(hFont); } |
それからTetris::Draw関数にShowScore関数を追加します。
1 2 3 4 5 6 7 8 9 10 |
void Tetris::Draw(HDC hdc) { DrawGhostBlocks(hdc); DrawFixedBlock(hdc); DrawOutsideBlocks(hdc); DrawTetrimino(hdc); ShowScore(hdc); } |
得点したらm_Scoreを増やす
あとは必要なときにm_Scoreを増やしていけばOKです。
まずハードドロップのときはウィンドウ全体を再描画しているのでスコアが変化すると自動的に表示もかわります。
1 2 3 4 5 6 7 8 9 10 |
BOOL Tetris::HardDrop() { int fallCount = GetHardDropCount(); m_Score += fallCount * 2; // スコア追加 m_TetriminoPositionY += fallCount; InvalidateRect(hMainWnd, NULL, TRUE); FixBlocks(); return TRUE; } |
ところがソフトドロップの場合は、チカチカを防止するため、ウィンドウ全体を再描画するのではなく灰色のブロックで囲まれた枠内だけを描画しています。これではスコア部分が再描画されないので、別途スコアが表示される領域を更新する処理を追加しています。
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 |
BOOL Tetris::MoveDown() { if(CheckDeletingLines()) return FALSE; // ソフトドロップは1つ落下させるたびに1点追加 if(m_IsSoftDrop) { m_Score++; // スコアが表示される領域を更新する RECT rect; rect.left = 0; rect.top = 0; rect.right = 300; rect.bottom = 70; InvalidateRect(hMainWnd, &rect, TRUE); } ResetTimer(); if (CanMove(0, 1)) { m_TetriminoPositionX += 0; m_TetriminoPositionY += 1; } else { FixBlocks(); CheckLine(); NewTetrimino(); } return TRUE; } |
ブロックライン得点はブロックが消えて上の段のブロックが落ちてきたときに点数加算の処理をしています。
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 |
void Tetris::DropLines() { int count = 0; for (int i = 0; i < 4; i++) { 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]; } m_FixedBlocks[0][x] = NULL; } count++; m_DeleteLineNumbers[i] = -1; } // 1列だけ消した場合は40点、2列:100点、3列:300点、4列:1200点 if(count == 1) m_Score += 40; if (count == 2) m_Score += 100; if (count == 3) m_Score += 300; if (count == 4) m_Score += 1200; KillTimer(hMainWnd, TIMER_DROP_LINES); } |
スコアを表示させるだけでずいぶんそれっぽいものになります。