c简单贪吃蛇游戏解析

0. 序言

这次课程的任务是找到一个规模合适的小项目, 对项目构成进行分析, 找出其中不合理的部分, 并且适当做增量开发, 受时间影响, 找的程序和增加的功能都比较简单. 它使用c语言.

贪吃蛇小游戏源码 https://blog.csdn.net/qq_35038153/article/details/70244811

1. 源码解析

#include<windows.h>
#include<time.h>
#include<stdlib.h>
#include<conio.h>
#include<iostream>
// 地图大小
#define N 21

1.1 各个头文件的作用:

  • conio.h 和随机数生成有关
  • window.h 提供c++对命令行窗口的控制
  • stdlib.h 动态开辟内存
  • time.h 时间相关

1.2 各个函数的作用:

// 将光标移动到命令行指定位置
void gotoxy(int x, int y);

// 设定文本颜色
void color(int a);

// 初始化函数(初始化围墙、显示信息、苹果)
void init(int apple[2]);

1.3 main函数主要做:

1. 定义蛇长, 苹果位置, 蛇的存储结构等参数
2. 初始化蛇的位置
3. 接收用户输入awsd移动, 其它键蛇会聚拢
4. 更新蛇的位置 
5. 蛇头在苹果位置, 更新计分, 生成新的苹果
6. 判断撞墙失败

1.4 源码

#include<windows.h>
#include<time.h>
#include<stdlib.h>
#include<conio.h>
// 地图大小
#define N 21
#include<iostream>
using namespace std;

void gotoxy(int x, int y)//位置函数
{
	COORD pos;
	pos.X = 2 * x;
	pos.Y = y;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
}

void color(int a)//颜色函数
{
	SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), a);
}

void init(int apple[2])//初始化函数(初始化围墙、显示信息、苹果)
{
	int i, j;//初始化围墙
	int wall[N + 2][N + 2] = { {0} };
	for (i = 1; i <= N; i++)
	{
		for (j = 1; j <= N; j++)
			wall[i][j] = 1;
	}
	color(11);
	for (i = 0; i < N + 2; i++)
	{
		for (j = 0; j < N + 2; j++)
		{
			if (wall[i][j])
				cout << "■";
			else cout << "□";
		}
		cout << endl;
	}
	gotoxy(N + 3, 1);//显示信息
	color(20);
	cout << "按 W S A D 移动方向" << endl;
	gotoxy(N + 3, 2);
	color(20);
	cout << "按任意键暂停" << endl;
	gotoxy(N + 3, 3);
	color(20);
	cout << "得分:" << endl;
	apple[0] = rand() % N + 1;//苹果
	apple[1] = rand() % N + 1;
	gotoxy(apple[0], apple[1]);
	color(12);
	cout << "●" << endl;
}

int main()
{
	int i, j;
	int** snake = NULL;
	int apple[2];
	int score = 0;
	int tail[2];
	int len = 3;
	char ch = 'p';
	srand((unsigned)time(NULL));  // 生成伪随机数队列
	init(apple);
  
        // 开辟int *数组
	snake = (int**)realloc(snake, sizeof(int*) * len);
        // 重点, 每个snake里的int *指向一个元素大小为2的数组; 其中第一个元素是x轴值, 第二个是y轴值. 用坐标轴表示蛇身的一个点
	for (i = 0; i < len; i++)
		snake[i] = (int*)malloc(sizeof(int) * 2);
  
        // 初始化蛇的位置
	for (i = 0; i < len; i++)
	{
		snake[i][0] = N / 2;
		snake[i][1] = N / 2 + i;
		gotoxy(snake[i][0], snake[i][1]);
		color(14);
		cout << "★" << endl;
	}
  
	while (1)//进入消息循环
	{
		tail[0] = snake[len - 1][0];
		tail[1] = snake[len - 1][1];
		gotoxy(tail[0], tail[1]);
		color(11);
		cout << "■" << endl;
    
                // 算法核心: 除蛇头外.后一格下一帧的位置一定是当前帧时前一格的位置
                // 从后往前移
		for (i = len - 1; i > 0; i--)
		{
			snake[i][0] = snake[i - 1][0];
			snake[i][1] = snake[i - 1][1];
			gotoxy(snake[i][0], snake[i][1]);
			color(14);
			cout << "★" << endl;
		}
    
                // _kbhit判断用户是否键盘键入内容, 它是不阻塞的
		if (_kbhit())
		{
                // 获取用户输入, 它是阻塞的, 同时它是回显的, 输入会显示在命令窗口中
			gotoxy(0, N + 2);
			ch = _getche();
		}
    
                // 确定蛇头这一帧的位置
		switch (ch)
		{
		case 'w':snake[0][1]--; break;
		case 's':snake[0][1]++; break;
		case 'a':snake[0][0]--; break;
		case 'd':snake[0][0]++; break;
		default: break;
		}
		gotoxy(snake[0][0], snake[0][1]);
		color(14);
		cout << "★" << endl;
    
                // 分数越高, 蛇移动速度越快
		Sleep(abs(200 - 0.5 * score));
    
                //吃掉苹果后蛇分数加1,蛇长加1
		if (snake[0][0] == apple[0] && snake[0][1] == apple[1])
		{
			score++;
			len++;
			snake = (int**)realloc(snake, sizeof(int*) * len);
			snake[len - 1] = (int*)malloc(sizeof(int) * 2);
                        // 生成新平果
			apple[0] = rand() % N + 1;
			apple[1] = rand() % N + 1;
			gotoxy(apple[0], apple[1]);
			color(12);
			cout << "●" << endl;
			gotoxy(N + 5, 3);
			color(20);
			cout << score << endl;
		}
    
                //撞到围墙后失败
		if (snake[0][1] == 0 || snake[0][1] == N || snake[0][0] == 0 || snake[0][0] == N)
		{
			gotoxy(N / 2, N / 2);
			color(30);
			cout << "失败!!!" << endl;
			for (i = 0; i < len; i++)
				free(snake[i]);
			Sleep(INFINITE);
			exit(0);
		}
	}
	return 0;
}

2. 问题

  • 对移动没有做限制, 蛇能够闯过自身

  • 并且, 当蛇头碰到蛇身, 游戏不会结束

  • 原本代码还有很多问题, 比如暂停时, 只有蛇头不移动, 蛇的其它位置还是移动的

3. 增量开发

3.1 限制移动

​ 在main中新增direction字符变量

​ 在_kbhit增加限制, 例如当蛇往左走时, 用户输入'd', 蛇不往右走, 还是往左走

if (_kbhit())
{
  
  ...other code
  
  if (ch == 'w' || ch == 'a' || ch == 's' || ch == 'd') {
    switch (ch) {
      // 保证不能反向走
      case 'w': direction = (direction == 's' ? direction : ch); break;
      case 's': direction = (direction == 'w' ? direction : ch); break;
      case 'a': direction = (direction == 'd' ? direction : ch); break;
      case 'd': direction = (direction == 'a' ? direction : ch); break;
    }
    continue;
  }
}

3.2 判断蛇头是否和蛇身重合

// 遍历非蛇头外的蛇身节点
for (int i = 1; i < len; ++i) {
  // 坐标完全吻合
  if (snake[0][0] == snake[i][0] && snake[0][1] == snake[i][1]) {
		...gameover logic code
  }
}

3.3 更新暂停逻辑

​ 原先代码按下非wsad键蛇头就不移动了, 我想为避免误操作, 将暂停功能改为空格键

​ 在main中新增pause整形变量, 用于控制是否暂停

​ 将获取用户的输入代码前移, 注意即使是暂停, 也要读入用户输入, 否则永远无法结束暂停

if (_kbhit()){
  gotoxy(N + 2, N + 2);
  ch = _getche();  // _getche()带回显; _getch()不带回显

  // 如果按下空格
  if (ch == ' ') {
    pause ^= 1;  // 按位异或, 将pause从0和1间切换
  }

  if (pause) continue; // 暂停了, 直接跳到下一帧, 后面内容不执行了

  ... other code
    
}

if (!pause) {
  ...code which execute frame update and gameover logic
}

3.4 增量后代码

#include<windows.h>
#include<time.h>
#include<stdlib.h>
#include<conio.h>
#define N 21
#include<iostream>
using namespace std;

/*
	增加功能1: 蛇不能够反向走
	增加功能2: 改为空格暂停, 暂停时蛇身不动
	增加功能3: 添加失败条件, 蛇头碰到蛇身游戏会结束
*/

void gotoxy(int x, int y)//位置函数
{
	// coordinate坐标系
	COORD pos;
	pos.X = 2 * x;
	pos.Y = y;
	SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
}

void color(int a)//颜色函数
{
	SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), a);
}

void init(int apple[2])//初始化函数(初始化围墙、显示信息、苹果)
{
	int i, j;//初始化围墙
	int wall[N + 2][N + 2] = { {0} };

	// 所有可行走区域为1, 围墙为0
	for (i = 1; i <= N; i++)
	{
		for (j = 1; j <= N; j++)
			wall[i][j] = 1;
	}
	color(11);

	// 画出地图
	for (i = 0; i < N + 2; i++)
	{
		for (j = 0; j < N + 2; j++)
		{
			if (wall[i][j])
				cout << "■";
			else cout << "□";
		}
		cout << endl;
	}

	// 显示提示信息
	gotoxy(N + 3, 1);
	color(20);
	cout << "按 W S A D 移动方向" << endl; //显示信息

	gotoxy(N + 3, 2);
	color(20);
	cout << "按任意键暂停" << endl;

	gotoxy(N + 3, 3);
	color(20);
	cout << "得分:" << endl;

	//随机生成苹果位置
	apple[0] = rand() % N + 1;
	apple[1] = rand() % N + 1;
	gotoxy(apple[0], apple[1]);
	color(12);
	cout << "●" << endl;
}

int main()
{
	int pause = 1;
	char direction = 'w';
	// ----------------------------新增------------------------------

	double sumTime;
	int fps;
	int i, j;
	int** snake = NULL;
	int apple[2];
	int score = 0;
	int tail[2];
	int len = 3;
	char ch = 'p';
	srand((unsigned)time(NULL));  // srand和rand用法参考https://blog.csdn.net/qq_43516928/article/details/118864806

	init(apple);

	// realloc尝试在原先地址内扩展
	snake = (int**)realloc(snake, sizeof(int*) * len);

	// 每个指针指向一个大小为2, 类型为int的数组, 两个元素分别表示x, y值
	for (i = 0; i < len; i++)
		snake[i] = (int*)malloc(sizeof(int) * 2);

	// 初始化蛇身
	for (i = 0; i < len; i++)
	{
		snake[i][0] = N / 2;
		snake[i][1] = N / 2 + i;
		gotoxy(snake[i][0], snake[i][1]);
		color(14);
		cout << "★" << endl;
	}

	while (1)//进入消息循环
	{
		if (_kbhit())  // _kbhit()和_getche()参考https://blog.csdn.net/hou09tian/article/details/86668083
		{
			gotoxy(N + 2, N + 2);
			ch = _getche();  // _getche()带回显; _getch()不带回显

			// 如果按下空格
			if (ch == ' ') {
				pause ^= 1;
			}

			if (pause) continue;

			if (ch == 'w' || ch == 'a' || ch == 's' || ch == 'd') {
				switch (ch) {
				// 保证不能反向走
				case 'w': direction = (direction == 's' ? direction : ch); break;
				case 's': direction = (direction == 'w' ? direction : ch); break;
				case 'a': direction = (direction == 'd' ? direction : ch); break;
				case 'd': direction = (direction == 'a' ? direction : ch); break;
				}
				continue;
			}
		}

		if (!pause) {
			tail[0] = snake[len - 1][0];
			tail[1] = snake[len - 1][1];
			gotoxy(tail[0], tail[1]);
			color(11);
			cout << "■" << endl;

			// 将后面的蛇身节点逐一移动到前面节点的位置
			for (i = len - 1; i > 0; i--)
			{
				snake[i][0] = snake[i - 1][0];
				snake[i][1] = snake[i - 1][1];
				gotoxy(snake[i][0], snake[i][1]);
				color(14);
				cout << "★" << endl;
			}

			switch (direction)
			{
			case 'w':snake[0][1]--; break;  // 向上y值减一
			case 's':snake[0][1]++; break;  // 向下y值加一
			case 'a':snake[0][0]--; break;  // 向左x值减一
			case 'd':snake[0][0]++; break;  // 向右x值加一
			default: break;
			}

			gotoxy(snake[0][0], snake[0][1]);
			color(14);
			cout << "★" << endl;

			Sleep(abs(200 - 0.5 * score));

			// 吃掉苹果后蛇分数加1,蛇长加1
			if (snake[0][0] == apple[0] && snake[0][1] == apple[1])
			{
				score++;
				len++;
				snake = (int**)realloc(snake, sizeof(int*) * len);
				snake[len - 1] = (int*)malloc(sizeof(int) * 2);

				// 生成苹果
				int xIndex, yIndex;
				do {
					xIndex = rand() % N + 1;
					yIndex = rand() % N + 1;
					apple[0] = xIndex;
					apple[1] = yIndex;
				} while (xIndex == snake[0][0] && yIndex == snake[0][1]);  // 保证苹果不生成在蛇头的位置(其实也应该包括蛇身的)

				gotoxy(apple[0], apple[1]);
				color(12);
				cout << "●" << endl;

				// 分数位置
				gotoxy(N + 5, 3);
				color(20);
				cout << score << endl;
			}
			
			// 判断蛇头是否和蛇身重合
			for (int i = 1; i < len; ++i) {
				if (snake[0][0] == snake[i][0] && snake[0][1] == snake[i][1]) {
					// 打印失败信息
					gotoxy(N / 2, N / 2);
					color(30);
					cout << "失败!!!" << endl;

					// 释放资源
					for (i = 0; i < len; i++)
						free(snake[i]);
					free(snake);

					Sleep(INFINITE);
					exit(0);
				}
			}
			// 撞到围墙后失败
			if (snake[0][1] == 0 || snake[0][1] == N || snake[0][0] == 0 || snake[0][0] == N)
			{
				// 打印失败信息
				gotoxy(N / 2, N / 2);
				color(30);
				cout << "失败!!!" << endl;

				// 释放资源
				for (i = 0; i < len; i++)
					free(snake[i]);
				free(snake);

				Sleep(INFINITE);
				exit(0);
			}
		}
	
	}
	return 0;
}

4. 结语

​ (1) 这次选择的小项目只有150多行, 找过其它的项目, 用到了自己不熟悉的c++模块, 规模也比较大, 一时半会是不能理顺了. 而且功能也比较完善, 不好做增量. 所以就选择了这个项目 :happy:

​ (2) 其实还能增加更多功能, 比如最高分, 蛇能穿过边界从另一边出来, 主界面和失败重新开始等. 但想到什么功能就加入什么功能并不符合软件开发的流程.

posted @   口乞厂几  阅读(111)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
历史上的今天:
2021-03-08 c语言指针(完)
点击右上角即可分享
微信分享提示