作为C语言初学者,在学完基础操作之后,可能每一个部分都会,但还不会融会贯通。可能常常会想C语言到底能干什么?经典游戏贪吃蛇,作为C语言入门的典型案例。在其中,我们用到了程序设计基础中所学习的几乎所有知识,并且严格按照msvc编译器的标准编写,保证了在Windows系统运行的稳定性。由于微软的标准并不支持VLA(虽然这个标准在C11中被改为了可选项),我们使用了malloc函数,这样除了单链表以外,每一章节的知识点均有涉及。
首先定义全局变量,蛇头、蛇身,均采用结构体。再定义边界与游戏区域,使用二维数组。并且使用宏定义区分边界与空的空间,提高代码的可读性与维护性。为了直观地展现程序运行逻辑,我使用了模块化编程,将主函数单独放在一个cpp文件中(c和cpp文件均可,不会影响本文代码)。
接着,定义每个功能的函数。初始化、移动、判断结束、打印删除、分数累计、最高分保存、游戏。这些函数的具体逻辑已经体现在了源代码中,这里不再过多阐述。然后在主函数中按需调用,组成逻辑完整的程序即可。
在编写中,同样的,光标等问题不可忽视。我们将使用Windows API里包含的库函数,以实现隐藏光标,光标跳转,打印颜色改变的操作。同样的,这些操作被封装成自定义函数。
最后进行一些已知的bug修复。
接下来先看源代码。
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();
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;
}
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); // 控制蛇的移动速度
}
}
自定义一个头文件包含操作的函数,让主函数直接引用即可调用,在多模块的项目中会用到,不过在本项目好处不是那么明显。
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;//有很多个身体所以用一个指针方面后续开辟空间
cppvoid 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);
}//在函数中输入坐标以实现跳转
cppvoid 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值),然后再定义行和列的大小,最后开辟足量的空间来储存空间小格子和蛇身。
cppvoid 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);
}
用“*****”来表示墙和蛇的身体
cppvoid 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函数。然后再用循环的方式打印蛇身,循环次数由蛇头结构体中储存的长度信息确定。删除蛇和打印的操作类似,只不过是用空格代替了“@”和“*”。
并且无论是打印还是删除,都不要忘记对新位置进行重新标记,是身体还是空格等等。
cppvoid 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-2
和ROW-2
的作用是让食物始终生成在游戏区域内部,避免与边界重叠。
judge()
cint 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; // 正常移动标记
}
此函数用于判断蛇头下一步的位置状态:
WALL
)或蛇身(BODY
),返回1
触发游戏结束逻辑。FOOD
),生成新食物、增加蛇长和分数,并返回2
标记正常移动。2
,允许蛇继续移动。move()
cvoid 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); // 绘制新蛇
}
}
移动逻辑分为两步:
snakepr(0)
清空当前蛇的位置。x
和y
的增量)更新坐标。snakepr(1)
重新渲染蛇。ReadGrade()
cvoid 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()
cvoid WriteGrade()
{
FILE* fp = fopen("grade.txt", "w");
if (fp == NULL) {
printf("写入历史分数失败");
return;
}
fprintf(fp, "%d", grade); // 写入当前分数
fclose(fp);
fp = NULL;
}
将当前分数写入文件,仅在游戏结束时且分数超过历史记录时调用。
cvoid 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()
实时检测键盘输入:
P
键暂停游戏,等待任意键继续。WASD
更新移动方向,并禁止反向移动(如向右时不能立即向左)。Sleep(200)
控制蛇的移动间隔,数值越大速度越慢。arr
和face
指向的内存(已在cleanfp()
中修复)。system("cls")
可能导致画面闪烁,可改用局部刷新优化。PlaySound()
函数添加吃食物或碰撞音效。通过本项目的实践,读者可以深入掌握C语言的模块化编程、内存管理、文件操作与控制台交互技巧。贪吃蛇虽然简单,但是涵盖了程序设计的核心思想:数据驱动逻辑,状态决定行为。建议在此基础上尝试添加新功能,比如存档读档、难度分级等,进一步巩固编程能力。PS:这是我第一次写博客,代码可能不太规范,以后会越来越好。
编辑——Molirain-jy
本文作者:Molirain-jy
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!