目录
1. 列出需要的头文件,结构体,全局变量和函数
#include <Windows.h>
#include <time.h>
#include <stdio.h>
using namespace std;
#define ID_DOWN_TIME 1
#define GAME_OVER 0
//坐标点
struct MyPoint
{
int m_x;
int m_y;
}curBlock[4],nextBlock[4];
//方块状态(空白、下落、落地)
enum STATE { BLANK, FALLING_BLOCKS, GROUND_BLOCKS };
//HANDLE g_hStdOutput = 0;
HINSTANCE g_hIns = 0;
int g_nScore = 0;//记录分数
int nBlockIndex = 0;//标记当前方块
int nNextBlockIndex = 0;
int nBlockSize = 20;
const int WIN_WIDTH = 600; //窗口宽度
const int WIN_HEIGHT = 600; //窗口高度
const int GAME_BORDER_W = 400; //方块运动区域宽
const int GAME_BORDER_H = 600; //方块运动区域高
const int ROW_COUNT = 30; //纵坐标数量
const int COL_COUNT = 20; //横坐标数量
int table[ROW_COUNT][COL_COUNT] = { 0 };//方块运动区域
//用二位数组表示方块的七种类型
int blocks[7][4] =
{
1,3,5,7, //I字型
0,2,3,5, //Z 1型
1,3,4,5, //Z 2型
1,2,3,5, //T
0,2,4,5, //L
1,3,4,5, //J
0,1,2,3 //田
};
//窗口回调函数
HRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam);
void OnPaint(HDC hdc); //窗口绘制
void DrawBlock(HDC hdc); //绘制方块
void CreateNextNewBlock(); //创建下一个方块
void Drop(HWND hWnd); //方块下落
void OnTime(HWND hWnd); //控制方块下落时间
BOOL IsLanding(); //判断方块是否正在下落
void MoveLeftOrRight(int setoff); //左右移动方块
BOOL CheckBorder(); //检查边界
void Rotate(); //方块旋转
void ClearLine(HWND hWnd); //方块满一行时清除
void SetCurBlocksValue(); //随机产生一个方块类型
void ShowInfor(HDC hdc); //显示信息
2. 设计窗口和显示窗口
int CALLBACK WinMain(
_In_ HINSTANCE hIns,
_In_opt_ HINSTANCE hPreIns,
_In_ LPSTR lpCmdLine,
_In_ int nCmdShow)
{
//显示控制台黑窗口
//AllocConsole();
/*g_hStdOutput = GetStdHandle(STD_OUTPUT_HANDLE);
if (g_hStdOutput == INVALID_HANDLE_VALUE)
{
return -1;
}*/
g_hIns = hIns;//程序进程句柄
//设计窗口类
char szAppClassName[] = "Tetris";//窗口类名
WNDCLASS wc = { 0 }; //
wc.cbClsExtra = 0; //窗口类的附加内存相当于缓冲区,一般置为0
wc.cbWndExtra = 0; //窗口的附加内存相当于缓冲区,一般置为0
wc.hbrBackground = CreateSolidBrush(RGB(0, 255, 0));//创建一个固定笔刷来填充窗口背景色
wc.hCursor = NULL; //鼠标指针,NULL默认样式
wc.hIcon = NULL; //图标指针,NULL默认样式
wc.hInstance = hIns; //程序的实例句柄,程序的唯一标识
wc.lpfnWndProc = WndProc; //由系统调用的消息处理函数
wc.lpszClassName = szAppClassName; //窗口类名指针
wc.lpszMenuName = NULL; //窗口菜单,NULL默认没有
wc.style = CS_HREDRAW || CS_VREDRAW;//窗口显示样式
//注册窗口
RegisterClass(&wc);
//创建窗口
HWND hWnd = CreateWindowExA(0, szAppClassName, "俄罗斯方块", WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU, 100, 100, WIN_WIDTH, WIN_HEIGHT, NULL, 0, g_hIns, NULL);
//显示刷新窗口
ShowWindow(hWnd, SW_SHOW);
UpdateWindow(hWnd);
//消息
MSG uMsg = { 0 };
while(GetMessageW(&uMsg,NULL,0,0))
{
TranslateMessage(&uMsg);//翻译消息
DispatchMessageW(&uMsg);//分发消息
}
//CloseHandle(g_hStdOutput);
//CloseHandle(hWnd);
return 0;
}
3. 消息处理函数
HRESULT CALLBACK WndProc(HWND hWnd, UINT uMsg, WPARAM wParam, LPARAM lParam)
{
switch (uMsg)
{
case WM_USER://自定义消息
//游戏结束时关闭定时器,并发出提示游戏结束,关闭窗口进程
KillTimer(hWnd, ID_DOWN_TIME);//关闭定时器
MessageBox(NULL, "游戏结束", "提示:", MB_OK);
PostQuitMessage(0);
break;
case WM_TIMER://定时器消息
switch (wParam)
{
case ID_DOWN_TIME:
//控制方块下落速度
PostMessage(hWnd, WM_PAINT, 0, 0);
OnTime(hWnd);
break;
default:
break;
}
break;
case WM_CREATE://在显示窗口前加载部分游戏数据
CreateNextNewBlock();//显示下一个方块类型
SetCurBlocksValue();//将下一个方块类型变为当前下落方块
SetTimer(hWnd, ID_DOWN_TIME, 300, NULL);//启动定时器
//重新创建下一个方块,要求下一个方块类型与当前类型不同
do {
CreateNextNewBlock();
} while (nBlockIndex == nNextBlockIndex);
break;
case WM_DESTROY://结束进程
PostQuitMessage(0);
break;
case WM_CLOSE://同上
DestroyWindow(hWnd);
break;
case WM_KEYUP://按键消息
switch (wParam)
{
case VK_RIGHT:
{
int count = 0;
for (int i = 0; i < 4; i++)
{
if (curBlock[i].m_x + 1 >= COL_COUNT || table[curBlock[i].m_y][curBlock[i].m_x + 1] == GROUND_BLOCKS)
{
count++;
}
}
if (count > 0)
{
break;
}
else
{
MoveLeftOrRight(1);
}
}
break;
case VK_LEFT:
{
int count = 0;
for (int i = 0; i < 4; i++)
{
if (curBlock[i].m_x - 1 < 0 || table[curBlock[i].m_y][curBlock[i].m_x - 1] == GROUND_BLOCKS)
{
count++;
}
}
if (count > 0)
{
break;
}
else
{
MoveLeftOrRight(-1);
}
}
break;
case VK_DOWN:
PostMessage(hWnd, WM_TIMER, ID_DOWN_TIME, 0);
break;
case VK_UP:
Rotate();
break;
default:
break;
}
InvalidateRect(hWnd, NULL, TRUE);
PostMessage(hWnd, WM_PAINT, 0, 0);
break;
case WM_PAINT: //刷新窗口
{
PAINTSTRUCT ps = { 0 };
HDC hdc = BeginPaint(hWnd, &ps);
OnPaint(hdc);
EndPaint(hWnd, &ps);
ClearLine(hWnd);
}
break;
default:
break;
}
return DefWindowProc(hWnd, uMsg, wParam, lParam);
}
4. 实现俄罗斯方块的主要功能
4.1 绘制图形
在游戏运行时,每个图形都是个画个的,窗口画面会出现闪烁现象,消除这一现象,需要利用双缓冲技术。
在内存中先创建一个与窗口区域大小一致的对象,将图形先绘制在内存对象上,再将这个对象一次性拷贝到窗口上,让窗口画面变得完整。
void OnPaint(HDC hdc)
{
//双缓冲绘图(在内存中重新开辟一个与当前窗口大小一样的窗口,先将图形绘制在内存中的窗口上)
HDC hMemDC = CreateCompatibleDC(hdc);//创建一个内存DC
HBITMAP hBackBmp = CreateCompatibleBitmap(hdc, WIN_WIDTH, WIN_HEIGHT);//在这个内存DC上创建画布
HGDIOBJ hOldBit = SelectObject(hMemDC, hBackBmp);//将这画布交给系统来绘制图形
//将图形绘制在内存DC中
RECT rect = { 0,0,WIN_WIDTH,WIN_HEIGHT };
FillRect(hMemDC, &rect, (HBRUSH)GetStockObject(LTGRAY_BRUSH));
//打印出分数
SetBkMode(hMemDC, TRANSPARENT);
ShowInfor(hMemDC);
HFONT hFont = CreateFont(40, 0, 0, 0, 1000, 0, 0, 0, 0, 0, 0, 0, 0, NULL);//用于更改字体样式
HFONT hOldFont = (HFONT)SelectObject(hMemDC, hFont);
char szText[255] = { 0 };
sprintf(szText, "得分:%d", g_nScore);
TextOut(hMemDC, 420, 200, szText, strlen(szText));//文字输出位置
SelectObject(hMemDC, hOldFont);
DeleteObject(hFont);//删除掉创建出的字体样式
//显示出下一个方块
for (int i = 0; i < 4; i++)
{
int xx = (nextBlock[i].m_x + 24) * nBlockSize;
int yy = (nextBlock[i].m_y + 2) * nBlockSize;
Rectangle(hMemDC, xx, yy, xx + nBlockSize, yy + nBlockSize);
}
Rectangle(hMemDC,0,0,GAME_BORDER_W,GAME_BORDER_H);
DrawBlock(hMemDC);
//将绘制完的内存图像复制到窗口上
BitBlt(hdc, 0, 0, WIN_WIDTH, WIN_HEIGHT, hMemDC, 0, 0, SRCCOPY);
//删除内存DC和画布
SelectObject(hMemDC, hOldBit);
DeleteObject(hBackBmp);
DeleteDC(hMemDC);
DeleteObject(hBackBmp);
}
//绘制方块
void DrawBlock(HDC hdc)
{
//将下落的方块涂成红色,落地的方块涂成绿色
HBRUSH hRedBrush = CreateSolidBrush(RGB(255, 0, 0));
HBRUSH hGreenBrush = CreateSolidBrush(RGB(0, 255, 0));
HBRUSH hPreBrush;
for (int i = 0; i < ROW_COUNT; i++)
{
for (int j = 0; j < COL_COUNT; j++)
{
if (table[i][j] == FALLING_BLOCKS)
{
hPreBrush = (HBRUSH)SelectObject(hdc, hRedBrush);
Rectangle(hdc, j * nBlockSize + 1, i * nBlockSize + 1, j * nBlockSize + nBlockSize - 1, i * nBlockSize + nBlockSize - 1);
SelectObject(hdc, hPreBrush);
table[i][j] = BLANK;
}
else if (table[i][j] == GROUND_BLOCKS)
{
hPreBrush = (HBRUSH)SelectObject(hdc, hGreenBrush);
Rectangle(hdc, j * nBlockSize + 1, i * nBlockSize + 1, j * nBlockSize + nBlockSize - 1, i * nBlockSize + nBlockSize - 1);
SelectObject(hdc, hPreBrush);
}
}
}
DeleteObject(hGreenBrush);
DeleteObject(hRedBrush);
}
4.2 创建方块
七种方块类型直接用二位数组表示可以节省空间,避免每种类型都要用一个二维数组。
用纵坐标表示类型,用横坐标表示四个小块的位置。
void CreateNextNewBlock()
{
//随机产生方块
srand((UINT)time(NULL));
nNextBlockIndex = 1 + rand() % 7; //从1~7中类型中任意产生一个类型
int n = nNextBlockIndex - 1;
//方块类型保存在二维数组中
for (int i = 0; i < 4; i++)
{
nextBlock[i].m_x = blocks[n][i] % 2;//方块中每个小块x坐标
nextBlock[i].m_y = blocks[n][i] / 2;//方块中每个小块y坐标
}
// 1 0
// 1 0
// 1 1 -> { 0, 2, 4, 5 },一个一维数组表示一个方块类型
// 0 0
}
//给当前下落方块赋值
void SetCurBlocksValue()
{
//将提示的下一个方块类型标记给当前需要显示的方块
nBlockIndex = nNextBlockIndex;
for (int i = 0; i < 4; i++)
{
curBlock[i].m_x = nextBlock[i].m_x + 8;
curBlock[i].m_y = nextBlock[i].m_y;
table[curBlock[i].m_y][curBlock[i].m_x] = FALLING_BLOCKS;//在地图上标记方块
}
}
4.3 方块下落
在下落过程中要判断是否越界
//方块自由下落
void Drop(HWND hWnd)
{
//判断方块是在下落且没有碰到其他方块或者地面,方块下落+1
if (!IsLanding())
{
for (int i = 3; i >= 0; i--)
{
curBlock[i].m_y += 1;
table[curBlock[i].m_y][curBlock[i].m_x] = FALLING_BLOCKS;
}
}
else
{
//将方块标记已落地
for (int i = 3; i >= 0; i--)
{
table[curBlock[i].m_y + 1][curBlock[i].m_x] = GROUND_BLOCKS;
}
SetCurBlocksValue();
//判断方块是否碰到顶部,碰到顶部不产生新方块游戏结束
if (table[2][8] != GROUND_BLOCKS && table[2][9] != GROUND_BLOCKS)
{
CreateNextNewBlock();
}
else
{
SendMessage(hWnd, WM_USER, GAME_OVER, NULL);
}
}
}
//刷新方块位置
void OnTime(HWND hWnd)
{
Drop(hWnd);
InvalidateRect(hWnd,NULL,TRUE);
}
//判断方块下落是否到底或者是否接触其他方块
BOOL IsLanding()
{
int count = 0;
//判断方块的四个小块是否有接触,如果有一个接触就返回true
for (int i = 3; i >= 0; i--)
{
if (curBlock[i].m_y + 4 >= ROW_COUNT || table[curBlock[i].m_y + 2][curBlock[i].m_x] == GROUND_BLOCKS)
{
count++;
}
}
if (count > 0)
{
return true;
}
return false;
}
4.4 方块左右移动
//方块左右移动
void MoveLeftOrRight(int setoff)
{
for (int i = 0; i < 4; i++)
{
curBlock[i].m_x += setoff;
table[curBlock[i].m_y][curBlock[i].m_x] = FALLING_BLOCKS;
}
}
4.5 旋转方块
利用数学公式:在坐标中(x,y)围绕某点旋转90°。
//旋转方块
void Rotate()
{
if (nBlockIndex == 7)//表示田字型方块不用旋转
{
return;
}
MyPoint bakBlock[4] = { 0 };
for (int i = 0; i < 4; i++)
{
bakBlock[i] = curBlock[i];
}
MyPoint midPoint = curBlock[1];
for (int i = 3; i >= 0; i--)
{
//根据坐标旋转公式将方块顺时针旋转
MyPoint tempPoint = curBlock[i];
curBlock[i].m_x = midPoint.m_x - tempPoint.m_y + midPoint.m_y;
curBlock[i].m_y = tempPoint.m_x - midPoint.m_x + midPoint.m_y;
table[curBlock[i].m_y][curBlock[i].m_x] = FALLING_BLOCKS;
}
//旋转时检查边界,不满足时返回原样
if (CheckBorder())
{
for (int i = 3; i >= 0; i-- )
{
table[curBlock[i].m_y][curBlock[i].m_x] = BLANK;
curBlock[i] = bakBlock[i];
table[curBlock[i].m_y][curBlock[i].m_x] = FALLING_BLOCKS;
}
}
}
//检测是否超界
BOOL CheckBorder()
{
int count = 0;
for (int i = 0; i < 4; i++)
{
if (curBlock[i].m_x < 0 || curBlock[i].m_x >= COL_COUNT
|| curBlock[i].m_y >= ROW_COUNT
|| table[curBlock[i].m_y][curBlock[i].m_x] == GROUND_BLOCKS)
{
count++;
}
}
if (count > 0)
{
return true;
}
return false;
}
4.6 清除满行方块
将方块区域从下到上遍历每行,如果满行标记为k,将k的上一行移至k行上,进行消除满行过程。
在将未满行逐行下移。
//某行上方块满时进行清除
void ClearLine(HWND hWnd)
{
int k = ROW_COUNT - 1;//标记k行满时,将它上一行位置移动到k行
//由下往上判断是否满足条件
for (int i = ROW_COUNT - 1; i >= 0; i--)
{
int count = 0;
for (int j = 0; j < COL_COUNT; j++)
{
if (table[i][j] == GROUND_BLOCKS)
{
count++;
}
table[k][j] = table[i][j];//将i行上的方块移到k行
if (table[0][j] == GROUND_BLOCKS)
{
SendMessage(hWnd, WM_USER, GAME_OVER, NULL);
}
}
//当k行方块已满,k留在原行上,不满时,k移至上一行
if (count < COL_COUNT)
{
k--;
}
//删掉一行分数+10
if (count == COL_COUNT)
{
g_nScore += 10;
}
}
}
5. 显示信息
//显示信息
void ShowInfor(HDC hdc)
{
char szInfor[100] = { 0 };
SetBkMode(hdc, TRANSPARENT);
HFONT hFont = CreateFont(20, 0, 0, 0, 600, 0, 0, 0, 0, 0, 0, 0, 0, NULL);
HFONT hOldFont = (HFONT)SelectObject(hdc, hFont);
sprintf(szInfor, "↑ 旋转方块");
TextOut(hdc, 420, 300, szInfor, strlen(szInfor));
sprintf(szInfor, "← 向左移动");
TextOut(hdc, 420, 350, szInfor, strlen(szInfor));
sprintf(szInfor, "→ 向右移动");
TextOut(hdc, 420, 400, szInfor, strlen(szInfor));
sprintf(szInfor, "↓ 向下加速");
TextOut(hdc, 420, 450, szInfor, strlen(szInfor));
SelectObject(hdc, hOldFont);
DeleteObject(hFont);
}