C语言控制台小游戏--贪吃蛇

游戏概览

上一篇文章归纳总结了一下C语言中的核心知识点,这一篇通过实现一个简单的贪吃蛇来巩固一下。

1
2
3
4
5
6
7
8
9
10
*******************************************************
* Welcome to Snake Game! *
* *
* ->开始游戏请按 enter键 *
* ->退出游戏请按 esc键 *
* ->暂停游戏请按 space键 *
* ->通过上下左右键来控制蛇的移动 *
* ->通过F1键减速 F2键加速 *
*******************************************************
请按任意键继续. . .

按enter进入游戏

1
2
3
4
5
6
7
8
9
10
■■■■■■■■■■■■■■■■■■
■ ■ 当前分数/通关分0/10
■ ■ 当前分每步得分:1
■ ■
■ ■ 速度越快 得分越高哦!!
■ *** ■
■ ■
■ ■
■ # ■
■■■■■■■■■■■■■■■■■■

预备知识

等待输入

system()函数属于stdlib标准库,这个库中定义了一些宏和通用的工具函数。

system("pause")就是从程序里调用“pause”命令,而“pause”这个系统命令的功能很简单,就是在命令行上输出一行类似于“Press any key to exit”或“请按任意键继续...”的字,等待用户按一个键,然后返回。

设置光标位置

编写Windows程序,要包含 windows.h 头文件。windows.h 还包含了其他一些Windows头文件,例如:

  • windef.h:基本类型定义
  • winbase.h:内核函数
  • wingdi.h:用户接口函数
  • winuser.h: 图形设备接口函数

这些头文件定义了Windows的所有数据类型、函数原型、数据结构和符号常量,也就是说,所有的Windows API都在这些头文件中声明。

设置光标位置就是用到了windows.h头文件中的SetConsoleCursorPosition函数。

1
2
3
4
BOOL WINAPI SetConsoleCursorPosition(
_In_ HANDLE hConsoleOutput,
_In_ COORD dwCursorPosition
);

其中COORD是一个结构体,用来定义屏幕的坐标。如下:

1
2
3
4
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;

HANDLE是一个句柄,用来标识不同设备的数值,GetStdHandle函数可以从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄。

链表实现

链表是存储数据的一种结构。

链表结构示意图

header为这个链表的头指针,第一个数据结点为头结点,有的链表实现头结点不存放有效数据,而是从下一个结点开始存放有效数据,这样做可以方便操作链表。

结点定义:

1
2
3
4
struct Node{
int data;
struct Node *pNext;
}

创建多个结点:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
//首先创建头结点并让header指向它
header = (pSnake)malloc(sizeof(Snake));
header->next = NULL;
//创建临时指针(表示目前的最后一个结点)
pSnake cur = head;
for(int i = 0; i < 3; i++){
//给第 i+1 个节点分配内存空间
struct Node *pNew = (struct Node *)malloc(sizeof(struct Node));
if(NULL == pNew){
exit(-1);
}
cur->next = pNew; //临时指针指向新结点
pNew->data = 10 + i;
pNew->next = NULL;
cur = cur->next;
}

产生随机数

time.h头文件中有一个srand()函数,srand函数是随机数发生器的初始化函数。通常使用srand((unsigned)time(NULL))来以时间作为种子,产生一个不一样的随机数。

1
2
3
4
5
6
7
#define ROW_MAP 10       //地图的行
#define COL_MAP 20 //地图的列

//产生x~y的随机数 k=rand()%(Y-X+1)+X
srand((unsigned)time(NULL));
int randx = rand() % (ROW_MAP - 2 - 1 + 1) + 1;
int radny = rand() % (COL_MAP - 3 - 2 + 1) + 2;

键盘监听

我们前面已经提过Windows.h和它包含的个别函数,键盘也要用到它里面的GetAsyncKeyState()

关于键盘的Keycode定义官方文档:https://docs.microsoft.com/en-us/windows/desktop/inputdev/virtual-key-codes

宏定义assert

assert宏的原型定义在assert.h中,其作用是如果它的条件返回错误,则终止程序执行。

assert的作用是先计算表达式expression,如果其值为假(即为0),那么它先向标准错误流stderr打印一条出错信息,然后通过调用abort来终止程序运行;否则,assert()无任何作用。宏assert()一般用于确认程序的正常操作,其中表达式构造无错时才为真值。完成调试后,不必从源代码中删除assert()语句,因为宏NDEBUG有定义时,宏assert()的定义为空。

实现过程

实现菜单界面

菜单界面的代码逻辑很简单,就是一些控制台输出。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char const *argv[])
{

while(1){
system("color 0C"); //设置控制台输出颜色
printf("*******************************************************\n");
printf("* Welcome to Snake Game! *\n");
printf("* *\n");
printf("* ->开始游戏请按 enter键 *\n");
printf("* ->退出游戏请按 esc键 *\n");
printf("* ->暂停游戏请按 space键 *\n");
printf("* ->通过上下左右键来控制蛇的移动 *\n");
printf("* ->通过F1键减速 F2键加速 *\n");
printf("*******************************************************\n");
system("pause"); //等待用户输入...
}
return 0;
}

初始化地图

1
2
3
4
5
6
7
8
9
10
11
■■■■■■■■■■■■■■■■■■
■ ■
■ ■
■ ■
■ ■
■ ■
■ ■
■ ■
■ ■
■■■■■■■■■■■■■■■■■■
请按任意键继续. . .

绘制这个地图需要边移动光标然后边输出字符,用for循环遍历移动输出两行和两列。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
#include <Windows.h>     

#define ROW_MAP 10 //地图的行
#define COL_MAP 20 //地图的列

void initMap(){
int i = 0;
for (i = 0; i < COL_MAP; i += 2){ //打印上下边框(每个■占用两列)
cursorPoint(i, 0);
printf("■");
cursorPoint(i, ROW_MAP - 1);
printf("■");
}
for (i = 0; i < ROW_MAP; i++){ //打印左右边框
cursorPoint(0, i);
printf("■");
cursorPoint(COL_MAP - 2, i);
printf("■");
}

printf("\n");
system("pause");
}

void cursorPoint(int x, int y){

COORD pos; //pos为结构体,记录坐标位置
pos.X = x;
pos.Y = y;

//读取标准输出句柄来控制光标为pos
SetConsoleCursorPosition(GetStdHandle(STD_OUTPUT_HANDLE), pos);
}

初始化蛇身

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
const char snake = '*';

//定义蛇体链表的结构体类型
typedef struct Snake{
size_t x; //行
size_t y; //列
struct Snake *next;
} Snake, *pSnake;

//定义蛇头指针
pSnake head = NULL;

//初始化蛇身链表数据
void initSnake(){
int initNum = 3; //创建三个蛇

head = (pSnake)malloc(sizeof(Snake));
head->x = 5;
head->y = 10;
head->next = NULL;

pSnake cur = head;
for (int i = 1; i < initNum; i++){
pSnake newNode = (pSnake)malloc(sizeof(Snake));
newNode->x = 5 + i;
newNode->y = 10;
newNode->next = NULL;
cur->next = newNode;
cur = cur->next;
}

printSnake();
}

//显示蛇身
void printSnake(){
pSnake cur = head;
while (cur){
cursorPoint(cur->y, cur->x);
printf("%c", snake);
cur = cur->next;
}
}

产生一个随机食物

事实上食物和蛇体的不同就是显示的样子不一样而已。

1
2
3
4
5
6
7
8
9
10
11
const char food = '#';
const char snake = '*';

typedef struct Snake{
size_t x; //行
size_t y; //列
struct Snake *next;
} Snake, *pSnake;

pSnake head = NULL; //定义蛇头指针
pSnake Food = NULL; //定义食物指针

接下来就是生成一个随机坐标,并检查是否和蛇身重合,如果重合继续生成一个。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
void createFood(){
pSnake cur = head;
Food = (pSnake)malloc(sizeof(Snake));

//产生x~y的随机数 k=rand()%(Y-X+1)+X;
srand((unsigned)time(NULL));
Food->x = rand() % (ROW_MAP - 2 - 1 + 1) + 1;
Food->y = rand() % (COL_MAP - 3 - 2 + 1) + 2;
Food->next = NULL;
while (cur){//检查食物是否与蛇身重合
if (cur->x == Food->x && cur->y == Food->y){
free(Food);
Food = NULL;
createFood();
return;
}
cur = cur->next;
}
cursorPoint(Food->y, Food->x);
printf("%c", food);
}

键盘事件

VK_RETURN代表的是键盘的Enter事件,点击Enter就可以触发进入地图。VK_ESCAPE代表的是键盘Esc事件,点击Esc退出游戏。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
int main(int argc, char const *argv[]){

while(1){

//显示菜单
showMenu();

//是否按了enter
if(GetAsyncKeyState(VK_RETURN)){

system("cls"); //清除显示

//显示地图
initMap();
//显示蛇体
initSnake();
//显示随机食物
createFood();

//光标移动到最后一行的下一行开始位置
cursorPoint(0, ROW_MAP + 1);
system("pause");
}else if (GetAsyncKeyState(VK_ESCAPE)){
exit(0);
}
}
return 0;
}

现在main函数中已经实现了从菜单界面到游戏地图界面的切换。

贪吃蛇自由移动

我们先定义一个枚举类型的方向。

1
2
3
4
5
6
enum Direction { //蛇行走的方向
R, //右
L, //左
U, //上
D //下
} Direction;

接下来当进入地图界面后循环监听键盘事件。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
//下面为从菜单进入地图界面后的逻辑

Direction = R; //蛇初始行走方向为右

while(1){
controllSnake();
}

//监听并控制蛇行走的方向
void controllSnake(){
if (GetAsyncKeyState(VK_UP) && Direction != D){ //向下
Direction = U;
}else if (GetAsyncKeyState(VK_DOWN) && Direction != U){ //向上
Direction = D;
}else if (GetAsyncKeyState(VK_LEFT) && Direction != R){ //向右
Direction = L;
}else if (GetAsyncKeyState(VK_RIGHT) && Direction != L){//向左
Direction = R;
}else if (GetAsyncKeyState(VK_ESCAPE)){ //Esc退出
exit(0);
}

snakeMove();

Sleep(500);
}

//在蛇头前面添加一个方块(相当于蛇向前移动了一格)
void snakeMove(){
pSnake newHead = (pSnake)malloc(sizeof(Snake));

if (Direction == R){
newHead->x = head->x;
newHead->y = head->y + 1;
newHead->next = head;
}else if (Direction == L){
newHead->x = head->x;
newHead->y = head->y - 1;
newHead->next = head;
}else if (Direction == U){
newHead->x = head->x - 1;
newHead->y = head->y;
newHead->next = head;
}else if (Direction == D){
newHead->x = head->x + 1;
newHead->y = head->y;
newHead->next = head;
}
head = newHead;
printSnake();
}

贪吃蛇

上面代码执行完虽然控制了方向,但是并不是蛇在移动,而是蛇在延长,而且是自动延长。最搞笑的是我们的蛇竟然跑出墙外了。

蛇吃食物

上面蛇虽然能控制方向并移动了,但是还有很多问题,我们先暂时不关心,来实现贪吃蛇的本质(吃食物)。

snakeMove()方法中添加如下判断

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
int isFood(pSnake pos){ //判断该位置是不是食物
assert(pos); //断言是否为空
if (pos->x == Food->x && pos->y == Food->y){
return 1;
}
return 0;
}

void snakeMove(){

//....上面代码省略

if(isFood(newHead)){ //是食物
head = newHead; //添加到头部
printSnake();
createFood(); //创建新食物
}else{
head = newHead;
pSnake cur = head;
//删除蛇尾并打印
while (cur->next->next != NULL){
cursorPoint(cur->y, cur->x);
printf("%c", snake);
cur = cur->next;
}

cursorPoint(cur->y, cur->x);
printf("%c", snake);
cursorPoint(cur->next->y, cur->next->x);
printf(" "); //打印空格来覆盖频幕上的蛇尾
free(cur->next); //动态释放分配的内存空间
cur->next = NULL;
}
}

判断是否撞墙

到这个时候我们游戏其实并没有状态,所以分不出输赢,下面我们先定义一下游戏中的状态。

1
2
3
4
5
6
enum State {        //游戏状态
ERROR_SELF, //咬到自己
ERROR_WALL, //撞到墙
NORMAL, //正常状态
SUCCESS //通关
} State;

初始化状态、判断是否撞墙,并设置新状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
State = NORMAL; //游戏初始为正常状态

int isCrossWall(){ //判断是否碰到墙
if (head->x <= 0 || head->x >= ROW_MAP - 1 || head->y <= 1 || head->y >= COL_MAP - 2){
State = ERROR_WALL; //撞墙后更改状态
return 1;
}
return 0;
}

void snakeMove(){

//...省略蛇移动的代码

if(isFood(newHead)){ //是食物

}else if(isCrossWall()){ //如果撞墙
free(newHead);
newHead = NULL;
}else{

}
}

状态虽然变化了,但是撞墙后没有提示游戏失败、下面来处理一下游戏状态所对应的逻辑。

游戏状态

controllSnake()后面判断当前游戏状态。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
int main(int argc, char const *argv[]){

while(1){

showMenu();

//是否按了enter
if(GetAsyncKeyState(VK_RETURN)){

system("cls"); //清除显示

initMap();
initSnake();
createFood();

Direction = R; //蛇初始行走方向为右
State = NORMAL; //游戏初始为正常状态

while(1){

controllSnake(); //控制后执行snakeMove()

//判断游戏状态
if (State == ERROR_SELF){
system("cls");
printf("很遗憾,蛇咬到自己,游戏失败!\n");
}else if (State == ERROR_WALL){
system("cls");
printf("很遗憾,蛇碰到墙壁,游戏失败!\n");
}else if (State == SUCCESS){
system("cls");
printf("恭喜您,已通关!!!\n");
}
}
}else if (GetAsyncKeyState(VK_ESCAPE)){
exit(0);
}
}
return 0;
}

判断咬到自己

1
2
3
4
5
6
7
8
9
10
11
12
int isEatSelf(pSnake newHead){ //判断是否咬到自己
pSnake cur = head;
assert(newHead);
while (cur){
if (cur->x == newHead->x && cur->y == newHead->y){
State = ERROR_SELF;
return 1;
}
cur = cur->next;
}
return 0;
}

到这里游戏的基本逻辑已经完成了,剩下的就是显示分数和控制速度等其他逻辑了,到此处的版本我称为简略版贪吃蛇吧。

源码下载

《简略版贪吃蛇源码下载》
《完整版贪吃蛇源码下载》

评论

Ajax Android AndroidStudio Animation Anroid Studio AppBarLayout Babel Banner Buffer Bulma ByteBuffer C++ C11 C89 C99 CDN CMYK COM1 COM2 CSS Camera Raw, 直方图 Chrome ContentProvider CoordinatorLayout C语言 DML DOM Dagger Dagger2 Darktable Demo Document DownloadManage ES2015 ESLint Element Error Exception Extensions File FileProvider Flow Fresco GCC Git GitHub GitLab Gradle Groovy HTML5 Handler HandlerThread Hexo Hybrid I/O IDEA IO ImageMagick IntelliJ Intellij Interpolator JCenter JNI JS Java JavaScript JsBridge Kotlin Lab Lambda Lifecycle Lint Linux Looper MQTT MVC MVP Maven MessageQueue Modbus Momentum MySQL NDK NIO NexT Next Nodejs ObjectAnimator Oracle VM Permission PhotoShop Physics Python RGB RS-232 RTU Remote-SSH Retrofit Runnable RxAndroid RxJava SE0 SSH Spring SpringBoot Statubar Task Theme Thread Tkinter UI UIKit UML VM virtualBox VS Code VUE ValueAnimator ViewPropertyAnimator Vue Web Web前端 Workbench api apk bookmark by关键字 compileOnly css c语言 databases demo hexo hotfix html iOS icarus implementation init jQuery javascript launchModel logo merge mvp offset photos pug query rxjava2 scss servlet shell svg tkinter tomcat transition unicode utf-8 vector virtual box vscode 七牛 下载 中介者模式 串口 临潼石榴 主题 书签 事件 享元模式 仓库 代理模式 位运算 依赖注入 修改,tables 光和色 内存 内核 内部分享 函数 函数式编程 分支 分析 创建 删除 动画 单例模式 压缩图片 发布 可空性 合并 同向性 后期 启动模式 命令 命令模式 响应式 响应式编程 图层 图床 图片压缩 图片处理 图片轮播 地球 域名 基础 增加 备忘录模式 外观模式 多线程 大爆炸 天气APP 太白山 头文件 奇点 字符串 字符集 存储引擎 宇宙 宏定义 实践 属性 属性动画 岐山擀面皮 岐山肉臊子 岐山香醋 工具 工厂模式 年终总结 开发技巧 异常 弱引用 恒星 打包 技巧 指针 插件 摄影 操作系统 攻略 故事 数据库 数据类型 数组 文件 新功能 旅行 旋转木马 时序图 时空 时间简史 曲线 杂谈 权限 枚举 架构 查询 标准库 标签选择器 样式 核心 框架 案例 桥接模式 检测工具 模块化 模板引擎 模板方法模式 油泼辣子 泛型 洛川苹果 浅色状态栏 源码 源码分析 瀑布流 热修复 版本 版本控制 状态栏 状态模式 生活 留言板 相册 相对论 眉县猕猴桃 知识点 码云 磁盘 科学 笔记 策略模式 类图 系统,发行版, GNU 索引 组件 组合模式 结构 结构体 编码 网易云信 网格布局 网站广播 网站通知 网络 美化 联合 膨胀的宇宙 自定义 自定义View 自定义插件 蒙版 虚拟 虚拟机 补码 补齐 表单 表达式 装饰模式 西安 观察者模式 规范 视图 视频 解耦器模式 设计 设计原则 设计模式 访问者模式 语法 责任链模式 贪吃蛇 转换 软件工程 软引用 运算符 迭代子模式 适配器模式 选择器 通信 通道 配置 链表 锐化 错误 键盘 闭包 降噪 陕西地方特产 面向对象 项目优化 项目构建 黑洞
Your browser is out-of-date!

Update your browser to view this website correctly. Update my browser now

×