c++ 实现贪吃蛇(附详细讲解)

更新

  1. 更新了不会刷屏的贪吃蛇版本, 链接为:snake2。
  2. 修复星星出现在墙上的bug。

需要用到的知识点

  1. 条件判断
  2. 循环
  3. 函数
  4. 数组
  5. 多cpp文件调用(不然你也可以直接写在一个文件里)
  6. 指针
  7. 结构体
  8. 链表(采用头插法)

整个程序的实现流程

1. 画图

这一步是最简单的,没错,我的习惯就是从最简单的开始。

我们的目标是画一个框框,然后这个框框里有条蛇, 还有个星星,根据这个目标,最后的代码就会书写成这样子:

void draw(int **map, int *star, int height, int weight){
	// 这里有一个需要关注的地方, 那就是边界是墙,那么到时候在判定的时候, 
	// 我们要将墙体的大小算进到整个地图大小中,也就是height是包含了墙体了的 

	/* 清空屏幕 */
	system("cls");
	for(int i = 0; i < height; i++){
		for(int j = 0; j < weight; j++){
			if(map[i][j] == 1){
				cout << "■";	// 蛇身和墙体 
			}else if(map[i][j] == 2){
				cout << "●";	// 蛇首 
			}else if(i == star[0] && j == star[1]){
				cout << "★";	// 星星 
			}else{
				cout << "□"; 	// 可移动区域 
			}
		}
		cout << endl;
	} 
	return;
}

这里,多介绍一些各个参数是什么, map是地图数组,star是星星的所在位置,height是地图的高,weight是地图的宽。

有一个细节我想你也注意到了,没错就是清空屏幕这个操作,这是为了什么要这么做呢,其实一开始是没有这个操作的,每次移动都会多画一个图在原来的图下面,这就会导致我们的地图位置不固定,影响我们的发挥,所以,我们要把这个地图固定住,这里明确一点,我们只会使用cmd这个窗口作为我们的游戏界面。

清空屏幕之后,就会开始画图,这样就会保证每次画的图都是新的。不过由于用了清屏的操作,所以会一闪一闪的,即便如此,我也玩得很嗨,没办法,原始人的快乐就是这么朴实。

不过上面的代码是完整版的代码,实际上, 如果你是从零开始搭建,开始的画图代码应该是只有地图,也就是一堵墙和地砖, 然后加上了蛇之后,画上蛇,然后有了小星星后, 画上小星星。

有个比较需要注意的细节, 由于蛇是用链表实现的,出于减少计算量的强迫症,我会先将蛇融合到地图中去,如下:

void drawSnake(int **map, struct snake snake){
	struct snake *p = snake.next;
	map[p->i][p->j] = 2; // 蛇头
	p = p->next;
	while(p){
		map[p->i][p->j] = 1;
		p = p->next;
	}
	return;
}

然后绘图时,就还是一次画完就可以了, 不用不断遍历蛇是否在某个点上,没错,强迫症真的很可怕。

以上代码你可以简单理解为 map = map + snake(当然,这个加可不是真的数值运算哈,我再偷偷提醒你一下吧。)。

也许你会问我, 那为什么不把星星也直接加到map里呢?问得好,这是因为蛇是变长的, 蛇最长能达到map的大小,假如我们不把蛇先用以上方式加到map中的话,在绘图时进行检查,最糟糕情况会是O(N*N),N=height*weight的计算复杂度,这我能忍吗?这我不能忍,直接融入地图吧, 你这个蛇儿。那星星呢?星星只是一个点,他永远都是一个点而已,按照时间复杂度的计算法来看, 都没有改变O(N),所以我就把它保留了,免得再写个函数。

没想到第一步就要知道这么多东西吧。(看到这里的都是热爱学习的好宝宝呀)。

2. 地图刷新

这个地图刷新其实上面已经讲过了, 并且也说了为什么要那么做,我们就不再细讲,我们可以在绘制好地图和蛇之后做这个地图刷新,至于星星,如果你现在就想画可以直接跳到后面,不过因为星星的特殊功能,我们现在可以先不管他。

3. 运动

我想,动起来,是整个游戏最最最重要的事情,不会动, 有什么用呢?没错,没有任何用处。

我们先来明确运动部分的代码我们需要做的事情。

  1. 当按下方向键后, 蛇会按照按下的方向键运动(注意最终的目标不是马上运动,不过我们是开发者,可以先写个立刻执行的)。
  2. 当没有输入任何命令时,蛇按照上一次的运动方向继续运动。(这个是第二阶段的目标,我们先来看看怎么实现第一阶段的目标吧。)

确定方向键:w:上,s:下,a:左,d:右。

确定ascii值:w:119, s:115,a:97, d:100。

ok,确定之后就可以直接撸代码了。

int move(struct snake *s, int *star){
	int sign = 0;
	switch(ch){
		case 119:
			/* 向上走 */
			sign = _move(s, star, -1, 0);
			break;
		case 115:
			/* 向下走 */ 
			sign = _move(s, star, 1, 0);
			break;
		case 97:
			/* 向左走 */
			sign = _move(s, star, 0, -1);
			break;
		case 100:
			/* 向右走 */
			sign = _move(s, star, 0, 1);
			break;
	}
	if(sign == 2){ // 吃到了星星 
		return 2;
	}
	/* 死亡检查 */
	return check_dead(s);
}

看到这个如果你不去深究_move这个函数到底怎么做的话,我相信你还是相当清晰到底是怎么回事的,那么我就接着将_move这个函数吧。

int _move(struct snake *s, int* star, int x, int y){
	/* 运动检查 */
	if(!check_action(s, x, y)){
//		return 0;
		x = direct_x;
		y = direct_y;
	}
	/* 修改目前运动方向 */
	direct_x = x;
	direct_y = y;
	
	/* 增加新节点 */
	struct snake *top = s->next;
	struct snake *p = s->next;

	struct snake *Ntop;
	Ntop = (struct snake*)malloc(sizeof(struct snake));
	Ntop->i = top->i + x;
	Ntop->j = top->j + y;
	s->next = Ntop;
	Ntop->next = top;
	
	/* 检查是否吃到星星 */
	if(check_star(s, star) == 1){
		return 2; 
	}
	
	/* 删除旧节点 */
	while(p->next->next != NULL){
		p = p->next;
	}
	p->next = NULL;
	return 0;
}

简单讲一下我没有提出来的代码(我也不准备后面贴出来),运动检查是看看这个输入的运动方向是否合理,比如不能倒退。还有更上面的死亡检查,就是看看是否这么走之后,蛇会不会撞死,我们知道它不能撞到自己,也不能撞到墙。

ok,这部分讲完我们开始讲上面贴出来的代码,如果方向错误的话,我们会选择使用原前进方向,也就说,如果你要倒退走的话,我们就直接照着之前的方向往前走,那么运动到底是怎么做到的,其实这里就需要你有个更加大局的看法了,我们之前把蛇融入到地图中去了,那么蛇的每个节点中的元素i,j其实代表的就是map中i,j坐标有蛇,且值是1或者2(当时蛇头时),所以,我们只需要把蛇前进方向的格子的坐标变成蛇头,变成蛇的新节点,然后删除尾节点就可以了。(这是没有吃到星星的情况下),如果吃到了星星,尾节点是不删除的,这样就可以实现长度+1。

好了,第一部分的运动我们说完了, 那么怎么做一个自动运动的蛇呢?其实这也很简单,只需要定时运动一下就好了, 前面我们知道我们有一个获取运动方向的函数,如下:

void getAction(){
	int sign = 0; // 是否已输入按键 
    double duration;	// 已检测时长 
     
    clock_t start, finish;
    start = clock();
	while(1){
		if(_kbhit() && sign == 0){
			int _ch = _getch();
			if(_ch == 119 || _ch == 115 || _ch == 97 || _ch == 100){
				ch = _ch;
				sign = 1;
			}
		}else if(_kbhit() && sign == 1){
			_getch();
		}
 		finish = clock();
    	duration = (double)(finish - start);
    	if(duration > speed)
			break;
	}
	return;
}

我们监视一定时长的键盘输入,并且将第一个有效输入保留到全局变量中的方向键ch中去,然后一直等到时间到了, 如果没有有效输入,就不做任何修改,我们会使用上次的方向键ch。这样子我们就得到了一个会执行一定时长的获取方向的模块了, 接下来就是调用这个模块的事情了,(没错,循环调用就好了, 这样就会没过一定时间获取一次,然后获取后运动,也就可以实现一段时间运动一次的目的了)。

一开始是想用多线程的, 后来发现定时的方式更加稳定且简单(毕竟多线程是一个很麻烦的东西,弱鸡伤不起)。

####4. 生成星星

生成星星的方式可以采用随机生成坐标的方式,然后检查是否会不会撞墙,也可以采用我代码中的方式,时间复杂度是相同的。

/* 生成星星 */
void generateStar(int **map, int *star, int height, int weight){
	int sum = 0;
	int index = 0;
	// 计算有多少空格 
	for(int i = 0; i < height; i++)
		for(int j = 0; j < weight; j++)
			if(map[i][j] == 0)
				sum += 1;
	// 随机生成空格下标 
	index = random(sum);
	// 选中index个空格作为星星的生成位置 
	for(int i = 0; i < height; i++)
		for(int j = 0; j < weight; j++){
			if(map[i][j] == 0)
				index -= 1;
				
			if(index == 0){
				star[0] = i;
				star[1] = j;
				cout << i << " " << j << endl;
				return;
			}
		}
	return;
}

5. 增加蛇长

增加蛇长上面已经有讲到了,就是吃到星星后, 头部增加,尾部不删,就可以实现增加蛇长。

6. 特殊情况检查

其实这个上面也已经有提到了。

  1. 死亡检查:是否撞到墙和撞到自己
  2. 运动检查:是否有导着走这种错误指令
  3. 是否吃到星星:看到这个你可能觉得有点奇怪,这不是基本的吗?其实我就是在再次提醒你,如果从零开始搭建,建议先不做吃到星星的操作,等到写好了第一部分的运动之后开始写也不迟。
  4. 是否超时:这个是写第二部分运动–自动运动的时候的定时功能。
  5. 输入合理性检查:如果不做这个的话,会导致输入wsad之外的键导致整个游戏暂停,我看一些博客将这个当做暂停方式,我想这个更应该算是一个bug,所以我选择避免这种情况。

代码链接

github: https://github.com/iajqs/pratice-c/tree/master/snake

你可能感兴趣的:(c++练习)