2025-03-15
C语言
00
请注意,本文编写于 55 天前,最后修改于 17 天前,其中某些信息可能已经过时。

目录

贪吃蛇(C语言小游戏教学)
引言
设计思路
源代码
fun.h
main.cpp
fun.cpp
详细思路
1.fun.h
2.fun.c
a.定义与声明
b.光标的隐藏和跳转
c.模式选择
d.初始化
e.蛇的打印和删除
f.食物生成
g.碰撞检测与移动逻辑
碰撞检测函数judge()
移动函数move()
h.分数记录与文件操作
最高分读取ReadGrade()
最高分写入WriteGrade()
i.游戏主循环与输入处理
已知问题与优化方向
已知问题
扩展优化
结语

贪吃蛇(C语言小游戏教学)

引言

作为C语言初学者,在学完基础操作之后,可能每一个部分都会,但还不会融会贯通。可能常常会想C语言到底能干什么?经典游戏贪吃蛇,作为C语言入门的典型案例。在其中,我们用到了程序设计基础中所学习的几乎所有知识,并且严格按照msvc编译器的标准编写,保证了在Windows系统运行的稳定性。由于微软的标准并不支持VLA(虽然这个标准在C11中被改为了可选项),我们使用了malloc函数,这样除了单链表以外,每一章节的知识点均有涉及。

设计思路

首先定义全局变量,蛇头、蛇身,均采用结构体。再定义边界与游戏区域,使用二维数组。并且使用宏定义区分边界与空的空间,提高代码的可读性与维护性。为了直观地展现程序运行逻辑,我使用了模块化编程,将主函数单独放在一个cpp文件中(c和cpp文件均可,不会影响本文代码)。

接着,定义每个功能的函数。初始化、移动、判断结束、打印删除、分数累计、最高分保存、游戏。这些函数的具体逻辑已经体现在了源代码中,这里不再过多阐述。然后在主函数中按需调用,组成逻辑完整的程序即可。

在编写中,同样的,光标等问题不可忽视。我们将使用Windows API里包含的库函数,以实现隐藏光标,光标跳转,打印颜色改变的操作。同样的,这些操作被封装成自定义函数。

最后进行一些已知的bug修复。

接下来先看源代码。

源代码

fun.h

h
#include <stdio.h> #include <Windows.h> #include <stdlib.h> #include <time.h> #include <conio.h> extern int max,grade; void modchoose(int a); void cleanfp(); void first(); void HideCursor(); void ReadGrade(); void WriteGrade(); void gamebegin();

main.cpp

cpp
#include"fun.h" int main(void) { max = 0, grade = 0; //初始化变量 system("title 贪吃蛇"); //设置cmd窗口的名字 system("mode con cols=100 lines=40"); //设置cmd窗口的大小 HideCursor(); //隐藏光标 ReadGrade(); //从文件读取最高分到max变量 printf("按回车键开始(只能全屏游玩)\n输入法调成英文\nwasd移动,p暂停"); getchar(); printf("请选择地图大小:\n1:20*40\n2:25*50\n3:30*60\n"); int a; while (1) { a = _getch(); if (a == 49 || a == 50 || a == 51) break; } modchoose(a);//模式选择 first();//初始化界面 gamebegin(); cleanfp();//最后清理指针 return 0; }

fun.cpp

cpp
#define _CRT_SECURE_NO_WARNINGS 1 #include <stdio.h> #include <Windows.h> #include <stdlib.h> #include <time.h> #include <conio.h> #define NT 0//空 #define WALL 1//墙 #define TOU 2//头 #define BODY 3//身体 #define FOOD 4//食物 extern int main(); int ROW;//行 int COL;//列 int* face = NULL; int max, grade; struct snake { int l;//长度(不包括头) int x;//坐标x int y;//坐标y }st;//蛇头 struct body//身体 { int x; int y; }*arr; void modchoose(int a) { if (a == 49) { ROW = 20; COL = 40; } if (a == 50) { ROW = 25; COL = 50; } if (a == 51) { ROW = 30; COL = 60; } face = (int*)malloc(sizeof(int) * ROW * COL); arr = (struct body*)malloc(sizeof(struct body) * ROW * COL); } void HideCursor()//隐藏光标 { CONSOLE_CURSOR_INFO curInfo; curInfo.dwSize = 1; curInfo.bVisible = FALSE; HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorInfo(handle, &curInfo); } void CursorJump(int x, int y)//光标跳转 { COORD pos; pos.X = x; pos.Y = y; HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorPosition(handle, pos); } void color(int c)//颜色设置 { SetConsoleTextAttribute(GetStdHandle(STD_OUTPUT_HANDLE), c); } void first() { for (int i = 0; i < ROW; i++) { for (int j = 0; j < COL; j++) { if (i == 0 || i == ROW - 1 || j == 0 || j == COL - 1) { color(191); CursorJump(j, i); printf("*"); face[i * COL + j] = WALL; } else { color(0); CursorJump(j, i); printf(" "); face[i * COL + j] = NT; } } } color(7); //颜色设置为白色 CursorJump(0, ROW); printf("当前得分:%d", grade); CursorJump(COL, ROW); printf("历史最高得分:%d", max); } void cleanfp()//清理 { free(face); face = NULL; free(arr); arr = NULL; } void snakepr(int i)//打印蛇或者删除 { if (i) { CursorJump(st.x, st.y); printf("@"); face[COL * st.y + st.x] = TOU; for (int j = 0; j < st.l; j++) { face[COL * arr[j].y + arr[j].x] = BODY; CursorJump(arr[j].x, arr[j].y); printf("*"); } } else { CursorJump(st.x, st.y); face[COL * st.y + st.x] = NT; printf(" "); for (int j = 0; j < st.l; j++) { face[COL * arr[j].y + arr[j].x] = NT; CursorJump(arr[j].x, arr[j].y); printf(" "); } } } void foodput()//生成食物 { int x, y; do { x = rand() % (COL-2)+1; y = rand() % (ROW-2)+1; } while (face[y * COL + x] != NT); CursorJump(x, y); printf("$"); face[y * COL + x] = FOOD; } int judge(int x,int y) { if (face[COL * (st.y + y) + st.x + x] == WALL || face[COL * (st.y + y) + st.x + x] == BODY) return 1; if (face[COL * (st.y + y) + st.x + x] == FOOD) { foodput(); face[COL * (st.y + y) + st.x + x] = NT; st.l++; grade += 10; color(7); //颜色设置为白色 CursorJump(0, ROW); printf("当前得分:%d", grade); return 2; } if (face[COL * (st.y + y) + st.x + x] == NT) { face[COL * arr[st.l - 1].y + arr[st.l - 1].x]; return 2; } } void WriteGrade() { FILE* fp = fopen("grade.txt", "w"); if (fp == NULL) { printf("写入历史分数失败"); return; } fprintf(fp, "%d", grade); fclose(fp); fp = NULL; } void move(int x, int y) { if (judge(x, y) == 1) {//撞墙或生体 Sleep(500); //留给玩家反应时间 system("cls"); //清空屏幕 color(7); //颜色设置为白色 CursorJump(2 * (COL / 3), ROW / 2 - 3); if (grade > max){ printf("恭喜你打破最高记录,最高记录更新为%d", grade); WriteGrade(); } else if (grade == max){ printf("与最高记录持平,加油再创佳绩"); } else{ printf("请继续加油,当前与最高记录相差%d", max - grade); } CursorJump(2 * (COL / 3), ROW / 2); printf("GAME OVER"); while (1) //询问玩家是否再来一局 { char ch; CursorJump(2 * (COL / 3), ROW / 2 + 3); printf("再来一局?(y/n):"); ch = _getch(); if (ch == 'y' || ch == 'Y'){ system("cls"); main(); } else if (ch == 'n' || ch == 'N'){ CursorJump(2 * (COL / 3), ROW / 2 + 5); exit(0); } else{ CursorJump(2 * (COL / 3), ROW / 2 + 5); printf("选择错误,请再次选择"); } } } if (judge(x, y) == 2) { snakepr(0); for (int i = st.l; i > 0; i--) { arr[i].x = arr[i - 1].x; arr[i].y = arr[i - 1].y; } arr[0].x = st.x; arr[0].y = st.y; face[COL * st.y + st.x] = BODY; st.x += x; st.y += y; face[COL * st.y + st.x] = TOU; snakepr(1); } } void ReadGrade() { FILE* fp = fopen("grade.txt", "r"); if (fp == NULL) { WriteGrade(); return; } if(fscanf(fp,"%d", & max)!=1) printf("读取最高分失败"); fclose(fp); fp = NULL; } void initsnake() { st.l = 2; st.x = COL / 2; st.y = ROW / 2; arr[0].x = st.x - 1; arr[0].y = st.y; arr[1].x = st.x - 2; arr[1].y = st.y; snakepr(1); } void gamebegin() { initsnake(); // 初始化蛇 foodput(); // 初始化食物 char f = 'd'; // 初始方向向右 while (1) { if (_kbhit()) { // 检测是否有按键输入 char temp = _getch(); if (temp == 'p' || temp == 'P') { // 检测是否按下暂停键 CursorJump(COL / 2 - 5, ROW / 2); printf("游戏暂停,按任意键继续..."); _getch(); // 等待任意键继续 CursorJump(COL / 2 - 5, ROW / 2); printf(" "); // 清空暂停信息 } else if ((temp == 'w' && f != 's') || (temp == 's' && f != 'w') || (temp == 'a' && f != 'd') || (temp == 'd' && f != 'a')) { f = temp; // 更新方向 } } // 根据当前方向更新蛇的移动 switch (f) { case 'w': move(0, -1); break; case 's': move(0, 1); break; case 'a': move(-1, 0); break; case 'd': move(1, 0); break; } Sleep(200); // 控制蛇的移动速度 } }

详细思路

1.fun.h

自定义一个头文件包含操作的函数,让主函数直接引用即可调用,在多模块的项目中会用到,不过在本项目好处不是那么明显。

2.fun.c

a.定义与声明

cpp
#define _CRT_SECURE_NO_WARNINGS 1//这一行禁用微软的安全性检测,让我们可以使用scanf等函数 #include <stdio.h> #include <Windows.h> #include <stdlib.h> #include <time.h> #include <conio.h> #define NT 0//没有任何物体的安全空间 #define WALL 1//边界,碰到就判定游戏结束 #define TOU 2//头 #define BODY 3//身体 #define FOOD 4//食物 extern int main();//重新开始游戏需要重新运行主函数,所以用extern声明一下 int ROW;//行 int COL;//列,由于行列在不同模式中大小不同所以就不用宏定义了 int* face = NULL; int max, grade;//最高分和当前分数 struct snake { int l;//长度(不包括头) int x;//坐标x int y;//坐标y }st;//这个结构体包含了蛇头的坐标和蛇的长度 struct body//身体 { int x; int y; }*arr;//有很多个身体所以用一个指针方面后续开辟空间

b.光标的隐藏和跳转

cpp
void HideCursor()//隐藏光标 { CONSOLE_CURSOR_INFO curInfo; curInfo.dwSize = 1; curInfo.bVisible = FALSE; HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorInfo(handle, &curInfo); } void CursorJump(int x, int y)//光标跳转 { COORD pos; pos.X = x; pos.Y = y; HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE); SetConsoleCursorPosition(handle, pos); }//在函数中输入坐标以实现跳转

c.模式选择

cpp
void modchoose(int a) { if (a == 49) { ROW = 20; COL = 40; } if (a == 50) { ROW = 25; COL = 50; } if (a == 51) { ROW = 30; COL = 60; } face = (int*)malloc(sizeof(int) * ROW * COL); arr = (struct body*)malloc(sizeof(struct body) * ROW * COL); }

这里让用户输入一个数字(在主函数中取值,因为是字符串所以是ASCII值),然后再定义行和列的大小,最后开辟足量的空间来储存空间小格子和蛇身。

d.初始化

cpp
void first() { for (int i = 0; i < ROW; i++) { for (int j = 0; j < COL; j++) { if (i == 0 || i == ROW - 1 || j == 0 || j == COL - 1) { color(191); CursorJump(j, i); printf("*"); face[i * COL + j] = WALL; } else { color(0); CursorJump(j, i); printf(" "); face[i * COL + j] = NT; } } } color(7); //颜色设置为白色 CursorJump(0, ROW); printf("当前得分:%d", grade); CursorJump(COL, ROW); printf("历史最高得分:%d", max); }

用“*****”来表示墙和蛇的身体

e.蛇的打印和删除

cpp
void snakepr(int i)//打印蛇或者删除 { if (i) { CursorJump(st.x, st.y); printf("@"); face[COL * st.y + st.x] = TOU; for (int j = 0; j < st.l; j++) { face[COL * arr[j].y + arr[j].x] = BODY; CursorJump(arr[j].x, arr[j].y); printf("*"); } } else { CursorJump(st.x, st.y); face[COL * st.y + st.x] = NT; printf(" "); for (int j = 0; j < st.l; j++) { face[COL * arr[j].y + arr[j].x] = NT; CursorJump(arr[j].x, arr[j].y); printf(" "); } } }

这里输入1即条件为真,打印蛇,输入0则删除蛇。首先根据在蛇头结构体中的蛇头位置将光标移动过去打印出来,以“@”表示蛇头,当然读者可以换成自己喜欢的。如果想换颜色可以在printf之前调用color函数。然后再用循环的方式打印蛇身,循环次数由蛇头结构体中储存的长度信息确定。删除蛇和打印的操作类似,只不过是用空格代替了“@”和“*”。

并且无论是打印还是删除,都不要忘记对新位置进行重新标记,是身体还是空格等等。

f.食物生成

cpp
void foodput()//生成食物 { int x, y; do { x = rand() % (COL-2)+1; y = rand() % (ROW-2)+1; } while (face[y * COL + x] != NT); CursorJump(x, y); printf("$"); face[y * COL + x] = FOOD; }

先用rand函数随机选取食物生成的位置,然后判断这个位置是不是空,如果是空的位置(即face[y * COL + x] == NT),则在此处生成食物,用“$”表示,并更新face数组标记该位置为FOOD。通过do-while循环确保食物不会生成在墙、蛇身或蛇头的位置。COL-2ROW-2的作用是让食物始终生成在游戏区域内部,避免与边界重叠。


g.碰撞检测与移动逻辑

碰撞检测函数judge()
c
int judge(int x, int y) { if (face[COL * (st.y + y) + st.x + x] == WALL || face[COL * (st.y + y) + st.x + x] == BODY) return 1; // 撞墙或撞身体,游戏结束 if (face[COL * (st.y + y) + st.x + x] == FOOD) { // 吃到食物 foodput(); // 生成新食物 face[COL * (st.y + y) + st.x + x] = NT; // 原食物位置恢复为空 st.l++; // 蛇身长度增加 grade += 10; // 分数增加 color(7); // 恢复白色 CursorJump(0, ROW); printf("当前得分:%d", grade); // 更新分数显示 return 2; // 正常移动标记 } if (face[COL * (st.y + y) + st.x + x] == NT) return 2; // 正常移动标记 }

此函数用于判断蛇头下一步的位置状态:

  1. 碰撞检测:若下一步是墙(WALL)或蛇身(BODY),返回1触发游戏结束逻辑。
  2. 吃到食物:若下一步是食物(FOOD),生成新食物、增加蛇长和分数,并返回2标记正常移动。
  3. 空位置:直接返回2,允许蛇继续移动。

移动函数move()
c
void move(int x, int y) { if (judge(x, y) == 1) { // 碰撞检测失败 Sleep(500); // 暂停供玩家反应 system("cls"); // 清屏 color(7); // 显示游戏结束信息与分数比较逻辑 // ... while (1) { // 询问是否重玩 // 玩家输入处理逻辑 } } if (judge(x, y) == 2) { // 正常移动 snakepr(0); // 删除旧蛇 // 更新蛇身坐标:后一节移动到前一节的位置 for (int i = st.l; i > 0; i--) { arr[i].x = arr[i - 1].x; arr[i].y = arr[i - 1].y; } // 更新蛇头坐标 arr[0].x = st.x; arr[0].y = st.y; st.x += x; st.y += y; snakepr(1); // 绘制新蛇 } }

移动逻辑分为两步:

  1. 删除旧位置:调用snakepr(0)清空当前蛇的位置。
  2. 更新坐标
    • 蛇身逐节向前移动,最后一节的位置被舍弃(若未吃到食物)。
    • 蛇头根据方向(xy的增量)更新坐标。
  3. 绘制新位置:调用snakepr(1)重新渲染蛇。

h.分数记录与文件操作

最高分读取ReadGrade()
c
void ReadGrade() { FILE* fp = fopen("grade.txt", "r"); if (fp == NULL) { // 文件不存在时创建 WriteGrade(); // 初始化文件 return; } if (fscanf(fp, "%d", &max) != 1) printf("读取最高分失败"); fclose(fp); fp = NULL; // 防止野指针 }

grade.txt读取历史最高分。若文件不存在,则调用WriteGrade()创建新文件并初始化。

最高分写入WriteGrade()
c
void WriteGrade() { FILE* fp = fopen("grade.txt", "w"); if (fp == NULL) { printf("写入历史分数失败"); return; } fprintf(fp, "%d", grade); // 写入当前分数 fclose(fp); fp = NULL; }

将当前分数写入文件,仅在游戏结束时且分数超过历史记录时调用。


i.游戏主循环与输入处理

c
void gamebegin() { initsnake(); // 初始化蛇 foodput(); // 生成食物 char f = 'd'; // 初始方向向右 while (1) { if (_kbhit()) { // 检测按键 char temp = _getch(); if (temp == 'p') { // 暂停功能 // 显示暂停信息并等待输入 } else if (/* 方向键合法性检查 */) { f = temp; // 更新方向 } } // 根据方向调用move() switch (f) { case 'w': move(0, -1); break; // 上 case 's': move(0, 1); break; // 下 case 'a': move(-1, 0); break; // 左 case 'd': move(1, 0); break; // 右 } Sleep(200); // 控制移动速度 } }

主循环通过_kbhit()实时检测键盘输入:

  1. 暂停功能:按下P键暂停游戏,等待任意键继续。
  2. 方向控制:根据WASD更新移动方向,并禁止反向移动(如向右时不能立即向左)。
  3. 移动速度:通过Sleep(200)控制蛇的移动间隔,数值越大速度越慢。

已知问题与优化方向

已知问题

  1. 内存泄漏:游戏结束时未释放arrface指向的内存(已在cleanfp()中修复)。
  2. 界面闪烁:频繁调用system("cls")可能导致画面闪烁,可改用局部刷新优化。

扩展优化

  1. 多关卡设计:增加不同地图模板或障碍物。
  2. 音效支持:通过PlaySound()函数添加吃食物或碰撞音效。
  3. 图形界面:迁移至EasyX等图形库实现更丰富的视觉效果。

结语

通过本项目的实践,读者可以深入掌握C语言的模块化编程、内存管理、文件操作与控制台交互技巧。贪吃蛇虽然简单,但是涵盖了程序设计的核心思想:数据驱动逻辑,状态决定行为。建议在此基础上尝试添加新功能,比如存档读档、难度分级等,进一步巩固编程能力。PS:这是我第一次写博客,代码可能不太规范,以后会越来越好。


编辑——Molirain-jy

如果对你有用的话,可以打赏哦
打赏
ali pay
wechat pay

本文作者:Molirain-jy

本文链接:

版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!