int main()
{
InitSnake();
while (1)
{
while (!kbhit()) //如果没有接收键盘
{
if (!EatFood()) //如果食物被吃掉,就画出新的食物来
{
CoorFood();
DrawFood();
}
Break(); //先检查再移动
MoveSnake();
IntervalTime = Level() * 60;
Sleep(IntervalTime);
}
GetKey();
}
getchar(); //卡屏
return 0;
}
游戏从 InitSnake() 开始,初始化画板,加载封面图片以及音乐。初始化蛇也就是只有一个结点的蛇最开始的样子,同时初始化一个食物结点。kbhit() 函数用于接收键盘,当没有键盘信息传来时(键盘信息包括改变蛇的移动方向以及游戏暂停),蛇一直移动。移动之前先 Break() 函数判断是否撞到自己或者撞到墙,如果撞到就 GameOver(),没有撞到就执行移动操作。蛇两次移动之间间隔 IntervalTime 时间,Level() 函数提供游戏等级,蛇的长度越大,Level() 的值越低,移动的间隔时间越短,也就是蛇的移动速度越来越快,游戏难度越来越大。移动之前还要EatFood() 函数判断食物有没被吃掉,如果被吃掉了,就要重新计算新食物的生成坐标,并画出新的食物结点。
void InitSnake()
{
initgraph(640, 480, NOMINIMIZE); //初始化绘图环境,创建画板
//加载图片
loadimage(NULL, L"E:\\贪吃蛇\\贪吃蛇\\pic.jpg", 640, 480);
Sleep(1000);
//加载音乐
mciSendString(L"open 谓风.mp3 alias bk", 0, 0, 0);
mciSendString(L"play bk repeat", 0, 0, 0);
setbkcolor(RGB(220, 20, 150)); //用 RGB 宏合成颜色
cleardevice(); //用背景色清空屏幕,并将当前点移至 (0, 0)
//初始化一条小蛇
setfillcolor(CYAN); //设置填充颜色,蛇头是青色
solidrectangle(0, 0, SNAKE_SIZE, SNAKE_SIZE);
snake.szb[0].x = 0;
snake.szb[0].y = 0;
snake.n = 1;
snake.c = Right;
//初始化食物
food.flag = true;
}
initgaph() 函数用于创建画板,函数原型
HWND initgraph(int width,int height,int flag = NULL);
指定画板的长宽以及绘图环境的样式,flag 的值可以为NOCLOSE(禁用画板的关闭按钮),NOMINIMIZE(禁用画板最小化按钮),SHOWCONSOLE(同时还显示控制台窗口)。函数返回创建的绘图窗口的句柄。
loadimage() 函数加载游戏封面,NULL表示图片将读取至绘图窗口。路径用字符串表示,用 \\ 可能是防止 \ 将后面的字符转义掉,长字符串前面加上 L 。后两个参数表示拉伸图片,这里将图片铺满整个绘图窗口。
休眠 1000 毫秒后加载音乐,填充画板,游戏开始。
加载音乐需要添加 #include
用 RGB() 宏合成背景颜色,cleardevice() 函数用背景色填充画板。游戏开始了。
初始化只有一个结点的小蛇,蛇头是 CYAN ,后面的蛇身是 BLUE,初始化蛇结点的形状是填充矩形,蛇头的坐标是 (0,0),初始化蛇的长度,方向,以及食物最开始没有被吃掉。
画圆函数 circle() 的三个参数是圆心的 x,y坐标以及半径,矩形函数 rectangle() 函数的四个参数是左上角顶点的 x,y 坐标以及右下角顶点的 x,y 坐标。蛇的结点是矩形,保存结点坐标,在这里就是保存矩形左上角顶点的坐标。
enum DIRECT {
up = 72,
down = 80,
Left = 75, //使用 std 之后,要对left ,right 重命名
Right = 77,
space = 32
};
struct COOR {
int x;
int y;
} coor;
struct SNAKE {
COOR szb[SNAKE_MAX_LENGTH]; //每一个蛇的结点的坐标
int n; //蛇的当前长度
DIRECT c; //蛇的方向
} snake;
struct FOOD {
COOR fzb; //食物坐标
bool flag; //true 表示食物没有被吃掉, false 表示食物被吃掉
} food;
DIRECT 枚举类型是为了将接收到的键盘信息(接收到的其实是 ASCII 码)转换成容易理解的符号常量。注意到后面进行文件处理的时候,要引入 std 名称空间,引入之后 left 和 right 在 cout<< 的时候,分别表示左对齐和右对齐,有了特殊的含义,所以这里要给枚举常量重新起个名字。
COOR 结构体保存结点坐标,也就是矩形左上方顶点的坐标。
SNAKE 结构体保存蛇的结点信息,这里定义了两个全局常量。
#define SNAKE_MAX_LENGTH 100 //蛇的最大长度
#define SNAKE_SIZE 10 //蛇的大小(宽度)
FOOD 结构体保存食物结点的相关信息。
在蛇移动之前,先判断一下蛇会不会撞到墙或者撞到自己。撞到墙意味着蛇头坐标超出了画板的边界。因为蛇头坐标表示的是左上角顶点的坐标,所以比较的参数是 630 和 470 ,而不是 640 和 480。撞到自己的话,意思就是蛇头结点和某一个身体结点重合了。其实这个关系作为判断条件并不准确,两个矩形只要有边碰到就是撞上了,这时候两个矩形还没有完全重合呢!
void Break()
{
//撞到墙
if (snake.szb[0].x < 0 || snake.szb[0].x > 630 || snake.szb[0].y < 0 ||
snake.szb[0].y > 470)
{
GameOver();
}
//撞到自己
//因为某个 bug ,这里必须要从 2 开始
for (int i = 2; i < snake.n; i++)
{
if (snake.szb[0].x == snake.szb[i].x && snake.szb[0].y == snake.szb[i].y)
{
GameOver();
}
}
}
void MoveSnake()
{
//蛇的后一个结点保存前一个结点的坐标,DrawSnake()之后就实现了每个结点坐标的更新
//同时,更新之前的最后一个结点还保留在,所以更新后会多出一个结点,要把它删去
for (int i = snake.n; i > 0; i--)
//这里 i=snake.n 初始化,就会只有一个结点
{
snake.szb[i].x = snake.szb[i - 1].x;
snake.szb[i].y = snake.szb[i - 1].y;
}
//对蛇的方向进行判断
switch (snake.c)
{
case up:
snake.szb[0].y -= SNAKE_SIZE;
break;
case down:
snake.szb[0].y += SNAKE_SIZE;
break;
case Left:
snake.szb[0].x -= SNAKE_SIZE;
break;
case Right:
snake.szb[0].x += SNAKE_SIZE;
break;
}
DrawSnake(); //移动完之后重新画蛇
}
For() 循环里面的 i 的初始条件,我还没有完全搞明白。
void DrawSnake()
{
for (int i = snake.n - 1; i > 0; i--)
{
setfillcolor(BLUE); //这里是画蛇的身体,蓝色
solidrectangle(snake.szb[i].x, snake.szb[i].y, snake.szb[i].x + SNAKE_SIZE,
snake.szb[i].y + SNAKE_SIZE);
}
//蛇头颜色不一样,单独拿出来画
setfillcolor(CYAN);
solidrectangle(snake.szb[0].x, snake.szb[0].y, snake.szb[0].x + SNAKE_SIZE,
snake.szb[0].y + SNAKE_SIZE);
//隐藏尾巴
setfillcolor(RGB(220, 20, 150)); //设置当前绘图前景色
//用和当前背景色相同的绘图前景色来绘制多出的结点,就把它隐藏了
solidrectangle(snake.szb[snake.n].x, snake.szb[snake.n].y,
snake.szb[snake.n].x + SNAKE_SIZE, snake.szb[snake.n].y + SNAKE_SIZE);
//setcolor(WHITE); //隐藏最后一个结点之后,别忘了恢复绘图色
}
蛇移动完之后,在新的每个蛇结点位置画上矩形,同时把多出来的那个尾巴结点删除。
//画食物
void DrawFood()
{
setfillcolor(GREEN); //食物是绿色填充
solidroundrect(food.fzb.x, food.fzb.y, food.fzb.x + SNAKE_SIZE,
food.fzb.y + SNAKE_SIZE, SNAKE_SIZE, SNAKE_SIZE);
food.flag = true; //新画的食物还没有被吃掉
}
//计算食物坐标
void CoorFood()
{
srand(unsigned(time(NULL)));
//这种求余方法是为了使食物位于 10 的整数倍的坐标上
food.fzb.x = rand() % (640 / SNAKE_SIZE) * SNAKE_SIZE;
food.fzb.y = rand() % (480 / SNAKE_SIZE) * SNAKE_SIZE;
}
当 EatFood() 函数返回食物被吃掉了,就先后调用 CoorFood() 函数和 DrawFood() 函数画出新的食物。注意,这里的求余方法非常巧妙。
//吃食物函数,返回食物有没有被吃掉
bool EatFood()
{
if (snake.szb[0].x == food.fzb.x && snake.szb[0].y == food.fzb.y)
{
snake.n++;
//将食物结点删掉
setfillcolor(RGB(220, 20, 150));
solidroundrect(food.fzb.x, food.fzb.y, food.fzb.x + SNAKE_SIZE,
food.fzb.y + SNAKE_SIZE, SNAKE_SIZE, SNAKE_SIZE);
food.flag = false; //食物已经被吃掉
}
return food.flag;
}
吃掉食物,意思就是蛇头结点和食物结点重合了。
//接收键盘
void GetKey()
{
int key;
key = getch();
switch (key)
{
case up: //这里应该是在枚举里面转换过了
if (snake.c != down)
{
snake.c = up;
}
break;
case down:
if (snake.c != up)
{
snake.c = down;
}
break;
case Left:
if (snake.c != Right)
{
snake.c = Left;
}
break;
case Right:
if (snake.c != Left)
{
snake.c = Right;
}
break;
//实现游戏暂停功能,按空格键继续游戏
case space:
do
{
mciSendString(L"pause bk", 0, 0, 0); //暂停音乐
key = getch();
} while (key != space);
mciSendString(L"resume bk", 0, 0, 0); //再次按下空格键恢复音乐
break;
}
}
这里注意 getchar() 和 getch() 的区别。getchar() 按下按键后,要按一下回车(Enter)才能有效果,而 getch() 是即时产生响应的。转换方向的时候,有个前提条件,就是蛇的当前移动方向不能和按下的方向键相反。按下 space 游戏暂停,一直在执行 do 里面的语句,即暂停音乐和接收按键。当再次按下 space 时,while() 里面条件成假,跳出循环,再次播放音乐,游戏重新开始。这里注意到 do while() 结构是如果为真则持续执行,如果为假则跳出循环。
//游戏难度等级,表现为蛇的移动速度越来越快
int Level()
{
int level;
if (snake.n < 15)
{
level = 5;
}
else if (snake.n < 30)
{
level = 4;
}
else if (snake.n < 40)
{
level = 3;
}
else if (snake.n < 50)
{
level = 2;
}
else
{
level = 1;
}
return level;
}
这个函数就是根据蛇的长度提供不同的难度等级。
//游戏结束
void GameOver()
{
cleardevice();
//先获取当前历史最高分
ifstream fin("历史最高分.txt");
int tmp;
fin >> tmp;
//输出游戏结束的画面
setcolor(YELLOW);
settextstyle(50, 20, L"宋体");
outtextxy(100, 100, L"亲爱的,游戏结束啦!");
outtextxy(100, 200, L"您的得分是:");
//输出数字的话,要先将数字格式化成字符串
TCHAR s[5];
_stprintf(s, _T("%d"), snake.n);
outtextxy(340, 200, s);
outtextxy(400, 200, L"分!");
if (snake.n > tmp) //超过了最高分
{
outtextxy(100, 300, L"恭喜超过了当前最高分!");
fstream file("历史最高分.txt", ios::out); //清空文件内容
ofstream fout("历史最高分.txt");
fout << snake.n;
}
else //没有超过最高分
{
outtextxy(100, 300, L"历史最高分是:");
TCHAR t[5];
_stprintf(t, _T("%d"), tmp);
outtextxy(400, 300, t);
outtextxy(460, 300, L"分!");
}
//关闭音乐
mciSendString(L"close bk", 0, 0, 0);
//手动关闭游戏窗口
getchar();
closegraph();
}
这个函数实现了历史最高分功能,该功能依赖于文件实现。引入 istream 和 ostream 这两个头文件,使用 ifstream 和 ofstream 这两个类创建 fin 和 fout 这两个对象。tmp 读取文件中的历史最高分,与结束时的蛇的长度比较。
settextstyle() 函数的三个参数是文字的高度,宽度,以及字体。
settextxy() 函数的三个参数是第一个字符的 x , y 坐标以及待输出的字符串。如果输出变量数字的话,要先格式化成字符串输出。
如果超过了最高分,就把文件数据清楚,把新的数据写进去。没有超过,就输出文件中的历史最高分。
最后,关闭音乐。
文件输入输出操作的头文件
std 对于 left 和 right 的影响
加载图片时路径中的 \\
GetKey() 函数里, switch 里面, case 里面的 if ,判断条件写错了
撞到墙时的判断条件写反了
移动函数里面 for()里面的 i 的初始值
为什么开始会有两个结点,撞到自己的 for() 循环初始值要从 2 开始
加上重新开始的功能,这样好像要重新调用 main 函数? 可以将 main() 里面的内容抽出来放到一个 gaming() 函数里,main() 调用这个 gaming(),如果游戏结束, gaming() 就返回给 main() 一个标志,main() 接收到这个意思为游戏结束的标志,就会再次调用 gaming(),游戏重新开始。
将面向过程改成面向对象。
更换添加音乐的方式。在 VS2012 外面,点击 exe 文件运行,音乐无法播放。
用 EasyX 做出来的窗口,不能在游戏中任意缩放窗口大小。
可以将代码中的窗口长宽这两个常量抽出来定义成全局常量。
加上一个写有游戏中当前得分的小窗口。
写一个玩家 id 登陆游戏功能,保存不同玩家的历史最高分,可以进行排序。
利用 Qt 做GUI ,做一些对话框之类的。