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) 其实还能增加更多功能, 比如最高分, 蛇能穿过边界从另一边出来, 主界面和失败重新开始等. 但想到什么功能就加入什么功能并不符合软件开发的流程.
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· DeepSeek 开源周回顾「GitHub 热点速览」
· 物流快递公司核心技术能力-地址解析分单基础技术分享
· .NET 10首个预览版发布:重大改进与新特性概览!
· AI与.NET技术实操系列(二):开始使用ML.NET
· 单线程的Redis速度为什么快?
2021-03-08 c语言指针(完)