俄罗斯方块-C语言-详注版
概述
本文详述了C语言版俄罗斯方块游戏的原理以及实现方法,对游戏代码进行了详细的分析和注释,通过本文能够让大家对WIN32编程框架有一个大致了解,对C语言运用有一定提高,同时也能给大家提供一个C语言小游戏编程的思路,也能完全够通过自己的实力去编写一个属于自己的游戏.
游戏体验
代码框架
俄罗斯方块游戏代码框架如下
在 main.c 中,创建应用窗口,并初始化一些系统资源,然后初始化gdi,初始化俄罗斯方块游戏.
在 gdi.c 中,对系统的HDC及绘图接口进行了一次封装,使用WIN32系统提供的绘图接口来实现我们自己的图形绘制API.这里使用到了双缓冲技术,下文会简单介绍.
在 Tetris.c 中,实现了俄罗斯方块的游戏逻辑,包括贴图系统,地图系统,方块生成,方块移动,方块变形,满行检测,等等.
下面将对代码进行详细的分析.
代码主函数
在 main.c 中,创建应用窗口,并初始化一些系统资源,然后初始化gdi,初始化俄罗斯方块游戏.
main.c
// GameTetris.c: 定义应用程序的入口点。
//
#include "Main.h"
/*......*/
int APIENTRY wWinMain(_In_ HINSTANCE hInstance,
_In_opt_ HINSTANCE hPrevInstance,
_In_ LPWSTR lpCmdLine,
_In_ int nCmdShow)
{
UNREFERENCED_PARAMETER(hPrevInstance);
UNREFERENCED_PARAMETER(lpCmdLine);
// TODO: 在此放置代码。
HACCEL hAccelTable;
MSG msg;
DWORD dwTetrisRunTId;
HANDLE hTetrisRunT;
// 初始化全局字符串
LoadStringW(hInstance, IDS_APP_TITLE, szTitle, MAX_LOADSTRING);
LoadStringW(hInstance, IDC_GAMETETRIS, szWindowClass, MAX_LOADSTRING);
MyRegisterClass(hInstance);
// 执行应用程序初始化:
if (!InitInstance(hInstance, nCmdShow))
{
return FALSE;
}
DebugOpen();
gdi_init(hWnd);//显示初始化
gdi_clear(GDI_RGB_BACKGROUND);//显示初始化
GameCtrlInit();
//GamePlaySound(IDR_WAVE_BACKG);
hTetrisRunT = CreateThread(NULL, 0, TetrisRun, NULL, 0, &dwTetrisRunTId);
//创建线程失败
if (hTetrisRunT == NULL)
{
ExitProcess(0);//主程序退出
}
//快捷键
hAccelTable = LoadAccelerators(hInstance, MAKEINTRESOURCE(IDC_GAMETETRIS));
// 主消息循环:
while (GetMessage(&msg, NULL, 0, 0))
{
if (!TranslateAccelerator(msg.hwnd, hAccelTable, &msg))
{
//TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return (int)msg.wParam;
}
//
// 函数: MyRegisterClass()
//
// 目的: 注册窗口类。
//
ATOM MyRegisterClass(HINSTANCE hInstance)
{
WNDCLASSEXW wcex;
HBRUSH hWindowBrush;
wcex.cbSize = sizeof(WNDCLASSEX);
wcex.style = CS_HREDRAW | CS_VREDRAW; //如果大小改变了重绘窗口
wcex.lpfnWndProc = WndProc; //窗口消息处理函数
wcex.cbClsExtra = 0; //无附加窗口类内存
wcex.cbWndExtra = 0; //无附加窗口内存
wcex.hInstance = hInstance; //应用程序实例
wcex.hIcon = LoadIcon(hInstance, MAKEINTRESOURCE(IDI_GAMETETRIS)); //图标
wcex.hCursor = LoadCursor(NULL, IDC_ARROW); //鼠标指针
#if 0
wcex.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1); //背景画刷(默认白色)
#else
hWindowBrush = CreateSolidBrush((COLORREF)GDI_RGB_BACKGROUND); //背景画刷(自定义)
wcex.hbrBackground = hWindowBrush;
#endif
wcex.lpszMenuName = MAKEINTRESOURCEW(IDC_GAMETETRIS); //菜单资源
wcex.lpszClassName = szWindowClass; //窗口类名
wcex.hIconSm = LoadIcon(wcex.hInstance, MAKEINTRESOURCE(IDI_SMALL)); //小图标
return RegisterClassExW(&wcex); //注册窗口类
}
//
// 函数: InitInstance(HINSTANCE, int)
//
// 目的: 保存实例句柄并创建主窗口
//
// 注释:
//
// 在此函数中,我们在全局变量中保存实例句柄并
// 创建和显示主程序窗口。
//
BOOL InitInstance(HINSTANCE hInstance, int nCmdShow)
{
/*......*/
/*HWND*/hWnd = CreateWindowW(
szWindowClass, //窗口类名
szTitle, //窗口名
/*WS_OVERLAPPEDWINDOW*/dwStyle, //窗口样式
CW_USEDEFAULT, //水平位置,默认
CW_USEDEFAULT, //垂直位置,默认
wndRect.right - wndRect.left, //宽
wndRect.bottom - wndRect.top, //高
NULL, //无父窗口
(HMENU)/*NULL*/LoadMenu(hInst, MAKEINTRESOURCE(IDC_GAMETETRIS)), //菜单
hInstance, //应用程序实例
NULL); //无窗口创建数据
if (!hWnd)
{
return FALSE;
}
ShowWindow(hWnd, nCmdShow);
UpdateWindow(hWnd);
return TRUE;
}
//
// 函数: WndProc(HWND, UINT, WPARAM, LPARAM)
//
// 目的: 处理主窗口的消息。
//
// WM_COMMAND - 处理应用程序菜单
// WM_PAINT - 绘制主窗口
// WM_DESTROY - 发送退出消息并返回
//
//
LRESULT CALLBACK WndProc(HWND hWnd, UINT message, WPARAM wParam, LPARAM lParam)
{
int wmId;
PAINTSTRUCT ps;
HDC hdc;
switch (message)
{
//case WM_CREATE:
// break;
case WM_COMMAND:
{
wmId = LOWORD(wParam);
// 分析菜单选择:
switch (wmId)
{
/*......*/
case IDM_ADDSPEED://加速
tetris_add_speed();
break;
case IDM_SUBSPEED://减速
tetris_sub_speed();
break;
case IDM_TETRISFIRE://变形
tetris_shape_deform();
break;
/*......*/
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
}
break;
case WM_PAINT:
{
hdc = BeginPaint(hWnd, &ps);
gdi_update();
EndPaint(hWnd, &ps);
}
break;
case WM_DESTROY:
gdi_dinit();
DebugClose();
PostQuitMessage(0);
break;
case WM_KEYDOWN:
switch (wParam)
{
/*......*/
case VK_DOWN://下
tetris_shape_move(DR_DOWN);
break;
case VK_LEFT://左
tetris_shape_move(DR_LEFT);
break;
case VK_RIGHT://右
tetris_shape_move(DR_RIGHT);
break;
case VK_RETURN://回车键变形
tetris_shape_deform();
break;
/*......*/
default:
break;
}
break;
//case WM_CHAR:
// break;
default:
return DefWindowProc(hWnd, message, wParam, lParam);
}
return 0;
}
// “关于”框的消息处理程序。
INT_PTR CALLBACK About(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
/*......*/
}
// “帮助”框的消息处理程序。
INT_PTR CALLBACK Readme(HWND hDlg, UINT message, WPARAM wParam, LPARAM lParam)
{
/*......*/
}
//游戏运行线程
DWORD WINAPI TetrisRun(LPVOID lpProgram)
{
tetris_game_init();
while (TRUE)
{
if (GAME_CTRL.run)//暂停游戏
{
if (tetris_game_run(GAME_CTRL.debug) != LF_LIVE)
{
//DEBUG_LOG("ERR");
MessageBox(/*NULL*/hWnd, TEXT("继续战斗吧!"), TEXT("你输了!"), MB_OK);
GameCtrlInit();
tetris_game_init();
}
Sleep(1100 - (tetris_get_speed() * 100));
}
}
return 0;
}
GDI绘图
在 gdi.c 中,对系统的HDC及绘图接口进行了一次封装,使用WIN32系统提供的绘图接口来实现我们自己的图形绘制API.这里使用到了双缓冲技术.
双缓冲技术
俄罗斯方块的每一次运行,坦克和炮弹的每一次移动,我们都要不断的清除屏幕上原来的内容,然后重新绘制新的内容,假如我们每一次更改显示内容都直接调用WIN32的api实时的刷新到屏幕上,一旦游戏运行速度很快的时候,就会造成屏幕上的内容一直在闪烁,非常影响游戏体验.
双缓冲技术主要是为了解决俄罗斯方块实时刷新造成的屏幕闪烁问题.其原理就是,加入地图上共有10辆坦在战斗,每辆坦克都在移动,每移动一步我们都需要重新计算并显示10辆坦克的位置,但是我们不必在计算每一辆坦克的时候都把他刷新到屏幕上,而是先把这辆坦克绘制到内存上,等到所有的坦克都计算并完成移动之后,再同一把内存上的内容刷新到屏幕上,这样做就大大减少了刷新屏幕的次数,也就可以避免实时刷新造成的屏幕闪烁问题.
更详细的介绍,请参考下面这篇文章:
gdi.c
#include "Gdi.h"
HPEN hGdiPen = NULL; //画笔
HBRUSH hGdiBrush = NULL; //画刷
HDC mGdiHdc; //内存设备(双缓冲技术)
HDC hGdiHdc; //硬件设备
HWND hGdiWnd; //窗口
RECT hGdiWndRect; //窗口客户区大小
HBITMAP mGdiBmp;
HBITMAP mGdiBmpOld;
#define maxX SCREEN_X
#define maxY SCREEN_Y
static void _gdi_clr_pencol(HPEN _hGdiPen)
{
DeleteObject(_hGdiPen);//释放资源
SelectObject(mGdiHdc, hGdiPen);//恢复初始画刷
}
static HPEN _gdi_set_pencol(int32 color)
{
HPEN _hGdiPen;
COLORREF color_t = (COLORREF)color;
_hGdiPen = CreatePen(PS_SOLID, 1, color_t);