翻看自己以前练习的小demo,忽然感觉代码写的一团糟,故今而重整一下代码,也算是温习win32。
90年后,是感受到互联网、it技术浪潮很深的一代,我等尤为其盛,更为游戏原理之巧妙、精湛而赞叹不已,因此便借温习之由,将笔者拙劣的实现抛砖引玉与诸君,同时希望为看到此篇文章小萌新的编程之路上添一块脚石。
win32不同于我们黑乎乎的console,在这里我们能创建动态的窗口,实现控件,更能自己做一个小游戏。
我们首先要知道的就是win32窗口的基本构造流程:这里我贴两个csdn的blog链接:
https://blog.csdn.net/zltpc007/article/details/1867671
https://blog.csdn.net/qq_36746738/article/details/72901339
我就不多做解释,主要是win32框架是基础的东西,用多了自然就懂了(本来笔者是想详细介绍win32框架,但又觉得不符合此篇文章的主题,且不熟悉win32框架的同学看此篇文章也是及其费劲,即便在此解释也难以接受实际的运用)
我们直接切入主题,这是程序运行时截图:
运行完截图:
我只简单做了一个界面切换。
首先大笔一挥:引入头文件。
然后定义好窗口大小,标题等。
#define WINDOW_HEIGHT 600
#define WINDOW_WIDTH 800
#define WINDOW_TITLE L"【游戏进度条的异步加载】"
然后就到了基础的部分:建造win32框架:
int WINAPI WinMain(HINSTANCE hInstance, HINSTANCE hPrevInstance, LPSTR lpCmdline, int nShowCmd)
{
WNDCLASS Wnd;
Wnd.cbClsExtra = NULL;
Wnd.cbWndExtra = NULL;
Wnd.hbrBackground = (HBRUSH)GetStockObject(GRAY_BRUSH);
Wnd.hCursor = NULL;
Wnd.hIcon = (HICON)::LoadImage(NULL, _T("icon.ico"), IMAGE_ICON, 0, 0, LR_DEFAULTSIZE | LR_LOADFROMFILE);
Wnd.hInstance = hInstance;
Wnd.lpfnWndProc = WndProc;
Wnd.lpszClassName = _T("demo");
Wnd.lpszMenuName = NULL;
Wnd.style = CS_HREDRAW | CS_VREDRAW;
if (!RegisterClass(&Wnd))
{
return -1;
}
HWND hWnd = CreateWindow(_T("demo"), WINDOW_TITLE,WS_OVERLAPPED | WS_MINIMIZEBOX | WS_SYSMENU,
200, 50, WINDOW_WIDTH, WINDOW_HEIGHT, NULL, NULL, hInstance, NULL);
ShowWindow(hWnd,nShowCmd);//SW_SHOWDEFAULT
UpdateWindow(hWnd);
MSG msg = { 0 };
while (msg.message!=WM_QUIT)
{
if (PeekMessage(&msg,NULL,NULL,NULL,PM_REMOVE))
{
TranslateMessage(&msg);
DispatchMessage(&msg);
}
}
return 0;
}
写好消息处理的回调函数:
回调函数参见:https://blog.csdn.net/u014337397/article/details/80328277
LRESULT CALLBACK WndProc(HWND hwnd, UINT message, WPARAM wparam, LPARAM lparam)
{
switch (message)
{
case WM_DESTROY:
PostQuitMessage(0);
break;
default:
return DefWindowProc(hwnd, message, wparam, lparam);
}
return 0;
}
这样一个窗口就出来了,整体构架出来了,我们才好开始实行我们的建造,好添砖加瓦。
游戏实行的三要素是什么?
1:资源的加载
2:画面的绘制
3:对用户的响应(消息处理机制去运作)
故我们写下三个函数:
void Game_Init(HWND hWnd, HINSTANCE hInstance);
void Game_Update();
void Game_Render();
以及相对应的资源定义:设备环境句柄、画布句柄和当前GUI的名字。
HDC hdc, buffDC, hbDC, processDC, manDC;
HBITMAP buffBitmap, hb, processBitmap, manBitmap;
wchar_t GUI_Name[20];
HINSTANCE g_hInstance;
俗话说的好:一口吃成大胖子,故我们需将程序分成几部分,一步一步吃下去。
(1)
先给窗口画上背景:
1:资源初始化
void Game_Init(HWND hWnd, HINSTANCE hInstance)
{
wcscpy(GUI_Name, _T("Main_UI.bmp"));
hdc = GetDC(hWnd);
buffDC = CreateCompatibleDC(hdc);
buffBitmap = CreateCompatibleBitmap(hdc, 1024, 768);
SelectObject(buffDC, buffBitmap);
hb = (HBITMAP)LoadImage(hInstance, GUI_Name, IMAGE_BITMAP, 790, 600, LR_LOADFROMFILE);
hbDC = CreateCompatibleDC(hdc);//创建兼容DC
SelectObject(hbDC, hb);
processBitmap = (HBITMAP)LoadImage(hInstance, _T("进度条.bmp"), IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
processDC = CreateCompatibleDC(hdc);//创建兼容DC
SelectObject(processDC, processBitmap);
manBitmap = (HBITMAP)LoadImage(hInstance, _T("ShopNpc.bmp"), IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
manDC = CreateCompatibleDC(hdc);//创建兼容DC
SelectObject(manDC, manBitmap);
}
2:写好Game_Update()函数和 Game_Render()函数 。
void Game_Update()
{
}
void Game_Render()
{
BitBlt(hdc, 0, 0, 1024, 768, hbDC, 0, 0, SRCCOPY);
}
这样我们就把背景贴上去了。
接下来便是贴上一个会动的进度条,那么进度条是怎么动的呢?
有很多种实现办法,这里笔者列举两种:
一:运用修图工具,实现长度与背景中框槽一样大小的进度条,然后依照进度和长度相匹配来贴图。
二:从网上随便裁剪一段进度条,每次循环贴图。
这里笔者采用第一种方法,代码会简单一些,不会ps的游戏程序员不是好程序员。
代码实现也非常简单,加寥寥几笔就行。
void Game_Update()
{
}
void Game_Render()
{
static int length=0;
int speed=3;
BitBlt(hdc, 0, 0, 1024, 768, hbDC, 0, 0, SRCCOPY);
BitBlt(hdc, 20, 518, length, 18, processDC, 0, 0, SRCCOPY);
length+=speed;
}
BitBlt(hdc, 20, 518, speed, 18, processDC, 0, 0, SRCCOPY);
hdc:是当前主设备的句柄。
我们将processDC(画板)所关联的 processBitmap(画布)给了 hdc,即将进度条画了上去,画在hdc上x=20,y=518的位置上,画的大小是从原图的0,0,坐标开始截取length长,18宽度的图片,并以speed的速度前进。
ps:我们应先画背景,再画进度条。
因为调用Game_Render()很快,所以截取的图就是进度条直达边框,这显然是不符合我们游戏实际,所以上述代码要修改。
void Game_Render()
{
static int length = 0;
static float beginTime = timeGetTime() / 1000.0f;//得到系统毫秒时间除完后变成开始的计时的秒数
float endTime = timeGetTime() / 1000.0f;//得到新的秒数
int speed = 3;
BitBlt(hdc, 0, 0, 1024, 768, hbDC, 0, 0, SRCCOPY);
BitBlt(hdc, 20, 518, length, 18, processDC, 0, 0, SRCCOPY);
if (endTime - beginTime >= 0.1f)//时间相减大于一个时间差执行
{
if(length < 740)
length += speed;
beginTime = endTime;//重新设置开始时间
}
}
程序运行截图:
这样进度条就能以我们想要的速度动起来,并不会超出边框,但这样的进度条有点乏善可陈,我们决定在进度条前面加个小人一直跑呀跑,于是上网找资源,但往往会找到这样的图片:
这样的图片是怎样实现动态的呢?很简单,把每行的人物按照次序裁剪然后贴图就实现了;
但我们实际使用时发现白色的背景也贴了上去,而且会有闪屏出现。
这个时候就需使用TransparentBlt()这个函数以及缓冲体系。
其实很简单,不是有背景吗,那我就透明贴图,你闪屏是吗,那我就多加一层画板,画布,给你做缓冲。
于是代码就改成了这样(同学可以尝试用定时器实现相同功能):
void Game_Render()
{
static int i = 0;//动画帧设置
static int length = 0;
static float beginTime = timeGetTime() / 1000.0f;//得到系统毫秒时间除完后变成开始的计时的秒数
float endTime = timeGetTime() / 1000.0f;//得到新的秒数
int speed = 3;
BitBlt(buffDC, 0, 0, 1024, 768, hbDC, 0, 0, SRCCOPY);
BitBlt(buffDC, 20, 518, length, 18, processDC, 0, 0, SRCCOPY);
TransparentBlt(buffDC, length, 768, 40, 40, manDC, i * 90, 0, 90, 90, RGB(255, 255, 255));
BitBlt(hdc, 0, 0, 1024, 495, buffDC, 0, 0, SRCCOPY);
if (endTime - beginTime >= 0.1f)//时间相减大于一个时间差执行
{
if(length < 740)
length += speed;
++i;//人物动画帧更改
if (i >= 4)
i = 0;//超出图片帧范围,从0重新开始
beginTime = endTime;//重新设置开始时间
}
}
就是中间加了一层buffDC,然后再转入hdc(主设备环境句柄)。
TransparentBlt(buffDC, length, 495, 40, 40, manDC, i * 90, 0, 90, 90, RGB(255, 255, 255));
把从macDC中x= i * 90,y=0,大小为90x90的图片变成40x40的图片并贴入buffDC中x=length,y=495的位置。
程序运行截图如下:
这样我们进度条运行便没什么问题了,但这很明显是假进度条加载,我们很清楚的知道进度条是以speed的速度匀速运动,完全没有加载资源,那么怎么联系上了? (游戏加载界面的作用:游戏加载资源时,给用户一个良好的体验)。
同学们应该知道进程、线程,游戏中资源加载往往是多线程,
一个加载音乐资源,一个加载纹理资源,一个加载模型资源,等等。
下面我们便来实现多线程,在实现它之前,先定义一些状态变量:
enum Status
{
thread,
thread2,
thread3
};
Status t_status;//线程状态定义
enum State
{
Main_GUI,
Start_GUI
};
State state; //GUI界面定义
HANDLE hThread, hThread2, hThread3;//线程句柄定义
在 Game_Init(HWND hWnd, HINSTANCE hInstance)里面创造线程。
void Game_Init(HWND hWnd, HINSTANCE hInstance)
{
g_state = Main_GUI;//设置GUI。
t_status = thread;//设置第一个检查的线程编号为thread。
wcscpy(GUI_Name, _T("Main_UI.bmp"));//设置GUI背景图。
DWORD ThreadID;
hThread = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)threadLoadFun, (LPVOID)thread, 0, &ThreadID);//创建线程。
DWORD Thread2ID;
hThread2 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)threadLoadFun, (LPVOID)thread2, 0, &Thread2ID);
DWORD Thread3ID;
hThread3 = CreateThread(NULL, 0, (LPTHREAD_START_ROUTINE)threadLoadFun, (LPVOID)thread3, 0, &Thread3ID);
hdc = GetDC(hWnd);
buffDC = CreateCompatibleDC(hdc);
buffBitmap = CreateCompatibleBitmap(hdc, 1024, 768);
SelectObject(buffDC, buffBitmap);
hb = (HBITMAP)LoadImage(hInstance, GUI_Name, IMAGE_BITMAP, 790, 600, LR_LOADFROMFILE);
hbDC = CreateCompatibleDC(hdc);//创建兼容DC
SelectObject(hbDC, hb);
processBitmap = (HBITMAP)LoadImage(hInstance, _T("进度条.bmp"), IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
processDC = CreateCompatibleDC(hdc);//创建兼容DC
SelectObject(processDC, processBitmap);
manBitmap = (HBITMAP)LoadImage(hInstance, _T("ShopNpc.bmp"), IMAGE_BITMAP, 0, 0, LR_LOADFROMFILE);
manDC = CreateCompatibleDC(hdc);//创建兼容DC
SelectObject(manDC, manBitmap);
}
写好线程加载函数:
void threadLoadFun(LPVOID param)
{
switch (int(param))
{
case thread:
Sleep(5000);//加载资源函数,本例写成sleep模拟加载
break;
case thread2:
Sleep(9000);
break;
case thread3:
Sleep(15000);
break;
}
}
在Game_Update()里写好对应线程的程序语句:
void Game_Update()
{
switch (g_state)
{
case Main_GUI:
break;
case Start_GUI:
break;
}
}
在Game_Render()里写好对应线程的程序语句:
void Game_Render()
{
switch (g_state)
{
case Main_GUI:
Main_Draw();//将函数封装为Main_Draw
break;
case Start_GUI:
Start_Draw();
break;
}
}
在 Main_Draw()里写好对应线程的程序语句:
void Main_Draw()
{
static int i = 0;//动画帧设置
static int length = 0;
static float beginTime = timeGetTime() / 1000.0f;//得到系统毫秒时间除完后变成开始的计时的秒数
float endTime = timeGetTime() / 1000.0f;//得到新的秒数
BitBlt(buffDC, 0, 0, 1024, 768, hbDC, 0, 0, SRCCOPY);
BitBlt(buffDC, 20, 518, length, 18, processDC, 0, 0, SRCCOPY);
TransparentBlt(buffDC, length, 495, 40, 40, manDC, i * 90, 0, 90, 90, RGB(255, 255, 255));
BitBlt(hdc, 0, 0, 1024, 768, buffDC, 0, 0, SRCCOPY);
if (endTime - beginTime >= 0.1)//时间相减大于一个时间差执行
{
switch (t_status)
{
case thread:
if (length <=180)
{
if (isFinshed(hThread))
{
t_status = thread2;
length = 180;
}
if (length < 180)
{
length += 5;
}
}
break;
case thread2:
if (length <= 500)
{
if (length<500)
{
length += 8;
}
if (isFinshed(hThread2))
{
length = 500;
t_status = thread3;
}
}
break;
case thread3:
if (length <= 750)
{
if (length <750)
{
length += 10;
}
if (isFinshed(hThread3))
{
length = 750;
g_state = Start_GUI;
}
}
break;
}
++i;//人物动画帧更改
if (i >= 4)
i = 0;//超出图片帧范围,从0重新开始
beginTime = endTime;//重新设置开始时间
}
}
在 Start_Draw()里写好对应线程的程序语句:
void Start_Draw()
{
if (wcscmp(GUI_Name, L"Start_UI.bmp"))
{
wcscpy(GUI_Name, _T("Start_UI.bmp"));
HBITMAP hb = (HBITMAP)LoadImage(g_hInstance, GUI_Name, IMAGE_BITMAP, 790, 600, LR_LOADFROMFILE);
hbDC = CreateCompatibleDC(hdc);//创建兼容DC
SelectObject(hbDC, hb);
}//因为循环很多次,如果每次循环都要执行getDC操作,内存会一直占用。
BitBlt(hdc, 0, 0, 1024, 768, hbDC, 0, 0, SRCCOPY);
}
到此,写完收工。
本次多线程处理没有抢占DC的情况,故无需用到event、互斥、原子锁等等。
完整代码:https://pan.baidu.com/s/1HPIGes0bVgt_MXgQLHTVIw
雁过留痕,人走有声。