用户:哲学喵查看:0 回复:1 评论:0 创建时间:2019-01-04T14:47:39
贪吃蛇游戏的设计思路很简单,相信有过一些编程经验的同学都不至于束手无策,可在我刚刚接触编程时,这个小小的贪吃蛇游戏可是让我费了不少脑筋,如今学习编程已经快一年了,前几天又看了一遍K&R,打算写几个贪吃蛇程序巩固一下知识。我打算写若干篇贪吃蛇的博客,从简单粗糙的开始,不断改良,希望能给初学C语言的同学一点借鉴。
话不多说,我们现在就开始吧,首先我们整理一下思路。首先我们要明确,既然贪吃蛇游戏理论上可以无限继续下去,那么游戏主体一定就是一个循环。蛇的移动就在这个循环中完成。如果是初学编程的话,可能会好奇如何让蛇移动起来,在这里我们是通过不断改变蛇的坐标,然后根据蛇的坐标不断刷新屏幕在视觉上形成蛇的移动效果。我们先来看看主函数:
1 int main()
2 {
3 int dir=UP; //初始方向默认为向上,UP是个宏
4 while (1){
5 print_game(); //打印游戏
6 dir=get_dir(dir); //获取方向
7 move_snake(dir); //移动蛇身
8 if (!isalive()) //判断蛇的生命状态
9 break;
10 }
11 printf("Game Over\n");
12 }
可见游戏的整体思路是比较简单的。有了框喵们再来具体实现,首先考虑使用的数据结构,正如博客名所示,这次我只用数组,不使用链表和结构体。那么我们用一个二维数组char map[17][17]来表示地图,用一维数组unsigned char snake[15]来表示蛇的坐标。这里之所以用char是因为本着能省则省的原则,我们这是一个17*17大小的小游戏,char足够了。
#include <stdio.h> #include <conio.h> #include <stdlib.h> #include <windows.h>
#include <time.h> #define UP 72 //72,80,75,77是方向键对应的键值 #define DOWN 80 #define LEFT 75 #define RIGHT 77 #define SNAKE 1 #define FOOD 2 #define BAR 3 char map[17][17] = { 0 }; //初始化地图 unsigned char snake[50] = { 77 }; //初始化蛇头坐标 unsigned char food = 68; //初始化食物坐标 char len = 1; //蛇长
只用数组做贪吃蛇的话,在蛇坐标与食物坐标的存储上会比较麻烦,毕竟我们的游戏坐标是二维的,这里我采用位运算的方法,一个unsigned char类型的变量八位,前四位记录x坐标,后四位记录y坐标,四位的话,最大值也就是15,所以我设置的是17*17的大小,(15+边界)*(15+边界)。要是觉得地图太小的话,用int或者long也是可以的。由于我们要经常做数字与x,y的转换,我们把它写成一个函数。
void tran(unsigned char num,unsigned char *x, unsigned char *y)
{
*x = num >> 4;
*y = (unsigned char)(num << 4) >> 4; //注意这里一定要做个强制类型转换
}
碰到被调用函数需要返回两个以上参数的时候,应该像上述函数一样,将要返回的值写成指针参数。
接下来,让我们把主函数里调用的函数一一填上。
void print_game(void)
{
int i, j;
for (i = 0; i<17; i++) { //根据地图上每点的情况绘制游戏
for (j = 0; j<17; j++) {
if (map[i][j] == 0)
putchar(' ');
else if (map[i][j] == SNAKE)
putchar('*');
else if (map[i][j] == BAR)
putchar('#');
else if(map[i][j]==FOOD)
putchar('!');
}
putchar('\n');
}
Sleep(100); //休眠函数,将进程或线程挂起100ms,包含在window.h中
system("cls"); //清屏函数,包含在stdlib.h中
}
获取方向的函数注意考虑蛇身超过一节的蛇不能回头的问题,代码如下:
int get_dir(int old_dir)
{
int new_dir = old_dir;
if (_kbhit()) { //用kbhit()与getch()组合实现键盘响应
_getch();
new_dir = _getch(); //getch()函数要使用两次
if (len > 1 && (abs(new_dir - old_dir) == 2 || abs(new_dir - old_dir) == 8))
new_dir = old_dir;
}
return new_dir;
}
getch()函数要用两次的原因是因为第一次返回的值指示该键扩展的字符,第二次调用才返回实际的键代码。abs为绝对值函数,之所以把绝对值函数与2,8比较是对方向的宏定义观察发现的规律。getch()和kbhit()前面的下划线是因为在vs环境下一些函数需要使用加下划线的版本,更安全。
接下来是移动蛇身的函数(游戏的大部分内容都在其中):
void move_snake(int dir)
{
int last = snake[0],current; //last与current用于之后蛇坐标的更新
int i, j;
int grow=0; //判断是否要长身体
unsigned char x, y,fx,fy; //蛇坐标与食物坐标
tran(food, &fx, &fy);
tran(snake[0], &x, &y);
switch (dir){ //更新蛇头坐标
case UP:
y--;
break;
case DOWN:
y++;
break;
case LEFT:
x--;
break;
case RIGHT:
x++;
break;
}
snake[0] = ((x ^ 0) << 4) ^ y; //将x,y换回一个数
if (snake[0] == food) {
grow = 1;
food = generate_food(); //产生新食物
}
for (i = 0; i<len; i++) { //蛇移动的关键,通过将蛇头原来的坐标赋给第二节,原来的第二节赋给第三节,依次下去,完成蛇坐标的更新
if (i == 0) //如果只有头,跳过,因为前面已更新蛇头坐标
continue;
current = snake[i]; //将当前操作的蛇节坐标存储到current里
snake[i] = last; //完成当前操作蛇节坐标的更新
last = current; //last记录的是上一次操作蛇节的坐标,这次操作已经结束,故把current赋给last
}
if (grow) {
snake[len] = last;
len++;
}
for (i = 0; i < 17; i++) //将边界与食物加到地图里去
for (j = 0; j < 17; j++)
if (i == 0 || i == 16 || j == 0 || j == 16)
map[i][j] = BAR;
else if (i == fy&&j == fx)
map[i][j] = FOOD;
else
map[i][j] = 0;
for (i = 0; i < len; i++) { //将蛇加到地图里去
tran(snake[i], &x, &y);
if (snake[i] > 0)
map[y][x] = 1;
}
}
其中有个生产食物的函数,generate_food(),它利用随机数生成函数生成食物坐标,代码如下:
unsigned char generate_food(void)
{
unsigned char food_,fx,fy;
int in_snake=0,i;
srand((unsigned int)time(NULL));
do {
food_ = rand() % 255;
tran(food_, &fx, &fy);
for (i = 0; i < len; i++)
if (food_ == snake[i])
in_snake = 1;
} while (fx == 0 || fx == 16 || fy == 0 || fy == 16 || in_snake);
return food_;
}
随机数生成函数,一般由srand()和rand()函数组成,前者以当前时间为参数提供种子供rand()函数生成更为随机的数。这里用do while语句做了一个筛选,最后产生在边界内且不再蛇身上的食物。
最好剩下一个判断蛇喵活的函数,直接上代码吧:
int isalive(void)
{
int self_eat = 0, i;
unsigned char x, y;
tran(snake[0], &x, &y);
for (i = 1; i < len; i++)
if (snake[0] == snake[i])
self_eat = 1;
return (x == 0 || x == 16 || y == 0 || y >= 16 || self_eat) ? 0 : 1;
}
判断了蛇是否撞到边界或者自食。
好了,有了这些函数,把他们组装起来就成了我们的简易贪吃蛇游戏。游戏画面如下:

虽然相当得简陋,由于用的是全屏刷新的方法,闪屏也比较严重,但也算是麻雀虽小五脏俱全,该有的功能也都有了,代码也不过150行左右。在接下来的博客里我还会继续折腾这个游戏,直到它看起来比较顺眼为止。由于本人还是新手,这也是个新手向的教程,有什么问题欢迎指出。