C++でつくったテトリスの完成品
前回宣言したとおり、今回はホールドの機能を実装します。
Contents
ホールドの機能を実装する
テトリスのガイドラインではホールドに関する記述があります。これによると
テトリミノを1個保持できる。
使うと現在のミノがホールドに入り、ホールドにあったミノが降ってくる
ホールドされているミノは存在しない場合は現在のミノがホールドに入りネクストにあるミノが降る。
1回使うと設置するまで使えない。
それからホールドするためのキーは「C」を使います。左回転ならZ、右回転ならX、ホールドはさらに右側にあるCです。
ではさっそくホールドの機能を実装していきましょう。そのために必要なメンバ変数とメンバ関数を追加します。
Tetris.h
1 2 3 4 5 6 7 8 9 10 11 12 13 |
class Tetris { // 以下を追加 // ホールドに関するメンバ変数とメンバ関数 private: TetriminoTypes m_holdTetrimino; BOOL m_CanHold = TRUE; void ShowHold(HDC hdc); public: BOOL Hold(); }; |
ホールドされているテトリミノはフィールドの左側に表示させます。
そこでTetris::Draw関数にホールドされているテトリミノを描画するためのShowHold関数を追加します。
Tetris.cpp
1 2 3 4 5 6 7 8 9 10 11 12 |
void Tetris::Draw(HDC hdc) { DrawGhostBlocks(hdc); DrawFixedBlock(hdc); DrawOutsideBlocks(hdc); DrawTetrimino(hdc); ShowScore(hdc); ShowNext(hdc); ShowHold(hdc); // 追加した } |
TetriminoTypes列挙体へのNoneの追加
ホールドの処理をするとき、すでにホールドされているものがあれば入れ替えが必要です。
現在ホールドされているものを TetriminoTypes m_holdTetrimino;で表現したいのでTetriminoTypes列挙体にNoneを追加します。
Block.h
1 2 3 4 5 6 7 8 9 10 11 |
enum class TetriminoTypes { I, O, S, Z, J, L, T, None, }; |
それからゲーム開始時にはホールドされているテトリミノは存在しないのでm_holdTetriminoにはTetriminoTypes::Noneを格納しておきます。
Tetris.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void Tetris::Init() { InitFixedBlocks(); // ゲーム開始時にはホールドされているテトリミノは存在しない // ゲーム開始直後はいつでもホールドは可能である m_holdTetrimino = TetriminoTypes::None; m_CanHold = TRUE; for (int i = 0; i < 4; i++) m_DeleteLineNumbers[i] = -1; unsigned int now = (unsigned int)time(0); srand(now); CreateBag(); NewTetrimino(); } |
ホールドされているテトリミノを表示するShowHold関数
あとはホールドされているものがあればフィールドの左側に描画します。テトリミノを描画する方法NEXTブロックの描画と同じ(違うのは描画する位置)なのでDrawNextBlock関数を使っています。
Tetris.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void Tetris::ShowHold(HDC hdc) { // 文字列「HOLD」を表示 SetTextColor(hdc, RGB(255, 255, 255)); HFONT hFont = SetScoreFont(hdc, 16); HFONT hFontOld = (HFONT)SelectObject(hdc, hFont); const WCHAR* str = L"HOLD"; TextOut(hdc, 30, 50, str, lstrlen(str)); SelectObject(hdc, hFontOld); DeleteObject(hFont); POINT leftTop; leftTop.x = 40; leftTop.y = 100; if(m_holdTetrimino != TetriminoTypes::None) DrawNextBlock(hdc, leftTop, m_holdTetrimino); } |
ホールドを実行するHold関数
ホールドボタンが押されたらHold関数を実行します。ホールドのルールは「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 |
BOOL Tetris::Hold() { // ホールドできない場合はなにもしない if(m_CanHold == FALSE) return FALSE; // ホールドしたら設置されるまでホールドはできなくなる m_CanHold = FALSE; // なにもホールドされていない場合は落下中のテトロミノをホールドして次のテトリミノを降らせる // すでになにかがホールドされている場合は落下中のテトロミノをホールドして // いままでホールドされていたテトリミノを一番上から降らせる if(m_holdTetrimino == TetriminoTypes::None) { m_holdTetrimino = m_CurTetriminoType; NewTetrimino(); } else { TetriminoTypes oldHold = m_holdTetrimino; m_holdTetrimino = m_CurTetriminoType; m_TetriminoAngle = TetriminoAngle::Angle0; m_TetriminoPositionX = 3; m_TetriminoPositionY = -1; m_CurTetriminoType = oldHold; } InvalidateRect(hMainWnd, NULL, TRUE); return TRUE; } |
再びホールドを可能にするには
ホールドするとテトリミノが設置されるまでホールドはできませんが、設置されてしまえなふたたびホールド可能になります。
これはテトリミノが設置されたタイミングでm_CanHoldをTRUEに変更すればいいですね。つまりFixBlocks関数が実行されたときです。
1 2 3 4 5 6 7 8 9 10 11 12 |
void Tetris::FixBlocks() { TetriminoPosition blocks[4]; GetTetriminosPosition(blocks, 4); for (int i = 0; i < 4; i++) { Block* pBlock = new Block(); pBlock->color = GetTetriminoColor(); m_FixedBlocks[blocks[i].m_row][blocks[i].m_column] = pBlock; } m_CanHold = TRUE; } |
以上でホールドの機能を実装することができました。
細かい部分の修正
思ったよりも簡単に終わってしまったので、ちょっと細かい部分の修正をおこないます。
ゲームオーバー判定
ゲームオーバーになったらゲームオーバーになったことを示す文字列の表示とゲームにもう一度挑戦できる手段を用意しておかないといけません。
そこでヘッダーに以下を追加します。
Tetris.h
1 2 3 4 5 6 7 8 9 10 11 12 |
class Tetris { // 既出のメンバ変数とメンバ関数は省略 // ゲームオーバーとゲーム再挑戦の処理 private: BOOL m_IsGameOver = FALSE; void ShowGameOver(HDC hdc); public: BOOL IsGameOver(); void Retry(); }; |
まずゲームオーバーの判定ですが、落下中のテトロミノが着地・固定されたあと新しいテトロミノが上から降ってきますが、このときすでに固定されているブロックと接触しているのであれば、ゲームオーバー状態になっていることがわかります。その場合は
キー操作をうけつけない
タイマーによるメッセージに対応しない
とよいと思われます。
新しいテトロミノが上方に出現するのはNewTetrimino関数が実行されたときです。ここでCanMove(0, 0)を実行すればテトロミノが動かない状態ですでに固定されているブロックと衝突しているかがわかります。
ゲームオーバーであることを示すメンバ関数m_IsGameOverを定義し、ゲームオーバーになったらこれをTRUEにします。
Tetris.cpp
1 2 3 4 5 6 7 8 9 10 |
void Tetris::NewTetrimino() { m_TetriminoAngle = TetriminoAngle::Angle0; m_TetriminoPositionX = 3; m_TetriminoPositionY = -1; SetNewBlockType(); if(!CanMove(0, 0)) // すでに固定されているブロックと衝突していないか? m_IsGameOver = TRUE; } |
それからゲームオーバーになってしまったら新しいテトロミノをつくるのをやめます。
1 2 3 4 5 6 7 8 9 10 11 |
void Tetris::SetNewBlockType() { if(m_IsGameOver) return; m_CurTetriminoType = m_vectorTetriminoTypes[0]; m_vectorTetriminoTypes.erase(m_vectorTetriminoTypes.begin()); CreateBag(); InvalidateRect(hMainWnd, NULL, TRUE); } |
またm_IsGameOverフラグの状態を取得するメンバ関数もつくります。これでTetrisクラスの外からもゲームオーバーであるかどうかがわかります。
1 2 3 4 |
BOOL Tetris::IsGameOver() { return m_IsGameOver; } |
ゲームに再挑戦できるようにする
キー操作が行なわれたときの処理です。ゲームオーバーのときにSキーを押すとゲームに再挑戦できます。ゲームオーバーのときはそれ以外のキー操作ができません。
MainWindow.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 |
LRESULT MainWindow::OnKeyDown(WPARAM wp, LPARAM lp) { if(m_Tetris.IsGameOver()) { UINT vk = (UINT)(wp); if(vk == 'S') m_Tetris.Retry(); return 0L; } // 以下はこれまでと同じ処理 // ゲームオーバーであればゲーム再開のための「S」キー以外では一切のキー操作ができない } |
タイマーに関するメッセージを処理する場合も同様にゲームオーバー時にはなにもしません。
MainWindow.cpp
1 2 3 4 5 6 7 8 |
LRESULT MainWindow::OnTimer(WPARAM wp, LPARAM lp) { if (m_Tetris.IsGameOver()) return 0L; // 以下はこれまでと同じ処理 // ゲームオーバーであれば一切のタイマーに関するメッセージに反応しない } |
ゲームを再開するときはTetris::Retry関数を実行します。スコアとホールド可能不可能のフラグ、ゲームオーバーフラグのクリア、次に落ちてくるテトリミノの情報の初期化をおこないます。それ以外の処理はInit関数でやってくれます。
Tetris.cpp
1 2 3 4 5 6 7 8 9 10 |
void Tetris::Retry() { m_Score = 0; m_CanHold = TRUE; m_holdTetrimino = TetriminoTypes::None; m_IsGameOver = FALSE; m_vectorTetriminoTypes.clear(); Init(); } |
ゲームオーバー時にゲームオーバー表示をする
普通のゲームはゲームオーバーになったらゲームオーバーと表示されます。Tetris::Draw関数のなかでm_IsGameOverフラグがセットされているならゲームオーバー表示を実行する関数を呼び出すだけです。さっそくやってみましょう。PRESS S KEYと表示させることで、ゲームをもう一度するのであればSキーを押せばよいということもわかります。
Tetris.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
void Tetris::ShowGameOver(HDC hdc) { HFONT hFont = SetScoreFont(hdc, 20); HFONT hFontOld = (HFONT)SelectObject(hdc, hFont); SetTextColor(hdc, RGB(255, 255, 255)); const WCHAR* str1 = L"GAME OVER"; TextOut(hdc, 160, 100, str1, lstrlen(str1)); SelectObject(hdc, hFontOld); DeleteObject(hFont); hFont = SetScoreFont(hdc, 15); hFontOld = (HFONT)SelectObject(hdc, hFont); const WCHAR* str2 = L"PRESS S KEY"; TextOut(hdc, 170, 130, str2, lstrlen(str2)); SelectObject(hdc, hFontOld); DeleteObject(hFont); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
void Tetris::Draw(HDC hdc) { DrawGhostBlocks(hdc); DrawFixedBlock(hdc); DrawOutsideBlocks(hdc); DrawTetrimino(hdc); ShowScore(hdc); ShowNext(hdc); ShowHold(hdc); // この部分を追加した if(m_IsGameOver) ShowGameOver(hdc); } |