近日里学习了关于win32编程的相关知识,利用这些知识制作了一款贪吃蛇小游戏,具体细节还是分模块来叙述
前期准备:在网上找到一些贪吃蛇的游戏素材图片,以及具体的逻辑框图
在正式写功能之前,先把一系列环境配置好,配置环境总体来说分为以下几步:
- 图片转化为bmp格式( Bitmap )二进制流
- 将图片加载到内存中,在加载内存中也分为三步
- 导入资源
- 将.rc文件代码中的绝对路径修改为相对路径(可不改,如果打包发给别人的话,不一定能保证对方存储文件的路径和你一致,我这里是将素材存储到 .c 文件的上一级当中)
- 在.c文件中利用LoadBitmap() 函数加载位图进内存,其中参数的意义就不赘述了,直接通过vs自带帮助文档进行查看,并且自定义一个 位图句柄 去接着函数返回的地址,引用宏的时候别忘了引入头文件
1 Hbitmap_BackGroup = LoadBitmap(hInstance,MAKEINTRESOURCE(IDB_BITMAP1)); 2 Hbitmap_Apple = LoadBitmap(hInstance,MAKEINTRESOURCE(IDB_BITMAP2)); 3 Hbitmap_SnackHead = LoadBitmap(hInstance,MAKEINTRESOURCE(IDB_BITMAP3)); 4 Hbitmap_SnackHead_Up = LoadBitmap(hInstance,MAKEINTRESOURCE(IDB_BITMAP5)); 5 Hbitmap_SnackHead_Down = LoadBitmap(hInstance,MAKEINTRESOURCE(IDB_BITMAP7)); 6 Hbitmap_SnackHead_Left = LoadBitmap(hInstance,MAKEINTRESOURCE(IDB_BITMAP6)); 7 Hbitmap_SnackHead_Right = LoadBitmap(hInstance,MAKEINTRESOURCE(IDB_BITMAP4));
现在就正式进入编写小游戏的阶段
先来分析一下贪吃蛇游戏的功能
- 显示背景
- 显示蛇
- 显示苹果
- 移动
- 吃苹果
- 生成新苹果
- 蛇长个
- 撞墙死亡
- 自咬死亡
逻辑上还是很清晰很简单的,接下来就是按功能实现
第一个,显示背景,首先一打开游戏就会有背景,那么只有重绘能实现,想要画图的话,必须获取窗口环境句柄(HDC),用完再去释放, 但是在win32中如何将我的背景位图,贴到HDC当中呢,查询后了解,在win32中贴图是以像素点为单位,一个像素点一个像素点的去传输到HDC当中,那么就得创建一个兼容性的HDC,再为兼容性DC选择我的背景位图,再分像素点进行传输
1 HDC hdc_compatible; 2 hdc_compatible = CreateCompatibleDC(hdc); //创建兼容性DC 3 SelectObject(hdc_compatible,Hbitmap_BackGroup); //为兼容性DC选择背景位图 4 BitBlt(hdc,0,0,600,600,hdc_compatible,0,0,SRCCOPY); //按像素点进行传输 5 DeleteDC(hdc_compatible); //删除兼容性DC 6 return;
第二个,显示蛇,首先以蛇的图片大小构建一个虚拟的坐标网(也可以不建,但像素点太小定位太麻烦),以我这个为例背景图是600*600的大小,蛇头,蛇身,苹果大小都是30*30大小,那么就是长宽都为0~19的坐标网,蛇的长个很容易想到可以用链表的添加进行实现,那么就把蛇身做成一个双向链表(为什么双向,在移动的时候就明白了),在添加链表后,再进行先蛇头后蛇身的贴图,上下左右用枚举类型
1 enum FX {UP,DOWN,LEFT,RIGHT};2 enum FX fx = RIGHT;
1 void AddNode(int x,int y) 2 { 3 Snack *pTemp = (Snack *)malloc(sizeof(Snack)); 4 pTemp->x = x; 5 pTemp->y = y; 6 pTemp->pLast = NULL; 7 pTemp->pNext = NULL; 8 9 if(pHead == NULL) 10 { 11 pHead = pTemp; 12 } 13 else 14 { 15 pEnd->pNext = pTemp; 16 pTemp->pLast = pEnd; 17 } 18 pEnd = pTemp; 19 } 20 void ShowSnack(HDC hdc) 21 { 22 Snack *pMark = pHead->pNext; 23 HDC hdc_compatible; 24 hdc_compatible = CreateCompatibleDC(hdc); 25 switch (fx) 26 { 27 case UP: 28 SelectObject(hdc_compatible,Hbitmap_SnackHead_Up); 29 break; 30 case DOWN: 31 SelectObject(hdc_compatible,Hbitmap_SnackHead_Down); 32 break; 33 case LEFT: 34 SelectObject(hdc_compatible,Hbitmap_SnackHead_Left); 35 break; 36 case RIGHT: 37 SelectObject(hdc_compatible,Hbitmap_SnackHead_Right); 38 break; 39 default: 40 break; 41 } 42 BitBlt(hdc,pHead->x*30,pHead->y*30,30,30,hdc_compatible,0,0,SRCCOPY); 43 44 while(pMark) 45 { 46 SelectObject(hdc_compatible,Hbitmap_SnackHead); 47 BitBlt(hdc,pMark->x*30,pMark->y*30,30,30,hdc_compatible,0,0,SRCCOPY); 48 pMark = pMark->pNext; 49 } 50 DeleteDC(hdc_compatible); 51 }
第三个,显示苹果,直接贴图即可
1 void ShowApple(HDC hdc) 2 { 3 HDC hdc_compatible; 4 hdc_compatible = CreateCompatibleDC(hdc); 5 SelectObject(hdc_compatible,Hbitmap_Apple); 6 BitBlt(hdc,apple.x*30,apple.y*30,30,30,hdc_compatible,0,0,SRCCOPY); 7 DeleteDC(hdc_compatible); 8 }
第四个,移动,win32中有个定时器的功能,每隔一段时间就向窗口发送定时器消息,那么蛇的坐标只要挨个代替前一个贴图就会实现移动的效果,但是问题就在于是从蛇头向蛇尾去遍历坐标变化,还是从蛇尾向蛇头变化,在几次尝试后发现还是蛇尾向蛇头,因为如果蛇头向蛇尾的话,蛇头坐标都改变了,而下一个蛇身就找不到蛇头的地址,就会出现蛇头蛇尾分家的情况,这也就是为什么做成双向链表的原因
1 void Move() 2 { 3 Snack *pMark = pEnd; 4 while(pMark != pHead) 5 { 6 pMark->x = pMark->pLast->x; 7 pMark->y = pMark->pLast->y; 8 pMark = pMark->pLast; 9 } 10 switch (fx) 11 { 12 case UP: 13 pHead->y--; 14 break; 15 case DOWN: 16 pHead->y++; 17 break; 18 case LEFT: 19 pHead->x--; 20 break; 21 case RIGHT: 22 pHead->x++; 23 break; 24 } 25 }
第五个,吃苹果,也就是当蛇头坐标和苹果坐标重合的时候代表吃到苹果了,一个判断就搞定了
1 BOOL IfEatApple() 2 { 3 if(pHead->x == apple.x && pHead->y == apple.y) 4 return TRUE; 5 return FALSE; 6 }
第六个,生成新苹果,利用随机数给苹果生成一个新的坐标,但是后期给室友测试的时候发现一个问题,这个苹果坐标的随机数在蛇长长之后会随到蛇身上,这个是需要改进的地方,所以就得每随机一次就得判断是否和蛇的所有坐标重合,没有的话,才把随机的x,y拿出来去贴图
void NewApple() { Snack *pMark = pHead; int x; int y; do { x = rand() % 18 + 1; y = rand() % 18 + 1; pMark = pHead; while(pMark) { if(pMark->x == x && pMark->y == y) break; pMark = pMark->pNext; } }while(pMark); apple.x = x; apple.y = y; }
第七个,蛇长个,添加链表即可,但是贴图坐标只要给到窗口以外,这样不影响游戏体验
1 AddNode(-10,-10);
第八个,撞墙死亡,给蛇头的坐标规定一个界限即可
1 BOOL IfBumpWall() 2 { 3 if(pHead->x == 0 || pHead->x == 19 || pHead->y == 0 || pHead->y == 19) 4 return TRUE; 5 return FALSE; 6 }
第九个,自咬死亡,判断蛇头坐标是否等于自身坐标即可
1 BOOL IfEatSelf() 2 { 3 Snack *pMark = pHead->pNext; 4 while(pMark) 5 { 6 if(pMark->x == pHead->x && pMark->y == pHead->y) 7 return TRUE; 8 pMark = pMark->pNext; 9 } 10 return FALSE; 11 }
至此,功能函数就完成了,接下来就是在回调函数里进行逻辑连接的过程了,在后期给多个朋友进行试玩测试的时候,也发现了不少的bug在此也一并写出
1.键盘上下左右的键码VK_加上对应的大写英文
2.由于贴图是连续的,上一次贴图无法销毁,那么就得用一层背景图,一层蛇图,一层苹果图的方式,实现游戏的实际效果
3.当蛇向右运行的时候,快速按下向上+向右的按键,会显示游戏结束,针对这个问题,是这样分析的,在一个定时器周期内,出现了两次按键反馈,也就会变成向左,那么蛇就出现自咬的情况,处理的办法就是人为的规定在一个定时器周期内只允许出现一次键盘消息,设置一个标记,没进定时器的时候为TURE,进过定时器为FALSE,此时键盘内的第二次快速按下的消息就无效了
4.暴力玩法,当屏幕内蛇几乎占满时,窗口会出现闪烁的问题,在进行查阅后,发现是因为重绘是有一个刷新周期的,我的所有贴图没在一个周期内贴完,屏幕就会出现闪烁的现象,要想处理这个bug,可以用双缓冲技术,也就是为当前HDC创建一个兼容性HDC,再为这个兼容性HDC,再创建一个兼容性HDC,两个兼容性HDC之间进行多次贴图操作,那么在一个重绘周期内,一次就把第一次的兼容性DC给贴上就可以了,满足一个刷新周期内图贴完的要求,具体实现的话,在以后深入学习c++的过程中继续完善。
完整代码如下(图片素材在文件里,有兴趣的可以自行下载):
1.贪吃蛇.rc
1 // Microsoft Visual C++ generated resource script. 2 // 3 #include "resource.h" 4 5 #define APSTUDIO_READONLY_SYMBOLS 6 ///////////////////////////////////////////////////////////////////////////// 7 // 8 // Generated from the TEXTINCLUDE 2 resource. 9 // 10 #include "afxres.h" 11 12 ///////////////////////////////////////////////////////////////////////////// 13 #undef APSTUDIO_READONLY_SYMBOLS 14 15 ///////////////////////////////////////////////////////////////////////////// 16 // 中文(简体,中国) resources 17 18 #if !defined(AFX_RESOURCE_DLL) || defined(AFX_TARG_CHS) 19 LANGUAGE LANG_CHINESE, SUBLANG_CHINESE_SIMPLIFIED 20 21 #ifdef APSTUDIO_INVOKED 22 ///////////////////////////////////////////////////////////////////////////// 23 // 24 // TEXTINCLUDE 25 // 26 27 1 TEXTINCLUDE 28 BEGIN 29 "resource.h " 30 END 31 32 2 TEXTINCLUDE 33 BEGIN 34 "#include ""afxres.h"" " 35 "