C++でつくったテトリスの完成品
前回の鳩ではなく猫並みの知能を手に入れるためにC++でアプリをつくるで宣言したとおり、C++でテトリスをつくります。まずは本体部分のソースファイルの内容を示します。無精をしてall.hのなかにinclude文とdefine文やグローバル変数を書いています。
Contents
WinMain関数
WinMain関数を示します。
app.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 |
#include "all.h" HWND hMainWnd = NULL; int WINAPI WinMain(_In_ HINSTANCE hInstance, _In_opt_ HINSTANCE hPrevInstance, _In_ LPSTR lpCmdLine, _In_ int nCmdShow) { MainWindow mainWindow(hInstance); MSG msg; while (GetMessage(&msg, NULL, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } return (msg.wParam); } |
ヘッダーファイルについて
all.hは以下のようになっています。
タイマーを3つ使います。IDがTIMER_FREE_FALLのタイマーは1秒経過するごとにテトリミノをひとつ下げるためのものです。TIMER_SOFT_DROPは↓ボタンを押しているあいだテトリミノを急速落下させるためのタイマーのIDで、TIMER_DROP_LINESは横一列にブロックを消すときにいきなり上のブロックが落ちてくるのではなく間を空ける(特に意味はない。ちょっとした演出)ためのものです。
all.h
1 2 3 4 5 6 7 8 9 10 11 12 13 |
#pragma once #include <windows.h> #include "Block.h" #include "Tetris.h" #include "MainWindow.h" extern HWND hMainWnd; #define TIMER_FREE_FALL 100 #define TIMER_SOFT_DROP 200 #define TIMER_DROP_LINES 300 |
基本は前回と同じ?
MainWindow.hとMainWindow.cppは前回のコードにちょっとだけ追加しただけです。タイマーを使うのとキーの押下に反応するようにしました。
MainWindow.h
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 |
#pragma once class MainWindow { HWND m_hWnd = NULL; HINSTANCE m_hInstance = NULL; const WCHAR* m_szClassNme = L"MyClassName1"; const WCHAR* m_szTitle = L"C++でテトリスをつくる"; Tetris m_Tetris; // ゲームの本体になるオブジェクト public: MainWindow(HINSTANCE hInstance); void Create(); static LRESULT CALLBACK WndProcStatic(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp); LRESULT WndProc(UINT msg, WPARAM wp, LPARAM lp); LRESULT OnCreate(WPARAM wp, LPARAM lp); LRESULT OnDestroy(WPARAM wp, LPARAM lp); LRESULT OnPaint(WPARAM wp, LPARAM lp); // ここから下を追加 LRESULT OnTimer(WPARAM wp, LPARAM lp); LRESULT OnKeyDown(WPARAM wp, LPARAM lp); LRESULT OnKeyUp(WPARAM wp, LPARAM lp); }; |
MainWindow.cppを示します。Create関数は表示するウィンドウのサイズが変更されています。OnDestroy関数は前回と同じです。WM_KEYDOWN、WM_KEYUP、WM_TIMERメッセージを処理するためにWndProc関数を変更しています。OnCreate関数のなかでゲームの初期化のような処理をしています。OnPaint関数も描画したいものが前回とは違っているので変更になります。それからそれっぽいイメージを出すために背景を黒にしています。これだけでなんとなくスタイリッシュなアプリっぽく見えます。
前回とほぼ同じ部分だけ先に示します。
MainWindow.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 32 33 34 |
#include "all.h" MainWindow::MainWindow(HINSTANCE hInstance) { Create(); } void MainWindow::Create() { WNDCLASS myProg; myProg.style = CS_HREDRAW | CS_VREDRAW; myProg.lpfnWndProc = WndProcStatic; myProg.cbClsExtra = 0; myProg.cbWndExtra = 0; myProg.hInstance = m_hInstance; myProg.hIcon = NULL; myProg.hCursor = LoadCursor(NULL, IDC_ARROW); myProg.hbrBackground = (HBRUSH)GetStockObject(BLACK_BRUSH); // 背景を黒にする myProg.lpszMenuName = NULL; myProg.lpszClassName = m_szClassNme; RegisterClass(&myProg); CreateWindow(m_szClassNme, m_szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, 530, 600, NULL, NULL, m_hInstance, this); ShowWindow(m_hWnd, SW_SHOWNORMAL); UpdateWindow(m_hWnd); } |
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
LRESULT CALLBACK MainWindow::WndProcStatic(HWND hWnd, UINT msg, WPARAM wp, LPARAM lp) { MainWindow *pMainWindow = (MainWindow *)GetWindowLongPtr(hWnd, GWL_USERDATA); if (msg == WM_CREATE) { LONG_PTR longPtr = (LONG_PTR)LPCREATESTRUCT(lp)->lpCreateParams; SetWindowLongPtr(hWnd, GWLP_USERDATA, longPtr); pMainWindow = reinterpret_cast<MainWindow*>(GetWindowLongPtr(hWnd, GWLP_USERDATA)); pMainWindow->m_hWnd = hWnd; } if (pMainWindow != NULL) return pMainWindow->WndProc(msg, wp, lp); else return DefWindowProc(hWnd, msg, wp, lp); } |
ここから前回とは違ってきます。
WndProc関数とその周辺
MainWindow.cpp
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
LRESULT MainWindow::WndProc(UINT msg, WPARAM wp, LPARAM lp) { switch (msg) { case WM_CREATE: return OnCreate(wp, lp); case WM_DESTROY: return OnDestroy(wp, lp); case WM_PAINT: return OnPaint(wp, lp); case WM_KEYDOWN: return OnKeyDown(wp, lp); case WM_KEYUP: return OnKeyUp(wp, lp); case WM_TIMER: return OnTimer(wp, lp); default: return(DefWindowProc(m_hWnd, msg, wp, lp)); } return 0L; } |
OnCreate関数
ウィンドウが生成されたらウィンドウハンドルが確定するのでこれをグローバル変数に格納しています。そしてタイマーをセットします。IDがTIMER_FREE_FALLのタイマーは1秒経過するごとにテトリミノをひとつ下に下げるためのものです。もうひとつのTIMER_SOFT_DROPは
MainWindow.cpp
1 2 3 4 5 6 7 8 9 |
LRESULT MainWindow::OnCreate(WPARAM wp, LPARAM lp) { hMainWnd = m_hWnd; SetTimer(hMainWnd, TIMER_FREE_FALL, 1000, NULL); SetTimer(hMainWnd, TIMER_SOFT_DROP, 50, NULL); return 0L; } |
OnDestroy関数は前回と同じです。
1 2 3 4 5 |
LRESULT MainWindow::OnDestroy(WPARAM wp, LPARAM lp) { PostQuitMessage(0); return 0L; } |
OnKeyDown関数
OnKeyDown関数はキーが押されたときの処理をおこないます。Tetrisクラスのメンバー関数を呼び出してテトリミノを移動させる処理(←キーが押されたらMoveLeft関数を呼び出してテトリミノを左に移動させるなど)をおこないます。
↓キーが押されているときはm_IsSoftDropフラグをTRUEにして、キーが押されている間高速落下させます。ソフトドロップという名前なのに高速落下なの?と思うかもしれませんが、ハードドロップの場合は一瞬で下まで落ちます。ハードドロップと比較すればソフトかも?
最後に再描画の処理をしています。その前にGetRect関数を実行していますが、これはInvalidateRect関数の第二引数をNULLにして全体を再描画しようとすると急速でテトリミノを移動させるときに画面がチカチカするので、再描画の範囲を限定するために使っています。とりあえず落下中のテトロミノとその周辺を再描画し、それ以外に必要なときはそのときでInvalidateRect関数を呼びます。
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 |
LRESULT MainWindow::OnKeyDown(WPARAM wp, LPARAM lp) { UINT vk = (UINT)(wp); // 移動の処理が実際におこなわれた場合だけ再描画の処理をする BOOL ret = FALSE; switch (vk) { case VK_UP: ret = m_Tetris.MoveUp(); break; case VK_DOWN: m_Tetris.m_IsSoftDrop = TRUE; // m_Tetris.MoveDown(); これだとひとつだけ下がるだけ。 // そうではなくキーが押されている間高速落下させる break; case VK_LEFT: ret = m_Tetris.MoveLeft(); break; case VK_RIGHT: ret = m_Tetris.MoveRight(); break; case 'Z': ret = m_Tetris.RotateLeft(); break; case 'X': ret = m_Tetris.RotateRight(); break; default: break; } if(ret) { RECT rect; m_Tetris.GetRect(&rect); InvalidateRect(m_hWnd, &rect, TRUE); } return 0L; } |
OnKeyUp関数
↓キーが離されたときにはm_IsSoftDropフラグをクリアするためにOnKeyUp関数内で処理をします。
1 2 3 4 5 6 7 8 9 10 11 12 |
LRESULT MainWindow::OnKeyUp(WPARAM wp, LPARAM lp) { UINT vk = (UINT)(wp); switch (vk) { case VK_DOWN: m_Tetris.m_IsSoftDrop = FALSE; break; default: break; } return 0L; } |
描画処理
描画処理はTetrisクラスのDraw関数を呼び出しておこないます。
1 2 3 4 5 6 7 8 9 10 |
LRESULT MainWindow::OnPaint(WPARAM wp, LPARAM lp) { PAINTSTRUCT ps; HDC hdc = BeginPaint(m_hWnd, &ps); m_Tetris.Draw(hdc); EndPaint(m_hWnd, &ps); return 0L; } |
タイマーの処理
タイマーがセットされているときはOnTimer関数が呼び出されます。どのタイマーからのメッセージなのかを判断(WPARAMでわかる)してテトリミノを下に移動させたり消えたラインを詰める処理をしています。
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 |
LRESULT MainWindow::OnTimer(WPARAM wp, LPARAM lp) { // ゆっくり下に移動 if (wp == TIMER_FREE_FALL) { if (!m_Tetris.m_IsSoftDrop) { m_Tetris.MoveDown(); RECT rect; m_Tetris.GetRect(&rect); InvalidateRect(m_hWnd, &rect, TRUE); } } // 急速に下に移動 if (wp == TIMER_SOFT_DROP) { if(m_Tetris.m_IsSoftDrop) { m_Tetris.MoveDown(); RECT rect; m_Tetris.GetRect(&rect); InvalidateRect(m_hWnd, &rect, TRUE); } } // ラインが消えたあと空間をつめる if(wp == TIMER_DROP_LINES) { m_Tetris.DropLines(); InvalidateRect(m_hWnd, NULL, TRUE); } return 0L; } |
TetriminoPositionクラスとBlockクラス
描画処理をおこなう場合、どこに何色で描画するかを知る必要があります。これはBlock.hで定義されています。メンバ関数はないのでBlock.cppはありません。
TetriminoPositionクラスとBlockクラスの違いですが、落下中のテトロミノを構成するブロックの位置はTetriminoPositionクラスを使い、着地して固定されたブロックはBlockクラスで管理します。
Block.h
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 |
enum class Color { Aqua, // Iミノ(水色) Yellow, // Oミノ(黄色) Green, // Sミノ(緑) Red, // Zミノ(赤) Blue, // Jミノ(青) Orange, // Lミノ(オレンジ) Violet, // Tミノ(紫) Gray, // 外枠用 }; class TetriminoPosition { public: int m_column = 0; int m_row = 0; }; enum class TetriminoAngle { Angle0, Angle90, Angle180, Angle270, }; enum class TetriminoTypes { I, O, S, Z, J, L, T, }; |
Block.h
1 2 3 4 5 6 7 |
class Block { public: int m_column = 0; int m_row = 0; Color color = Color::Aqua; }; |
まだ肝心のTetrisクラスについてなにも説明できていませんが長くなるので(本当に長い……)次回にします。