题目 | 贪吃蛇游戏 |
---|---|
主 要 内 容 | 借助Easyx图形界面库,利用鼠标和键盘,实现贪吃蛇的基本游戏功能。 可实现四种模式的切换,分别是三种固定速率刷新和一种变频加速刷新。 游戏过程中实现一键暂停和复原。 |
说 明 | 本文档仅作学习交流使用,任何转载引用请注明出处!文档末尾附有本文档的全部资源包括源程序代码,请合理使用。 |
目录
一、概述
从简单的对数据进行处理,再到实现具体问题,我想这次利用计算机语言实现我一直梦寐以求的事情,做一个游戏。在做出成品之前,我自己甚至都不敢相信这件事对于我来说可行的。期间有多次陷入瓶颈,只知道一个大致方向,却不知如何描述自己的问题,然后导致寻求帮助无果。不过总会有“踏破铁鞋无觅处,得来全不费功夫”的激动,也会有“山重水复疑无路,柳暗花明又一村”的喜悦。在这一个过程中,我也学会了自己独立分析问题,查找资料并解决它。
通过这次课程设计,学会了许多老师不曾涉及的具体问题,比如画图这一应用,但是还是有些功能我想实现却未曾实现的地方,奈何水平不够,甚是遗憾。
二、内容具体描述
(一)、编译环境与语言
所使用编译软件为Visual Studio 2017
其中最关键的图形界面库文件由EsayX提供。开源且较为方便,头文件名为: #include <graphics.h>
所使用计算机语言为C++
(二)、概要设计
1)初始化蛇(数据)
注意到蛇身与蛇头具有不同的特性,故对蛇身与蛇头分别处理。
使用结构体mypoint保存蛇的节点坐标,且0号节点为蛇头。在本课设中,蛇的节点数据使用的是线性表,且最大表长为102。理论上可借助链表来达到蛇长不受限。蛇头需要控制移动方向,故利用5种枚举类型(分别为上、下、左、右、暂停)来区分实现对蛇头移动方向的不同处理方式。最后是蛇的移动方向,即所谓蛇的朝向,蛇下步该移动到的方向,初始化时,蛇的position为向右,蛇长为3,从窗口左上角开始移动。
2)初始化食物
2.1利用结构体mypoint作为食物的坐标,坐标通过随机函数生成。这里应当注意,随机函数有可能把食物坐标生成到蛇的节点坐标上,此时应当做判断,考虑重新生成。
2.2利用fillrerectangle()函数在窗口显示出食物。
3)蛇的移动(数据)
蛇身:前一个蛇身节点所在位置为后一个蛇身节点将要所在的位置。故利用for循环赋值即可实现。
蛇头:根据所获取到的按键信息,将之处理为5种枚举类型之一,再分别对不同的状态,进行不同的蛇头坐标处理。
4)蛇的可视化(图像)
在这里,只需根据前一步得到的蛇的节点坐标,利用for循环即可利用fillrerectangle()函数在窗口显示出蛇的模样。
5)游戏结束的判断
5.1蛇撞墙。即蛇头节点坐标超出窗口边界。
5.2蛇吃蛇身。即蛇头节点坐标与蛇身坐标重合。
6)控制蛇移动的速度
整个蛇移动就是不断重复上述操作,但是注意到计算机的运行速度远比人眼反映的快,故我们借助Sleep函数,通过控制程序休眠时长(以毫秒计),就可控制蛇的移动速度。
7)通关此游戏
贪吃蛇每吃一个食物,全局变量”score”就会加一,当计数器为100时,系统判定此玩家游戏通关,结束游戏。
(三)、操作集合
1)函数汇总:
snake(); //构造函数,蛇的初始化
void drawsnake(); //画蛇
void movesnake(); //移动蛇
void keydown(); //获取按键
void judge(); //判断游戏结束
void gameover(); //游戏结束输出信息
void graduated(); //判断游戏是否通关
int& getsnakesize() //返回蛇长
mypoint getsnakepos(int pos)//返回蛇的节点坐标
food(); //构造函数,初始化食物
void drawfood(); //画食物
void setfoodpos(snake *psnake); //若食物出现在蛇身上,则重新生成
int& getflag() //返回食物状态
mypoint getfoodpos() //返回食物坐标
void start() //生成主界面
int help() //帮助说明
int degreechoice() //模式选择
void speed(int d) //给各个模式分配速度
int mouse() //捕获鼠标信息,并返回
void eatfood(snake *psnake, food *pfood)//判断是否吃到食物
void xiaochuang() //游戏界面小窗输出信息
2)主函数:
int main()
{
choice = 2;
srand((unsigned)time(0)); //设置随机数种子
initgraph(800, 480); //开辟游戏窗口
setbkcolor(RGB(51, 204, 235)); //填充背景颜色
PlaySound(TEXT("Happy.wav"), NULL, SND_FILENAME | SND_ASYNC | SND_LOOP); //播放背景音乐
while (choice == 2)
{
start(); //初始化主界面
choice = mouse();
if (choice == 1)
degree = degreechoice(); //难度选择
else if (choice == 4)
choice = help(); //操作说明
snake* psnake = new snake;
food* pfood = new food;
while (choice == 1)
{
BeginBatchDraw(); //开启批量绘图模式
cleardevice(); //清屏函数
xiaochuang();
eatfood(psnake, pfood); //判断是否吃到食物
if (pfood->getflag() == 0)
pfood->setfoodpos(psnake);//如果没有生成食物,则生成食物
if (_kbhit()) //判断是否存在操作
psnake->keydown(); //反馈控制信息
psnake->movesnake(); //更新蛇的节点坐标
psnake->drawsnake(); //根据蛇身坐标画蛇,使之可视化
pfood->drawfood(); //根据食物坐标画食物,使之可视化
psnake->judge();
//判断蛇是否撞墙或穿蛇身,并生成结束游戏界面
psnake->graduated(); //判断是否通关
FlushBatchDraw(); //绘制图形
speed(degree);
//利用程序休眠的时长来控制蛇移动的速度
}
delete(psnake); //释放内存
delete(pfood); //释放内存
}
closegraph(); //关闭窗口
return 0;
}
3)头文件:
#include <iostream>
#include <graphics.h> //图形界面头文件
#include <conio.h> //控制按键头文件
#include <Windows.h> //系统头文件
#include <time.h> //时间头文件
#pragma comment(lib,"winmm.lib") //音乐播放头文件
4)全局变量或常量:
int score = 0;
int choice;
int degree = 0;
const int N = 100;
int i;
MOUSEMSG m; //鼠标指针
5)类:
class snake;
class food;
6)结构体:
struct mypoint //坐标
(四)、调试分析
整个程序的难点在于(1)如何抽象出蛇的线性表(2)如何绘画出相应的图形(3)如何使界面更美观,更为人性化,游戏更为丰富多彩。
解决方案:
-
主要的贪吃蛇模型的搭建参考网上教程①,后再参考网上教程②,进行程序的优化和适当扩充。其中这两个教程贪吃蛇的状态均只有四种,即(right,left, down,up),(stop)状态为本人自行添加,起到程序暂停的一个作用。这也是当时想实现这个功能的解决方案。
-
绘图使用的是Easy-x中的图形界面库中的函数。<graphics.h>该头文件下有许多可直接调用的绘图函数,这也是编译器选择VS的原因之一。
-
对于这个问题,我创建过两个版本的贪吃蛇游戏,区别正是在于游戏界面的展示。早期是直接利用_getch()函数来获取键盘信息,再通过switch语句实现不同功能;后期是通过获取鼠标信息来控制不同功能的实现。
程序调试过程中发现的问题:
(1)在程序运行过程中,很大概率下会出现肉眼可察觉的闪屏现象。
(2)在不合法输入的条件下,即在主界面使用键盘输入,其中的输入信息会遗留至贪吃蛇的运动程序中去。
原因分析:
- 在使用Easy库的时候,由于每次都花大量时间绘制背景,导致闪烁厉害。这里用批量绘图方法将所有绘图一次性显示出来,以解决闪烁的问题。
- 未清空键盘缓冲区内的信息。
(五)、运行结果分析
1)主界面
2)操作说明
3)模式选择
4)贪吃蛇游戏
5)游戏结束显示
6)程序结构图
三、实现(源程序清单及注释)
#include <iostream>
#include <graphics.h> //图形界面头文件
#include <conio.h> //控制按键头文件
#include <Windows.h> //系统头文件
#include <time.h> //时间头文件
#pragma comment(lib,"winmm.lib") //音乐播放头文件
using namespace std;
int score = 0;
int choice;
int degree = 0;
const int N = 102;
int i;
MOUSEMSG m; //鼠标指针
struct mypoint { //定义节点坐标
int x;
int y;
};
class snake {
public:
snake(); //构造函数,蛇的初始化
void drawsnake(); //画蛇
void movesnake(); //移动蛇
void keydown(); //获取按键
void gameover(); //游戏结束输出信息
void judge(); //判断游戏结束
void graduated(); //判断游戏是否通关
int& getsnakesize() {
return snakesize; //返回蛇长
}
mypoint getsnakepos(int pos) {
return snakepos[pos]; //返回蛇的节点坐标
}
protected:
mypoint snakepos[N]; //蛇的节点坐标
int snakesize; //蛇长
char position; //蛇的移动方向
enum snakeposition { right, left, down, up, stop };//枚举类型
};
snake::snake()
{
snakesize = 3;
for (int i = snakesize - 1; i >= 0; i--)
{
snakepos[i].x = 10 * (3 - i);
snakepos[i].y = 10;
}
position = right;
}
void snake::drawsnake()
{
for (int i = 0; i < snakesize; i++)
{
if (i == 0)
setfillcolor(GREEN);
else
setfillcolor(RGB(51, 153, 255));
fillrectangle(snakepos[i].x, snakepos[i].y, snakepos[i].x + 10, snakepos[i].y + 10);
}
}
void snake::movesnake()
{
if (position != stop) {
for (int i = snakesize - 1; i > 0; i--)
{
snakepos[i].x = snakepos[i - 1].x;
snakepos[i].y = snakepos[i - 1].y;
}
}
switch (position)
{
case right:
snakepos[0].x += 10;
break;
case left:
snakepos[0].x -= 10;
break;
case up:
snakepos[0].y += 10;
break;
case down:
snakepos[0].y -= 10;
break;
case stop:
break;
}
}
void snake::keydown()
{
char userkey = 0;
userkey = _getch();
switch (userkey)
{
case 'W':
case 'w':
case 72:
if (position != up)
position = down;
break;
case 'S':
case 's':
case 80:
if (position != down)
position = up;
break;
case 'A':
case 'a':
case 75:
if (position != right)
position = left;
break;
case 'D':
case 'd':
case 77:
if (position != left)
position = right;
break;
case 'F':
case 'f':
position = stop;
break;
case 'L':
case 'l':
choice = 3;
EndBatchDraw(); //关闭批量绘图模式
break;
case 'J':
case 'j':
score = 0;
choice = 2;
EndBatchDraw(); //关闭批量绘图模式
break;
}
}
void snake::gameover()
{
EndBatchDraw(); //关闭批量绘图模式
cleardevice();
settextcolor(RED);
settextstyle(50, 0, L"宋体");
outtextxy(280, 130, L"Game Over!");
settextcolor(BLACK);
settextstyle(36, 0, L"宋体");
outtextxy(340, 220, L"得分:");
TCHAR s[5];
_stprintf_s(s, _T("%d"), score);
outtextxy(440, 220, s);
settextstyle(24, 0, L"宋体");
outtextxy(280, 300, L"请按J键回到主界面...");
outtextxy(280, 324, L"或按L键退出游戏.....");
char userkey = 0;
while (1) {
userkey = _getch();
if (userkey == 'J' || userkey == 'j')
{
score = 0;
choice = 2;
break;
}
else if (userkey == 'L' || userkey == 'l')
{
choice = 3;
break;
}
}
}
void snake::judge()
{
if (snakepos[0].x < 10 || snakepos[0].x>550 ||
snakepos[0].y < 10 || snakepos[0].y>460) //判断蛇是否撞墙
gameover();
for (int i = snakesize - 1; i > 0; i--) //判断蛇头是否碰蛇身
if (snakepos[i].x == snakepos[0].x && snakepos[i].y == snakepos[0].y)
gameover();
}
void snake::graduated()
{
if (score == 100) {
EndBatchDraw(); //关闭批量绘图模式
cleardevice();
settextcolor(RED);
settextstyle(50, 0, L"宋体");
outtextxy(220, 130, L"Congratulations!");
settextcolor(BLACK);
settextstyle(36, 0, L"宋体");
outtextxy(300, 220, L"恭喜你通关了!");
settextstyle(24, 0, L"宋体");
outtextxy(260, 300, L"请按J键回到主界面...");
outtextxy(260, 324, L"或按L键退出游戏.....");
char userkey = 0;
while (1) {
userkey = _getch();
if (userkey == 'J' || userkey == 'j')
{
score = 0;
choice = 2;
break;
}
else if (userkey == 'L' || userkey == 'l')
{
choice = 3;
break;
}
}
}
}
class food {
public:
food(); //构造函数,初始化食物
void drawfood(); //画食物
void setfoodpos(snake* psnake); //若食物出现在蛇身上,则重新生成
int& getflag()
{
return foodflag; //返回食物状态
}
mypoint getfoodpos()
{
return foodpos; //返回食物坐标
}
protected:
mypoint foodpos; //食物坐标
int foodflag; //食物状态
};
food::food()
{
foodpos.x = (rand() % 55+1) * 10;
foodpos.y = (rand() % 46+1) * 10;
foodflag = 1;
}
void food::drawfood()
{
setfillcolor(RGB(255, 0, 0));
fillrectangle(foodpos.x, foodpos.y, foodpos.x + 10, foodpos.y + 10);
}
void food::setfoodpos(snake* psnake)
{
int j;
j = psnake->getsnakesize();
while (foodflag != 1)
{
foodpos.x = (rand() % 55 + 1) * 10;
foodpos.y = (rand() % 46 + 1) * 10;
for (int i = 0; i < j; i++)
{
if (psnake->getsnakepos(i).x == foodpos.x
&& psnake->getsnakepos(i).y == foodpos.y)
{
foodflag = 0;
break;
}
foodflag = 1;
}
}
}
void eatfood(snake* psnake, food* pfood) //判断是否吃到食物
{
if (psnake->getsnakepos(0).x == pfood->getfoodpos().x
&& psnake->getsnakepos(0).y == pfood->getfoodpos().y)
{
psnake->getsnakesize()++;
pfood->getflag() = 0;
score++;
}
}
void start() //生成主界面
{
cleardevice();
settextcolor(RED);
settextstyle(18, 0, L"宋体");
outtextxy(670, 455, L"power by Alan");
settextcolor(BLUE);
settextstyle(36, 0, L"宋体");
outtextxy(200, 100, L"欢迎来到贪吃蛇的世界!");
setlinecolor(RED);
settextcolor(RED);
settextstyle(24, 0, L"宋体");
rectangle(310, 200, 422, 242);
outtextxy(320, 210, L"开始游戏");
rectangle(310, 250, 422, 292);
outtextxy(320, 260, L"退出游戏");
rectangle(310, 300, 422, 342);
outtextxy(320, 310, L"操作说明");
}
int help() //帮助说明
{
cleardevice();
settextstyle(20, 0, L"宋体");
rectangle(0, 0, 90, 30);
outtextxy(4, 4, L"退出说明");
settextstyle(30, 0, L"宋体");
outtextxy(120, 50, L"操作说明:请在英文模式下开始游戏。");
settextstyle(20, 0, L"宋体");
outtextxy(40, 175, L"W:向上,S:向下,A:向左,D:向右,F:暂停");
outtextxy(40, 200, L"J:返回主界面,L:退出游戏");
outtextxy(40, 225, L"或使用方向键盘。");
outtextxy(40, 250, L"暂停后仅需重新按键控制蛇的运动即可恢复游戏。");
settextcolor(RED);
settextstyle(18, 0, L"宋体");
outtextxy(670, 455, L"power by Alan");
while (true)
{
m = GetMouseMsg(); //获取一条鼠标消息
if (m.uMsg == WM_LBUTTONDOWN)
{
for (i = 0; i <= 10; i++)
{
setlinecolor(GREEN); //设置圆颜色
circle(m.x, m.y, 2 * i);
Sleep(20); //停顿20ms
circle(m.x, m.y, 2 * i); //抹去刚刚画的圆
}
if (m.x >= 0 && m.x <= 90 && m.y >= 0 && m.y <= 30)
return 2;
FlushMouseMsgBuffer(); //清空鼠标消息缓存区
}
}
}
int degreechoice() //模式选择
{
cleardevice();
settextcolor(RED);
settextstyle(18, 0, L"宋体");
outtextxy(670, 455, L"power by Alan");
settextcolor(BLUE);
settextstyle(36, 0, L"宋体");
outtextxy(0, 0, L"请选择难易度:");
settextcolor(RED);
setlinecolor(RED);
settextstyle(30, 0, L"宋体");
rectangle(215, 75, 285, 110);
outtextxy(220, 80, L"普通");
rectangle(215, 115, 285, 150);
outtextxy(220, 120, L"一般");
rectangle(215, 155, 285, 190);
outtextxy(220, 160, L"高级");
rectangle(215, 195, 285, 230);
outtextxy(220, 200, L"进阶");
settextcolor(BLACK);
settextstyle(18, 0, L"宋体");
outtextxy(0, 240, L"难度说明:");
outtextxy(0, 258, L"进阶:会随着分数的提高而逐步加速。");
while (true)
{
m = GetMouseMsg(); //获取一条鼠标消息
if (m.uMsg == WM_LBUTTONDOWN)
{
for (i = 0; i <= 10; i++)
{
setlinecolor(GREEN); //设置圆颜色
circle(m.x, m.y, 2 * i);
Sleep(20); //停顿20ms
circle(m.x, m.y, 2 * i); //抹去刚刚画的圆
}
if (m.x >= 215 && m.x <= 285 && m.y >= 75 && m.y <= 110)
return 1;
else if (m.x >= 215 && m.x <= 285 && m.y >= 115 && m.y <= 150)
return 2;
else if (m.x >= 215 && m.x <= 285 && m.y >= 155 && m.y <= 190)
return 3;
else if (m.x >= 215 && m.x <= 285 && m.y >= 195 && m.y <= 230)
return 4;
FlushMouseMsgBuffer(); //清空鼠标消息缓存区
}
}
}
void speed(int d) //给各个模式分配速度
{
switch (d)
{
case 1: //普通
Sleep(140);
break;
case 2: //一般
Sleep(80);
break;
case 3: //高级
Sleep(60);
break;
case 4: //进阶
if (score < 5)
Sleep(160);
else if (score < 10 && score >= 5)
Sleep(100);
else if (score < 30 && score >= 10)
Sleep(80);
else
Sleep(60);
break;
}
}
int mouse() //捕获鼠标信息,并返回
{
setrop2(R2_NOTXORPEN); //二元光栅——NOT(屏幕颜色 XOR 当前颜色)
while (true)
{
m = GetMouseMsg(); //获取一条鼠标消息
if (m.uMsg == WM_LBUTTONDOWN)
{
for (i = 0; i <= 10; i++)
{
setlinecolor(GREEN); //设置圆颜色
circle(m.x, m.y, 2 * i);
Sleep(20); //停顿20ms
circle(m.x, m.y, 2 * i); //抹去刚刚画的圆
}
if (m.x >= 310 && m.x <= 422 && m.y >= 200 && m.y <= 242)
return 1;
if (m.x >= 310 && m.x <= 422 && m.y >= 250 && m.y <= 292)
return 3;
if (m.x >= 310 && m.x <= 422 && m.y >= 300 && m.y <= 342)
return 4;
FlushMouseMsgBuffer(); //清空鼠标消息缓存区
}
}
}
void xiaochuang() //游戏界面小窗输出信息
{
setfillcolor(RGB(227,187,223));
solidrectangle(0, 0, 570, 10);
solidrectangle(560, 0, 570, 480);
solidrectangle(0, 0, 10, 480);
solidrectangle(0, 470, 570, 480);
settextstyle(20, 0, L"宋体");
outtextxy(575, 5, L"操作说明:请在英文模式");
outtextxy(575, 30, L"下开始游戏");
outtextxy(575, 55, L"W:向上 A:向左 S:向下 ");
outtextxy(575, 80, L"D:向右 F:暂停 ");
outtextxy(575, 105, L"J:返回主界面");
outtextxy(575, 130, L"L:退出游戏");
outtextxy(670, 455, L"power by Alan");
settextstyle(30, 0, L"宋体");
outtextxy(575, 222, L"得分:");
TCHAR s[5];
_stprintf_s(s, _T("%d"), score);
outtextxy(655,222,s);
}
int main()
{
choice = 2;
srand((unsigned)time(0)); //设置随机数种子
initgraph(800, 480); //开辟游戏窗口
setbkcolor(RGB(51, 204, 235)); //填充背景颜色
PlaySound(TEXT("Happy.wav"), NULL, SND_FILENAME | SND_ASYNC | SND_LOOP); //播放背景音乐
while (choice == 2)
{
start(); //初始化主界面
choice = mouse();
if (choice == 1)
degree = degreechoice(); //难度选择
else if (choice == 4)
choice = help(); //操作说明
snake* psnake = new snake;
food* pfood = new food;
while (choice == 1)
{
BeginBatchDraw(); //开启批量绘图模式
cleardevice(); //清屏函数
xiaochuang();
eatfood(psnake, pfood); //判断是否吃到食物
if (pfood->getflag() == 0)
pfood->setfoodpos(psnake); //如果没有生成食物,则生成食物
if (_kbhit()) //判断是否存在操作
psnake->keydown(); //反馈控制信息
psnake->movesnake(); //更新蛇的节点坐标
psnake->drawsnake(); //根据蛇身坐标画蛇,使之可视化
pfood->drawfood(); //根据食物坐标画食物,使之可视化
psnake->judge(); //判断蛇是否撞墙或穿蛇身,并生成结束游戏界面
psnake->graduated(); //判断是否通关
FlushBatchDraw(); //绘制图形
speed(degree); //利用程序休眠的时长来控制蛇移动的速度
}
delete(psnake); //释放内存
delete(pfood); //释放内存
}
closegraph(); //关闭窗口
return 0;
}
四、参考资料
1. 《一起来面向对象编程吧!C++ 版本 多文件的贪吃蛇》
https://www.bilibili.com/video/BV1q4411X7RN
2.《C++语言实例_贪吃蛇小游戏制作_潭州教育》
https://www.bilibili.com/video/BV1DE411o7qM
3.《C语言图形化界面——含图形、按钮、鼠标、进度条等部件制作(带详细代码、讲解及注释)》
https://blog.csdn.net/weixin_44044411/article/details/104276757
5.《c语言中的rand()函数和srand()函数产生随机的整数》
https://blog.csdn.net/qin_zhangyongheng/article/details/8033936?utm_medium=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param&depth_1-utm_source=distribute.pc_relevant.none-task-blog-BlogCommendFromMachineLearnPai2-1.channel_param
五、资源下载
链接:https://pan.baidu.com/s/1_oTzhF8rNpB9XbUGiiwnvw
提取码:smay