Hoàn Thiện Giao Diện Và Gameplay Của Tetris (Phần 3)
Welcome back !
Tiếp tục series Làm Game Tetris Với C++ Siêu đơn giản, trong bài viết này, mình sẽ cùng các bạn hoàn thiện giao diện và gameplay của Tetris.
Source code bài trước, xem tại đây.
1. Tăng kích thước cửa sổ game
Kích thước hiện tại hơi nhỏ, hãy tăng lên nào.
Screen buffer phụ thuộc window size, nên ta sẽ không thay đổi nó, vì nếu vậy sẽ phải sửa lại nhiều chỗ. Thay vào đó, ta sẽ thay đổi font size.
Hàm Configure()
chứa các cấu hình định dạng console, thêm vào nó:
CONSOLE_FONT_INFOEX cfiex;
cfiex.cbSize = sizeof(CONSOLE_FONT_INFOEX);
GetCurrentConsoleFontEx(hConsoleOutput, 0, &cfiex);
cfiex.dwFontSize.Y = 36;
SetCurrentConsoleFontEx(hConsoleOutput, 0, &cfiex);
* Font size mặc định của Console là 16. Mình thay đổi thành 36.
* Truy cập Microsoft Docs để biết thêm thông tin về GetCurrentConsoleFontEx()
và SetCurrentConsoleFontEx()
.
Bây giờ, chúng ta đã có một cửa sổ game lớn hơn.
2. Tăng độ khó cho game
Để làm khó người chơi, ta có thể tăng tốc độ rơi của các khối Tetromino. Bài viết trước, mình có nói một Game Tick (thời gian để các khối Tetromino rơi xuống thêm 1 ô) ban đầu dài 1 giây. Về sau thời gian này sẽ giảm dần.
Đây là đoạn code bài trước:
bool bForceDown = 0;
int nFrame = 20;
int nFrameCount = 0;
Sleep(50);
nFrameCount++;
if (nFrameCount == nFrame)
{
bForceDown = 1;
}
else
{
bForceDown = 0;
}
Và lý do mình không Sleep(1000)
mà thay vào đó là 20 lần Sleep(50)
thứ nhất là để Input – điều khiển khối mượn mà hơn, thứ hai chính là để tăng độ khó.
Để giảm độ dài của mỗi Game Tick, ta giảm nFrame
. Lấy gì làm mốc để giảm? Số Tetromino đã hạ cánh. Các bạn có thể cho sau mỗi x khối thì nFrame--
. Hoặc để game thú vị hơn một tí, nFrame--
sau x khối, rồi 2 * x khối, 4 * x khối, …
Tuy nhiên phải có một giới hạn. Nhanh thôi, đừng nhanh quá !
int nPieceCount = 0;
int nLevelLimit = 2;
// Game loop
while (1)
{
// ...
if (bForceDown == 1)
{
// ...
// Check and decrease
if (nPieceCount == nLevelLimit)
{
if (nFrame > 10)
{
nPieceCount = 0;
nFrame--;
nLevelLimit *= 2;
}
}
if (CheckPiece(pMatrix, nCurrentPiece, nCurrentRotation, nCurrentX, nCurrentY + 1)) {...}
else
{
if (nCurrentY < nLimit) {...}
else
{
// Count
nPieceCount++;
// Fix the Tetromino
}
}
}
}
* nFrame
giảm từ 20 xuống 11, vậy ta có 10 level. Giới hạn của từng level lần lượt là 2, 4, 8, 16, 32, 64, 128, ..., 1024.
3. Get Ready
Hiện tại trò chơi của chúng ta, khi mở ra, khối Tetromino sẽ lập tức rơi xuống, như thế quá đột ngột. Ta cần đếm ngược vài giây để người chơi sẵn sàng.
Tạo các Word Art
const vector<wstring> wsThree = {
L"──▄",
L" ─█",
L"──▀"
};
const vector<wstring> wsTwo = {
L"──▄",
L"▄─▀",
L"▀──"
};
const vector<wstring> wsOne = {
L"─▄ ",
L" █ ",
L" ▀ "
};
const vector<wstring> wsReady = {
L"▄──┐ ▄── ┌──▄ ▄──┐ ▄ ┬",
L"█─┬┘ █─ ├──█ █ ┌┘ ▀▄┘",
L"▀ └─ ▀── ┴ ▀ ▀─┘ ▀ "
};
const vector<vector<wstring>> wsCountDown = { wsThree, wsTwo, wsOne, wsReady };
* Các Text Art này các bạn có thể tự tạo bằng các ký tự hoặc có thể tạo nó trên trang TAAG (Text to ASCII Art Generator).
Clear Screen
for (int i = 0; i < nScreenWidth; i++)
{
for (int j = 0; j < nScreenHeight; j++)
{
pBuffer[j * nScreenWidth + i] = L' ';
pColor[j * nScreenWidth + i] = 8 * 16 + 9;
}
}
Cải tiến hàm Text()
Hàm Text()
ở bài trước chỉ có ký tự, bây giờ ta thêm màu sắc nữa. Như vậy, ta có thể linh hoạt hơn trong việc sử dụng nó.
void Text(wchar_t*& pBuffer, WORD*& pColor, wstring wsContent, WORD wColor, int nPosX, int nPosY)
{
for (int i = 0; i < wsContent.length(); i++, nPosX++)
{
pBuffer[nPosY * nScreenWidth + nPosX] = wsContent.at(i);
pColor[nPosY * nScreenWidth + nPosX] = wColor;
}
}
Đếm ngược
Cấu trúc vòng lặp đếm ngược:
for (int i = 0; i < wsCountDown.size(); i++)
{
// Take Text Art into buffer
// Display
// Delay
}
Đưa Text Art vào buffer
Vị trí: trung tâm màn hình game.
Màu sắc: xanh lá (màu của khối S).
for (int j = 0; j < wsCountDown.at(i).size(); j++)
{
if (i == 3)
{
Text(pBuffer, pColor, wsCountDown.at(i).at(j), 8 * 16 + 4, 9, 9 + j);
}
else
{
Text(pBuffer, pColor, wsCountDown.at(i).at(j), 8 * 16 + 4, 18, 9 + j);
}
}
Hiển thị
WriteConsoleOutputAttribute()
và WriteConsoleOutputCharacter()
thôi.
for (int j = 0; j < nScreenHeight; j++)
{
for (int i = 0; i < nScreenWidth; i++)
{
COORD cPos;
cPos.X = i;
cPos.Y = j;
WriteConsoleOutputAttribute(hConsole, &pColor[j * nScreenWidth + i], 1, cPos, &dwBytesWritten);
}
}
WriteConsoleOutputCharacter(hConsole, pBuffer, nScreenWidth * nScreenHeight, { 0,0 }, &dwBytesWritten);
Delay
1 giây
Sleep(1000);
Kết quả
3. Làm lại Game Over
Ở bài viết trước, mình chỉ mới cout
thông báo Game Over và điểm người chơi đạt được. Giờ ta hãy làm lại nó cho hoành tráng :D
Sử dụng 1 vòng while
:
// Text Art
// Clear screen
while (1)
{
// Take Text Art and score into buffer
// Create buttons and selections
// Display
}
Bổ sung bảng màu
Màn hình Game Over, mình muốn tối màu một chút, do đó mình cần bổ sung các màu mới.
Thêm vào trong hàm Configure()
:
csbiex.ColorTable[10] = RGB(50, 44, 46);
csbiex.ColorTable[11] = RGB(225, 225, 225);
2 màu vừa thêm vào:
Text Art "Game Over"
const vector<wstring> wsGameOver = {
L"▄──┐┌──▄ ┌─▄─▄ ▄──",
L"█ ─┐├──█ │ ▀ █ █─ ",
L"▀──┘┴ ▀ ┴ ▀ ▀──",
L"▄──┐ ▄ ┬ ▄── ▄──┐",
L"█ │ █ ┌┘ █─ █─┬┘",
L"▀──┘ ▀─┘ ▀── ▀ └─"
};
Clear Screen
Cũng giống như trên, nhưng khác màu.
for (int i = 0; i < nScreenWidth; i++)
{
for (int j = 0; j < nScreenHeight; j++)
{
pBuffer[j * nScreenWidth + i] = L' ';
pColor[j * nScreenWidth + i] = 10 * 16 + 11;
}
}
Đưa Text Art và điểm vào buffer
Text Art:
for (int i = 0; i < wsGameOver.size(); i++)
{
Text(pBuffer, pColor, wsGameOver.at(i), 10 * 16 + 1, 10, 3 + i);
}
Điểm:
Text(pBuffer, pColor, L"════ SCORE ════", 10 * 16 + 3, 12, 11);
Text(pBuffer, pColor, L"═══════════════", 10 * 16 + 3, 12, 13);
int nScorePosX = 19;
int nScoreComp = 100;
while (nScore >= nScoreComp)
{
nScorePosX--;
nScoreComp *= 100;
}
Text(pBuffer, pColor, to_wstring(nScore), 10 * 16 + 3, nScorePosX, 12);
Button
Gồm có Play Again và Quit.
Trước tiên, ta tạo một biến lưu lựa chọn (bên ngoài while
):
int nSelect = 0;
Khi nSelect == 0
, thì button được lựa chọn Play Again. Ngược lại nSelect == 1
, button được chọn là Quit. Khi ở lựa chọn 0, nếu người dùng nhấn phím S thì chuyển xuống lựa chọn 1, ngược lại với lựa chọn 1, khi người dùng nhấn W sẽ chuyển lên lựa chọn trên. Nhấn Enter thì lựa chọn được thực hiện.
if (nSelect == 0)
{
Text(pBuffer, pColor, L">> Play Again <<", 10 * 16 + 4, 11, 17);
Text(pBuffer, pColor, L" Quit ", 10 * 16 + 11, 11, 18);
if (GetKeyState('S') & 0x8000)
{
nSelect++;
}
if (GetKeyState(13) & 0x8000)
{
break;
}
}
else
{
Text(pBuffer, pColor, L" Play Again ", 10 * 16 + 11, 11, 17);
Text(pBuffer, pColor, L">> Quit <<", 10 * 16 + 6, 11, 18);
if (GetKeyState('W') & 0x8000)
{
nSelect--;
}
if (GetKeyState(13) & 0x8000)
{
return 0;
}
}
* Get ready, Game loop và vòng lặp của Game over cùng nằm trong 1 vòng while
. Khi người dùng chọn Play Again, break
vòng lặp của Game Over, vòng lặp lớn bao bên ngoài sẽ chạy vòng mới Get ready và Game loop mới. Còn khi người dùng chọn Quit, return 0
thoát khỏi trò chơi.
Hiển thị
WriteConsoleOutputAttribute()
và WriteConsoleOutputCharacter()
again.
Kết quả:
4. Tạm dừng game
Người chơi đôi lúc không thể chơi xong 1 ván game đang dở. Như nhiều game khác, cần cho phép người dùng tạm dừng game.
Chỉnh sửa Input
Thêm 1 phím vào vector<char> key
. Mình chọn phím Esc mã ASCII là 27.
const vector<char> key = { 'W', 'A', 'S', 'D', 27 };
bool bKey[5];
Xử lý
Khi tạm dừng chúng ta sẽ hiện một thông báo game đang tạm dừng, và có 2 button lựa chọn tiếp tục chơi hoặc thoát.
Cấu trúc đoạn code như sau:
if (bKey[4] == 1)
{
// Create a new pColor array
while (1)
{
// Create the pause notice
// Create the buttons and selections
// Display
}
}
Tạo mảng màu mới
Việc này giúp pColor không bị tác đông. Khi tiếp tục chơi, màu sắc hiển thị sẽ được trả lại như trước khi tạm dừng.
WORD* pTmpColor = new WORD[nScreenWidth * nScreenHeight];
for (int i = 0; i < nScreenWidth * nScreenHeight; i++)
{
pTmpColor[i] = pColor[i];
}
* Sao không WORD* pTmpColor = pColor
cho nhanh? Cách này không giống cách trên. Khi tiếp tục chơi, nó cũng làm thay đổi màu.
Tạo thông báo tạm dừng
Text(pBuffer, pTmpColor, L" ═════ PAUSE ════ ", 10 * 16 + 11, 2, 8);
Text(pBuffer, pTmpColor, L" ", 10 * 16 + 11, 2, 9);
Text(pBuffer, pTmpColor, L" ════════════════ ", 10 * 16 + 11, 2, 12);
Tạo các button và lựa chọn
Tương tự phần Game Over bên trên.
if (nSelect == 0)
{
Text(pBuffer, pTmpColor, L" >> Continue << ", 10 * 16 + 4, 2, 10);
Text(pBuffer, pTmpColor, L" Quit ", 10 * 16 + 11, 2, 11);
if (GetKeyState('S') & 0x8000)
{
nSelect++;
}
else if (GetKeyState(13) & 0x8000)
{
break;
}
}
else
{
Text(pBuffer, pTmpColor, L" Continue ", 10 * 16 + 11, 2, 10);
Text(pBuffer, pTmpColor, L" >> Quit << ", 10 * 16 + 6, 2, 11);
if (GetKeyState('W') & 0x8000)
{
nSelect--;
}
else if (GetKeyState(13) & 0x8000)
{
return 0;
}
}
* Continue, break
while
loop, game tiếp tục. Quit, return
0, thoát khỏi trò chơi.
Hiển thị
Kết quả
Source code
Tetris của chúng ta có vẻ đã hoàn hảo, các bạn xem source code tại đây.
Bài viết tiếp theo và chắc là bài viết cuối của series, mình sẽ xây dựng một menu chính và lưu điểm cao.
Hãy rate 5*, share và để lại ý kiến của các bạn bên dưới phần bình luận nhé. Cảm ơn các bạn!
Hẹn các bạn ở bài viết sau.