Làm Game Tetris Với C++ Siêu Đơn Giản (Phần 1)
Trong chuỗi bài này, mình sẽ hướng dẫn các bạn tự tạo game xếp gạch Tetris trong console bằng C++ cực kỳ đơn giản, không cần biết nhiều kiến thức nâng cao, phức tạp.
Tetris (Tiếng Nga là Тетрис) là một tile-matching video game được tạo ra bởi kỹ sư phần mềm người Nga Alexey Pajitnov năm 1984. Game đã được phát hành bởi nhiều công ty và trở thành một game rất nổi tiếng thời bấy giờ. Trong game, những khối gạch (được gọi là tetromino) hình dạng khác nhau rơi xuống một ma trận 20x10. Nhiệm vụ của bạn là xoay, di chuyển các khối gạch đó sao cho chúng lấp đầy các hàng và ghi điểm …
FunFact: Cái tên Tetris được Pajitnov kết hợp từ tiền tố “tetra-“ của tiếng Hy Lạp, có nghĩa là “bốn” (mỗi khối gạch Tetromino có 4 phần) và tennis môn thể thao ông thích nhất.
Let's dig in this legend game!
1. Về Tetris
Game Board
Hay còn được gọi là Play Field, Game Board thường là một ma trận 20 hàng và 10 cột. Đây là nơi chứa các khối gạch rơi xuống.
Tetromino
Là những khối hình thù quái dị từ trên trời rơi xuống. Có 7 loại tất cả: khối chữ L, J, O, T, S, Z, và I. Mỗi loại khối có màu sắc tương ứng khác nhau.
Các khối này đều có thể bị xoay (theo chiều kim đồng hồ) cũng như di chuyển (sang trái hoặc phải), đương nhiên là nếu không có vật cản.
Game Tick
Là khoảng thời gian để khói tetromino rơi xuống thêm một ô.
Sau khi khối hiện tại rơi xuống tận cùng, chạm đáy hoặc các khối đã hạ cánh, nó sẽ bị gắn lại và một khối khác sẽ rơi xuống từ chính giữa của cạnh trên Game Board.
Ăn điểm
Khi một hàng bị lấp đầy, bạn sẽ được ăn điểm. Số hàng hoàn thành cùng lúc càng nhiều (tối đa 4 hàng), số điểm tăng thêm càng nhiều. Đồng thời, các hàng đã được lấp đầy sẽ biến mất, làm các khối ô bên trên rơi xuống.
Game Over
Trò chơi kết thúc khi lượng khối đã rơi xuống chồng chất lên đến mức chạm vào cạnh trên cùng của Game Board, và khối mới không thể rơi xuống được nữa.
2. Ý tưởng
Về gameplay cơ bản cũng đã nói ở trên, các bạn cũng có thể sáng tạo thêm.
Còn về giao diện, thường thì cũng sẽ có Game Board ở bên trái rồi các thông tin về điểm, số hàng, khối tiếp theo ở bên phải. Kiểu:
Vẽ xấu một tí thôi, còn lại hoàn hảo =)
3. Định dạng Console
Kích thước
Trong Console các ô ký tự có chiều cao gấp đôi chiều rộng chính xác là 16
và 8 px
. Do đó, để có một Game Board vuông vắn, mỗi ô trong ma trận sẽ gồm 2 ô ký tự kề nhau.
Theo đó, Game Board sẽ có kích thước: chiều rộng 22
ô (10 * 2 + 2), chiều cao 21
ô (20 + 1); Score-Box và Line-Box: chiều rộng 17 ô (cái này là tùy ý, mình thấy 17 là phù hợp với giao diện của mình), chiều cao 3 ô. Next-Box: chiều rộng bằng Score-Box và Line-Box, 17 ô, chiều cao 6 ô.
Như vậy, kích thước cửa số console: chiều rộng 22 + 17 = 39, chiều cao 21.
Khởi tạo các hằng số kích thước
const int nScreenWidth = 39;
const int nScreenHeight = 21;
const int nBoardWidth = 22;
const int nBoardHeight = 21;
Set kích thước cửa sổ console
system("MODE 39, 22");
*Không hiểu câu lệnh này bị sao, chiều rộng thì giữ nguyên, còn chiều cao lại phải cộng thêm 1.
4. Thiết lập bảng màu
Bảng màu console mặc định có 16 màu là:
0 = Black |
8 = Gray |
1 = Blue |
9 = Light Blue |
2 = Green |
10 = Light Green |
3 = Aqua |
11 = Light Aqua |
4 = Red |
12 = Light Red |
5 = Purple |
13 = Light Purple |
6 = Yellow |
14 = Light Yellow |
7 = White |
15 = Bright White |
Thế này không đủ dùng. Rất may là chúng ta vẫn có thể thay đổi được.
Cú pháp:
BOOL WINAPI SetConsoleScreenBufferInfoEx(
_In_ HANDLE hConsoleOutput,
_In_ PCONSOLE_SCREEN_BUFFER_INFOEX lpConsoleScreenBufferInfoEx
);
hConsoleOutput
Một handle đến console screen buffer. Handle phải có quyền truy cập GENERIC_WRITE
.
lpConsoleScreenBufferInfoEx
Một cấu trúc CONSOLE_SCREEN_BUFFER_INFOEX
chức thông tin của console screen buffer:
typedef struct _CONSOLE_SCREEN_BUFFER_INFOEX {
ULONG cbSize;
COORD dwSize;
COORD dwCursorPosition;
WORD wAttributes;
SMALL_RECT srWindow;
COORD dwMaximumWindowSize;
WORD wPopupAttributes;
BOOL bFullscreenSupported;
COLORREF ColorTable[16];
} CONSOLE_SCREEN_BUFFER_INFOEX, *PCONSOLE_SCREEN_BUFFER_INFOEX;
Với mục đích thay đổi bảng màu, ta chỉ quan tâm đến ColorTable[16]
.
Mình đã chọn ra một số màu như sau:
Áp dụng:
HANDLE hConsoleOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_SCREEN_BUFFER_INFOEX csbiex;
csbiex.cbSize = sizeof(CONSOLE_SCREEN_BUFFER_INFOEX);
GetConsoleScreenBufferInfoEx(hConsoleOutput, &csbiex);
csbiex.ColorTable[0] = RGB(0, 188, 212);
csbiex.ColorTable[1] = RGB(63, 81, 181);
csbiex.ColorTable[2] = RGB(255, 87, 34);
csbiex.ColorTable[3] = RGB(255, 235, 59);
csbiex.ColorTable[4] = RGB(76, 175, 80);
csbiex.ColorTable[5] = RGB(156, 39, 176);
csbiex.ColorTable[6] = RGB(237, 28, 36);
csbiex.ColorTable[7] = RGB(242, 242, 242);
csbiex.ColorTable[8] = RGB(248, 248, 248);
csbiex.ColorTable[9] = RGB(20, 20, 20);
SetConsoleScreenBufferInfoEx(hConsoleOutput, &csbiex);
Game của mình chỉ sử dụng 10 màu này nên mình bỏ qua 6 màu còn lại.
Lưu ý: Phải #include <Windows.h>
nha các bạn.
5. Xây dựng giao diện
Tạo Screen Buffer
Trên Youtube, có nhiều người làm kiểu sau 1 game tick lại xóa màn hình rồi hiển thị lại. Mình thấy như thế game cứ giật giật nhìn rất khó chịu, còn dùng screen buffer này mình chưa thấy bị giật.
HANDLE hConsole = CreateConsoleScreenBuffer(GENERIC_READ | GENERIC_WRITE, 0, NULL, CONSOLE_TEXTMODE_BUFFER, NULL);
SetConsoleActiveScreenBuffer(hConsole);
Khi đã tạo screen buffer, mọi thay đổi sẽ thực hiện thông qua các mảng con trỏ 1 chiều và được hiển thị thông qua hàm WriteConsoleOutputCharacter()
và WriteConsoleOutputAttribute()
.
Tạo các mảng con trỏ
Hàm WriteConsoleOutputCharacter()
hiển thị Character, còn WriteConsoleOutputAttribute()
hiển thị Attribute - màu sắc, Vậy nên ta cần có 2 mảng:
WORD* pColor = new WORD[nScreenWidth * nScreenHeight];
wchar_t* pBuffer = new wchar_t[nScreenWidth * nScreenHeight];
for (int i = 0; i < nScreenWidth; i++)
{
for (int j = 0; j < nScreenHeight; j++)
{
pBuffer[j * nScreenWidth + i] = L' ';
if (i == 0 || i >= nBoardWidth - 1 || j == nBoardHeight - 1)
{
pColor[j * nScreenWidth + i] = 8 * 16 + 9;
}
else
{
if (j % 2 == 1)
{
if (i % 4 == 1 || i % 4 == 2)
{
pColor[j * nScreenWidth + i] = 8 * 16 + 9;
}
else
{
pColor[j * nScreenWidth + i] = 7 * 16 + 9;
}
}
else
{
if (i % 4 == 3 || i % 4 == 0)
{
pColor[j * nScreenWidth + i] = 8 * 16 + 9;
}
else
{
pColor[j * nScreenWidth + i] = 7 * 16 + 9;
}
}
}
}
}
Đoạn code bên trên, toàn bộ pBuffer
được set bằng các khoảng trống.
pColor
, trong khung vực Game Board sẽ có 2 màu nền xen kẽ tạo thành một ma trận rõ ràng, còn lại sẽ được set bằng 1 màu.
Hiển thị
BOOL WINAPI WriteConsoleOutputCharacter(
_In_ HANDLE hConsoleOutput,
_In_ LPCTSTR lpCharacter,
_In_ DWORD nLength,
_In_ COORD dwWriteCoord,
_Out_ LPDWORD lpNumberOfCharsWritten
);
hConsoleOutput
Một handle tới console screen buffer. Handle này phải có quyền truy cập GENERIC_WRITE
.
lpCharacter
Các ký tự được write lên console screen buffer.
nLength
Số ký tự được write.
dwWriteCoord
Một cấu trúc COORD
chỉ định tọa độ của ký tự đầu tiên được write lên.
lpNumberOfCharsWritten
Một con trỏ đến một biến nhận số ký tự thực sự được write.
BOOL WINAPI WriteConsoleOutputAttribute(
_In_ HANDLE hConsoleOutput,
_In_ const WORD *lpAttribute,
_In_ DWORD nLength,
_In_ COORD dwWriteCoord,
_Out_ LPDWORD lpNumberOfAttrsWritten
);
hConsoleOutput
Một handle tới console screen buffer. Handle này phải có quyền truy cập GENERIC_WRITE
.
lpAttribute
Các thuộc tính được sử dụng khi write lên console screen buffer.
nLength
Số ô ký tự của console screen buffer thuộc tính sẽ được sao chép.
dwWriteCoord
Một cấu trúc COORD
chỉ định tọa độ của ký tự đầu tiên thuộc tính sẽ được write lên.
lpNumberOfAttrsWritten
Một con trỏ tới một biến nhận số thuộc tính thực sự được write.
Sử dụng:
DWORD dwBytesWritten = 0;
for (int i = 0; i < nScreenWidth; i++)
{
for (int j = 0; j < nScreenHeight; j++)
{
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);
Kết quả:
Tạo Board Game
Khởi tạo các chi tiết:
const wstring detail = L" █▓░╚╝║═";
Khởi tạo mảng chứa thông tin Boad Game:
int* pMatrix = new int[nBoardWidth * nBoardHeight];
for (int i = 0; i < nBoardWidth; i++)
{
for (int j = 0; j < nBoardHeight; j++)
{
if (j == nBoardHeight - 1)
{
if (i == 0)
{
pMatrix[j * nBoardWidth + i] = 4;
}
else if (i == nBoardWidth - 1)
{
pMatrix[j * nBoardWidth + i] = 5;
}
else
{
pMatrix[j * nBoardWidth + i] = 7;
}
}
else
{
if (i == 0 || i == nBoardWidth - 1)
{
pMatrix[j * nBoardWidth + i] = 6;
}
else
{
pMatrix[j * nBoardWidth + i] = 0;
}
}
}
}
pMatrix
sẽ đánh dấu thành bao quanh Board Game và các khối teterimo đã hạ cánh. Đoạn code trên, các vị trí trong pMatrix
đánh dấu bởi các số ứng với chỉ số trong detail
.
Chuyển vào pBuffer
để hiển thị:
for (int i = 0; i < nBoardWidth; i++)
{
for (int j = 0; j < nBoardHeight; j++)
{
pBuffer[j * nScreenWidth + i] = detail[pMatrix[j * nBoardWidth + i]];
}
}
Vẽ các ô Score, Line, Next
Xây dựng hàm vẽ khung:
void Frame(wchar_t*& wcBuffer, wstring wsCaption, int nWidth, int nHeight, int nPosX, int nPosY)
{
for (int i = nPosX; i < nWidth + nPosX; i++)
{
for (int j = nPosY; j < nHeight + nPosY; j++)
{
if (i == nPosX)
{
if (j == nPosY)
{
wcBuffer[j * nScreenWidth + i] = L'╔';
}
else if (j == nHeight + nPosY - 1)
{
wcBuffer[j * nScreenWidth + i] = L'╚';
}
else
{
wcBuffer[j * nScreenWidth + i] = L'║';
}
}
else if (i == nWidth + nPosX - 1)
{
if (j == nPosY)
{
wcBuffer[j * nScreenWidth + i] = L'╗';
}
else if (j == nHeight + nPosY - 1)
{
wcBuffer[j * nScreenWidth + i] = L'╝';
}
else
{
wcBuffer[j * nScreenWidth + i] = L'║';
}
}
else
{
if (j == nPosY || j == nHeight + nPosY - 1)
{
wcBuffer[j * nScreenWidth + i] = L'═';
}
else
{
wcBuffer[j * nScreenWidth + i] = L' ';
}
}
}
}
int CapIndex = nPosY * nScreenWidth + (nPosX + 1);
for (int i = 0; i < wsCaption.length(); i++, CapIndex++)
{
wcBuffer[CapIndex] = wsCaption.at(i);
}
}
Vẽ:
Frame(pBuffer, L"[ SCORE ]", 17, 3, 22, 1);
Frame(pBuffer, L"[ LINE ]", 17, 3, 22, 4);
Frame(pBuffer, L"[ NEXT ]", 17, 6, 22, 7);
Kết quả:
Perfect!
Tạm kết
Bài viết này, mình đã hướng dẫn các bạn xây dựng giao diện game Tetris, các bạn có thể xem source code tại đây. Bài viết tiếp theo mình sẽ hướng dẫn các bạn xây dựng Gameplay. Hãy vote 5*, để lại ý kiến của các bạn bên dưới phần comment và chia sẻ bài viết nhé. Cảm các bạn!