【C语言】实践:贪吃蛇小游戏(附源码)

  欢迎光顾我的homepage

前言

        贪吃蛇小游戏想必大家都玩过吧,现在就要C语言代码来实现一下贪吃蛇小游戏

在实现之前,我们要对C语言结构体、指针、链表(单链表)有一定的基础

先来看一下预期运行效果

一、Win32 API

        这里实现贪吃蛇游戏会使用一些Win32 API的知识,这里简单学习一下

        Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外,它也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、绘制图形、使用周边设备等目的,由于这些函数服务的对象是应用程序,所以便称之为Appliccation Programming Interface,简称API。WIN32 API也就是Microsoft Windows32位平台的应用程序编程接口。

1.1 控制台程序

        在我们的电脑中,windows系统使用快捷键win + R可以打开一个窗口,然后输入cmd就可以打开一个控制台程序,这个控制台可以输入一些命令来控制我们的电脑,这里输入cmd即可打开一个控制台程序窗口

        1.1.1 设置控制台程序

        本次贪吃蛇小游戏是在VS2022上来实现的,平常我们运行起来的黑框程序就是控制台层序

在VS2022上运行默认是以下情况

这里就需要先修改一个控制台

调出控制台(这里可以使用Win+R,输入cmd调出窗口),点击设置

在默认终端应用程序这里设置成Windows 控制台主机(默认是Windows 终端),点击保存

设置完成后,就是以下这种界面了

        1.1.2 设置控制台程序大小

这里我们控制台程序是默认大小,这里我们自己设置控制台程序大小,这里使用cmd控制台程序设置窗口的大小(设置大小为行33,列100)

mode con cols=100 lines=33

        1.1.3 设置控制台程序名称

我们设置控制台名称为 贪吃蛇,使用title 指令

title 贪吃蛇

        当然,这些能够在控制台窗口执行的命令,也可以通过调用C语言的system函数在中来完成

这里再补充一个指令,暂停控制台程序

system("pause");

        这个指令可以暂停程序运行,并会提示按下任意键继续...

int main()
{system("mode con cols=100 lines=33");system("title 贪吃蛇");system("pause");return 0;
}

        1.1.4 控制台屏幕上的坐标

COORD是Windows API中自定义的一个结构体,表示一个字符在控制台屏幕缓冲区的坐标,坐标(0,0)的原点位于缓冲区的顶部左侧单元格。

COORD类型声明

typedef struct _COORD {SHORT X;SHORT Y;
} COORD, *PCOORD;

给坐标赋值

 COORD pos = { 10, 15 };

GetStdHandle

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

HANDLE WINAPI GetStdHandle(_In_ DWORD nStdHandle);

函数参数

函数使用

	HANDLE hOutput = NULL;//获取标准输出的句柄(用来标识不同设备的数值)hOutput = GetStdHandle(STD_OUTPUT_HANDLE);

GetConsoleCursorInfo

        GetConsoleCursorInfo函数检索有关指定控制台屏幕缓冲区的光标的大小和可见性的信息

函数语法

BOOL WINAPI GetConsoleCursorInfo(_In_  HANDLE               hConsoleOutput,_Out_ PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标(光标)的信息

        CONSOLE_CURSOR_INFO结构体

        这个结构体包含了有关控制台光标的信息

typedef struct _CONSOLE_CURSOR_INFO {DWORD dwSize;BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;

        dwSize 由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从完全填充单元格到单元格底部的水平线条

        bVisible 游标的可见性。如果光标可见,则此成员为true;如果不可见,此成员为false

函数参数

        这里就用到上面GetStdHandle函数获得的句柄了,还需要用到CONSOLE_CURSOR_INFO结构体(注意,这里第二个参数是指针)

函数使用

HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息

SetConsoleCursorInfo

        SetConsoleCursorInfo函数设置指定控制台屏幕缓冲区的光标的大小和可见性

函数参数

BOOL WINAPI SetConsoleCursorInfo(_In_       HANDLE              hConsoleOutput,_In_ const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);

        与GetConsoleCursorInfo函数参数相同         

函数使用

HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//影藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态

SetConsoleCursorPosition

        SetConsoleCursorPosition函数设置指定控制台屏幕缓冲区的位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。

BOOL WINAPI SetConsoleCursorPosition(_In_ HANDLE hConsoleOutput,_In_ COORD  dwCursorPosition
);

函数参数

函数使用

	HANDLE hOutput = NULL;//获取标准输出的句柄(用来标识不同设备的数值)hOutput = GetStdHandle(STD_OUTPUT_HANDLE);COORD pos = { 10, 5 };//设置标准输出上光标的位置为posSetConsoleCursorPosition(hOutput, pos);

这里为了方便后面定位屏幕坐标,单独封装一个函数来实现

void SetPos(short x, short y)
{COORD pos = { x, y };HANDLE hOutput = NULL;//获取标准输出的句柄(用来标识不同设备的数值)hOutput = GetStdHandle(STD_OUTPUT_HANDLE);//设置标准输出上光标的位置为posSetConsoleCursorPosition(hOutput, pos);
}

GetAsyncKeyState

        GetAsyncKeyState函数获得按键情况

SHORT GetAsyncKeyState([in] int vKey
);

函数参数

        这里函数参数是虚拟键码。

这里仅列出一些在游戏中可能用到的按键的虚拟键码,可以点击查看详细虚拟键码

VK_UP0x26
VK_DOWN0x28
VK_LEFT0x25
VK_RIGHT0x27
VK_F30x72F3
VK_F40x73F4
VK_ESCAPE0x1BEsc
VK_SPACE0x20空格

函数返回值

        GetAsyncKeyState 函数返回值是short类型,在上一次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中,如果最高位是1,说明按键的状态是按下,如果最高位是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0。

        在游戏中我们需要检测一个按键是否被按过,就检测 GetAsyncKeyState 函数返回值的最低值是否是1,可以写一个宏来实现:

#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

二、<locale.h>本地化

在贪吃蛇游戏中,我们会涉及到墙体□ 和蛇的身体● 的打印,而在VS中我们输出出来的是?

这就是因为没有本地化设置,无法输出这些特殊字符(宽字符)。

        我们需要通过修改地区,让程序来适应不同的区域,我们就需要进行本地化设置

这里就要使用到C语言中的库函数  setlocale 函数

在C标准中,依赖地区的部分有以下几项

数字量的格式

货币量的格式

字符集

日期和时间的表示形式     

        通过修改地区,程序可以改变它的行为来适应世界的不同地域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类型进行修改,下面的一个宏就指定一个类型。 

LC_COLLATE :影响字符串表函数 strcoll strxfrm

LC_CTYPE : 影响字符处理函数的行为。

LC_MONETARY : 影响货币格式。

LC_NUMERIC : 影响 printf 的数字格式。

LC_TIME : 影响时间格式 strftimewcsftime  。

LC_ALL : 针对所有类型修改,将以上所有类别设置为给定的语言环境。

每一个类别都有详细说明,这里就不一一讲解了

        2.1 setlocale 函数

setlocale 函数用于修改当前地区,可以针对一个类项进行修改,也可以针对所有类项

函数的第一个参数可以是前面类项中的一个,也可以是LC_ALL(影响所以的类项)

函数的第二个参数

        C标准给第二个参数仅定义了2种可能取值:"C"(正常模式)和" "(本地化模式)

这里我们需要进行本地化设置

 setlocale(LC_ALL, " ");//切换到本地环境

        2.2 宽字符的打印

在屏幕中,我们需要打印宽字符        

        宽字符的字面量必须加上前缀“L”,否则C语言就会把字符量当成窄字符来处理。前缀“L”子啊单引号(或者双引号)的前面,表示宽字符,对于 wprintf 的占位符是 %lc;双引号的前面,对于wprintf 的占位符是 %ls。

可以看到,这里宽字符占两个窄字符的位置。

三、游戏分析和设计

        3.1 贪吃蛇数据结构设计

        在游戏运行的过程中,蛇每吃一次食物,蛇的身体就会变长;这样我们就可以使用链表来存储蛇的信息,蛇的每一个节身体其实就是链表的一个节点。每个节点只需记录蛇身节点在地图上的坐标就可以。

        蛇身节点的结构:

typedef struct Snakenode
{int x;int y;struct Snakenode* next;
}Snakenode, * pSnakenode;
//这里也可以写 typedef Snakenode* pSnakenode

        接下来,我们还需要记录游戏过程中的相关信息

贪吃蛇,食物的位置,蛇的方向,游戏状态,当前的分数,每一个食物的分数,蛇的速度等

而这里蛇的方向和游戏状态都可以一一列举出来,这里就使用枚举变量

//蛇的方向
enum DIRECT
{UP = 1,DOWN,LEFT,RIGHT
};
//蛇的状态——游戏状态
//正常、撞墙、撞到自己、正常退出
enum GAME_STATE
{OK,KILL_WALL,KILL_SELF,NORMAL_END
};
//贪吃蛇的相关信息
typedef struct Snake
{pSnakenode psnake; //指向蛇头部的指针pSnakenode pfood;   //指向食物的指针enum DIRECT dir;//蛇的方向enum GAME_STATE state;//蛇的状态int food_scores;//每个食物的分数int all_scores; //总分数int sleep_time;  //休息的时间 --即蛇的速度
}Snake;
typedef Snake* pSnake;

这样,我们就创建了一个Snake结构体来维护游戏相关信息(维护整条贪吃蛇)

        3.2 游戏流程分析

游戏大概分析如下

四、游戏逻辑实现

        程序开始就设置程序本地化,然后就进入到游戏的主逻辑当中

根据游戏大概分析,游戏可以分为三个阶段

阶段一:游戏开始 --- 完成游戏的初始化 

阶段二:游戏运行 --- 完成游戏运行逻辑的实现

阶段三:游戏结束 --- 完成游戏结束的说明,实现资源释放

        当然,这里我们玩完一局游戏后,可以选择继续或者结束(这里就以输入Y/N来判断游戏是否继续运行)

这里我们在测试test.c文件开始就让程序本地化

void test()
{Snake snake = { 0 };int ch = 0;do{ch = 0;system("cls");//游戏初始化GameStart(&snake);//游戏运行GameRun(&snake);//游戏结束GameOver(&snake);KeyFun();SetPos(30, 20);wprintf(L"再来一局吗? (Y/N)");ch = getchar();while (getchar() != '\n');} while (ch == 'Y' || ch == 'y');SetPos(0, 27);
}
int main()
{//本地化setlocale(LC_ALL, "");srand((unsigned int)time(NULL));test();//KeyFun();return 0;
}

测试大概框架就是这样,接下来就分别来实现这些框架的内容

        4.1 游戏开始(GameStart)

1. 设置控制台大小和名字

这里设置控制台大小,100列,33行;设置控制台名称为:贪吃蛇

	//设置窗口名称大小system("title 贪吃蛇");system("mode con cols=100 lines=33");

2. 隐藏屏幕光标

隐藏屏幕光标,这里就用到了前面Win32 API的知识

	//隐藏光标HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(houtput, &CursorInfo);//获得有关指定控制台屏幕缓冲区的光标大小和可见的信息CursorInfo.bVisible = false; //隐藏控制台光标SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态	

3. 打印欢迎界面

输出欢迎界面,这里分装成函数WelcomeToGame

        我们观察欢迎界面,可以发现这里并不是在坐标为(0,0)处打印的,这里就要用到设置光标位置(这里单独写一个函数,设置光标位置)

设置光标位置

//设置光标位置
void SetPos(int x, int y)
{HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);COORD pos = { x,y };SetConsoleCursorPosition(houtput, pos);
}

接下来就是游戏欢迎界面的打印,这里中间会用到 pause 和 cls(清理屏幕) 指令

//欢迎界面打印
void WelcomeToGame()
{//设置光标位置SetPos(40, 15);printf("欢迎进入贪吃蛇小游戏\n");SetPos(42, 20);system("pause");system("cls");//清理屏幕SetPos(20, 11);printf("请使用↑ 、 ↓ 、 ← 、 → 来控制贪吃蛇的移动,按F3加速、F4减速 ");SetPos(20, 13);printf("加速可以获得更多的分数");SetPos(20, 15);system("pause");system("cls"); //清理屏幕
}

这样就可以实现预期效果图那样了,接下来就是绘制我们贪吃蛇游戏的地图了。

4. 绘制地图

这里我们使用宽字符来打印地图,先来看一下预期效果

我们把地图分为上、下、左、右这四个部分,这样我们只需依次打印这些宽字符就可以了

//地图绘制
void CreatMap()
{//上int i = 0;for (i = 0; i < 29; i++){wprintf(L"%lc", WALL);}//下SetPos(0, 26);for(i = 0; i < 29; i++){wprintf(L"%lc", WALL);}//左for (i = 1; i < 26; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右for (i = 1; i < 26; i++){SetPos(56, i);wprintf(L"%lc", WALL);}
}

5. 初始化贪吃蛇

初始化贪吃蛇,也是创建贪吃蛇,贪吃蛇身体这里其实就是一个链表,里面存放着每个节点的坐标

        初始化贪吃蛇也要给上一些初始数据

初始长度为  --  5

初始方向  --  向右(RIGHT)

初始状态  --  正常(OK)

每个食物得分  --  10

初始总分  --  0

初始速度  --  这里设定眠时间为200毫秒

初始蛇的位置  --  这里就随机生成(也可以指定)

当然初始指向食物的指针置为NULL(因为这里还未创建食物)

//创建贪吃蛇
void InitSnake(pSnake ps)
{//创建蛇的身体pSnakenode pcur = NULL;int i = 0;int x, y;//蛇初始位置do{x = rand() % 31 + 4; //x: 4 - 34y = rand() % 20 + 2; //y: 1 - 25} while (x % 2 != 0);for (i = 0; i < 5; i++){pcur = (pSnakenode)malloc(sizeof(Snakenode));if (pcur == NULL){perror("InitSnake()::malloc()");return;}pcur->next = NULL;//pcur->x = SNAKE_X + i * 2;//pcur->y = SNAKE_Y;pcur->x = x + i * 2;pcur->y = y;//头插到贪吃蛇链表中if (ps->psnake == NULL) //链表为空{ps->psnake = pcur;}else{pcur->next = ps->psnake;ps->psnake = pcur;}}//输出蛇的初始位置pcur = ps->psnake;while (pcur){SetPos(pcur->x, pcur->y);wprintf(L"%lc", SNAKENODE);pcur = pcur->next;}//初始化贪吃蛇的信息ps->dir = RIGHT; //蛇的方向ps->pfood = NULL; //指向食物 --NULL  ps->state = OK;  //状态ps->food_scores = 10; //每个食物的得分ps->all_scores = 0;  //总分ps->sleep_time = 200;//速度,即休息时间 单位是毫秒//getchar();
}

6. 创建食物

        创建完贪吃蛇,接下来就是创建食物了,其实食物和贪吃蛇身体节点一样,都存放着坐标;所以这里就创建一个结构体,再随机生成坐标
        这里需要注意:

        坐标x必须是偶数

        坐标必须在地图内

        生成食物的坐标不能与蛇的身体重复

//创建食物
void CreatFood(pSnake ps)
{int x, y;//随机生成坐标 x , y //x 2-54//y 1-25again:do {x = rand() % 53 + 2;y = rand() % 25 + 1;} while (x % 2 != 0);//x y 不能与贪吃蛇身体重复pSnakenode pcur = ps->psnake;while (pcur){if (x == pcur->x && y == pcur->y){goto again;}pcur = pcur->next;}	pSnakenode food = (pSnakenode)malloc(sizeof(pSnakenode));if (food == NULL){perror("CreatFood()::malloc");return;}food->x = x;food->y = y;food->next = NULL;ps->pfood = food;SetPos(x, y);wprintf(L"%lc", FOOD);//getchar();
}

这样我们的初始化就完成了

        4.2 游戏运行(GameRun)

1.输出右侧提示信息和分数详情

看预期效果图,我们在地图的右侧输出一些提示信息,并且输出当前得分详情

void Printgame(pSnake ps) {SetPos(60, 15);printf("请使用↑ 、 ↓ 、 ← 、 → 来控制贪吃蛇");SetPos(60, 16);printf("按F3加速、F4减速 ");SetPos(60, 18);printf("加速可以获得更高的分数 ");SetPos(60, 20);printf("ESC:退出游戏  space:暂停 ");SetPos(60, 10);printf("当前总得分:%d", ps->all_scores);SetPos(60, 12);printf("当前每个食物得分:%d", ps->food_scores);SetPos(60, 22);printf("努力学习的小廉");
}

2. 获取按键情况

现在就要获取我们键盘按键的信息了,这里写一个宏来判断按键是否被按过

#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

这里把获取按键信息直接写到游戏运行这个函数内,顺便看一下游戏运行都需要实现哪些东西?

//游戏运行
void GameRun(pSnake ps)
{do{Printgame(ps);//判断按键是否被按过if (KEY_PRESS(VK_UP) && ps->dir != DOWN){ps->dir = UP;}else if(KEY_PRESS(VK_DOWN) && ps->dir != UP){ps->dir = DOWN;}else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT) {ps->dir = LEFT;}else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT) {ps->dir = RIGHT;}else if (KEY_PRESS(VK_SPACE)) //空格 -- 暂停{Pause();}else if (KEY_PRESS(VK_ESCAPE)) //游戏正常退出{ps->state = NORMAL_END;break;}else if (KEY_PRESS(VK_F3)){if (ps->sleep_time >= 100){ps->sleep_time -= 50;ps->food_scores += 5;//设定食物分数最高25}}else if (KEY_PRESS(VK_F4)){if (ps->sleep_time < 300){ps->sleep_time += 100;ps->food_scores -= 5;//⼀个⻝物分数最低是5分}}Sleep(ps->sleep_time);//贪吃蛇的移动SnakeMove(ps);//判断贪吃蛇是否撞墙KillByWall(ps);//判断贪吃蛇是否撞到自己KillBySelf(ps);} while (ps->state == OK);}

这里当游戏状态不是正常运行时,就结束了循环(即游戏结束)

3. 贪吃蛇移动

        看上述游戏运行代码,可以看到贪吃蛇的移动还有判断蛇是否撞到墙和自己,这些的实现在贪吃蛇移动当中。

        1> 蛇身的移动

        蛇身的移动,其实就是根据当前蛇的方向,找到下一个节点,再判断下一个节点是否是食物,和判断是否撞到墙和自己

        2> 判断是否吃到食物

        判断蛇的下一个节点是否是食物,就是判断下一个位置的坐标和实物的坐标是否重复

如果重复,就让蛇身变长一节,如果不是,就让蛇往前走

这里蛇移动还有一些知识,就是直接为蛇下一个位置创建一个新的节点

        再判断下一个位置是否是食物,如果是就将节点头插到蛇身链表中,不删除尾节点;如果不是就直接将节点头插到蛇身链表中,删除尾节点(这里还需在蛇的尾部输出两个空格"  ")

//下一个位置是食物
void IsFood(pSnakenode next, pSnake ps)
{//把下一个位置的节点头插到贪吃蛇中next->next = ps->psnake;ps->psnake = next;//打印贪吃蛇pSnakenode cur = ps->psnake;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", SNAKENODE);cur = cur->next;}ps->all_scores += ps->food_scores;CreatFood(ps);//SetPos(ps->pfood->x, ps->pfood->y);//wprintf(L"%lc", FOOD);
}
//下一个位置不是食物
void NoFood(pSnakenode next, pSnake ps)
{//把下一个位置的节点头插到贪吃蛇中next->next = ps->psnake;ps->psnake = next;pSnakenode cur = ps->psnake;while (cur->next->next != NULL){SetPos(cur->x, cur->y);wprintf(L"%lc", SNAKENODE);cur = cur->next;}SetPos(cur->next->x, cur->next->y);wprintf(L"%ls", L"  ");free(cur->next);cur->next = NULL;
}
//贪吃蛇的移动
void SnakeMove(pSnake ps)
{pSnakenode next = (pSnakenode)malloc(sizeof(Snakenode));if (next == NULL){perror("SnakeMove():malloc()");exit(1);}switch (ps->dir){case UP:next->x = ps->psnake->x;next->y = ps->psnake->y - 1;break;case DOWN:next->x = ps->psnake->x;next->y = ps->psnake->y + 1;break;case LEFT:next->x = ps->psnake->x - 2;next->y = ps->psnake->y;break;case RIGHT:next->x = ps->psnake->x + 2;next->y = ps->psnake->y;break;}//判断下一个位置是不是食物if (NextIsFood(next, ps)){IsFood(next, ps);}else {NoFood(next, ps);}
}
        3> 判断是否撞到墙和自己

判断蛇是否撞墙,就是判断蛇身节点的坐标是否超出地图的范围

//判断贪吃蛇是否撞墙
void KillByWall(pSnake ps)
{if (ps->psnake->x == 0 || ps->psnake->x == 56|| ps->psnake->y == 0 || ps->psnake->y == 26)ps->state = KILL_WALL;
}

判断蛇是否撞到自己,就遍历链表,判断蛇身的头结点是否和身体其他节点重复

//判断贪吃蛇是否撞到自己
void KillBySelf(pSnake ps)
{pSnakenode pcur = ps->psnake->next;while (pcur){if (pcur->x == ps->psnake->x && pcur->y == ps->psnake->y){ps->state = KILL_SELF;break;}pcur = pcur->next;}
}

到这里,代码就已经实现的差不多了,接下来就是游戏结束后的一些善后工作

        4.3 游戏结束(GameOver)

1. 打印游戏结束的原因

        游戏结束,打印出来游戏结束的原因,是撞到墙了呢?还是撞到自己了呢?还是按Esc正常退出了呢?

2. 释放蛇身节点

        因为我们蛇身的节点是动态申请的内存,我们需要手动释放掉内存(养成好习惯!)

//游戏结束
void GameOver(pSnake ps)
{SetPos(8, 12);switch(ps->state){case KILL_WALL:wprintf(L"Sorry,game over because you hit the wall !\n");break;case KILL_SELF:wprintf(L"Sorry,game over because you hit youself !\n");break;case NORMAL_END:wprintf(L"Game exits normally !");break;}//释放贪吃蛇的节点内存pSnakenode pcur = ps->psnake;while (pcur){pSnakenode del = pcur;pcur = pcur->next;free(del);}ps->psnake = NULL;
}

到这里我们游戏的代码就已经全部实现了。

源代码

Snake.h

#pragma once
#define _CRT_SECURE_NO_WARNINGS#include<stdio.h>
#include<stdlib.h>
#include<locale.h>
#include<windows.h>
#include<stdbool.h>
#include<wchar.h> 
#include<time.h>
#include<conio.h>//初始数据
#define SNAKE_X 24
#define SNAKE_Y 5
#define WALL L'□'
#define SNAKENODE L'●'
#define FOOD L'★'//蛇的节点
typedef struct Snakenode
{int x;int y;struct Snakenode* next;
}Snakenode;
typedef Snakenode* pSnakenode;//蛇的方向
enum DIRECT
{UP = 1,DOWN,LEFT,RIGHT
};
//蛇的状态——游戏状态
//正常、撞墙、撞到自己、正常退出
enum GAME_STATE
{OK,KILL_WALL,KILL_SELF,NORMAL_END
};//贪吃蛇的相关信息
typedef struct Snake
{pSnakenode psnake; //指向蛇头部的指针pSnakenode pfood;   //指向食物的指针enum DIRECT dir;//蛇的方向enum GAME_STATE state;//蛇的状态int food_scores;//每个食物的分数int all_scores; //总分数int sleep_time;  //休息的时间 --即蛇的速度
}Snake;
typedef Snake* pSnake;//设置光标位置
void SetPos(int x, int y);
//游戏初始化
void GameStart(pSnake ps);
//欢迎界面的输出
void WelcomeToGame();
//地图绘制
void CreatMap();
//创建贪吃蛇
void InitSnake(pSnake ps);
//创建食物
void CreatFood(pSnake ps);//游戏运行
void GameRun(pSnake ps);
//贪吃蛇的移动
void SnakeMove(pSnake ps);
//判断下一个位置是不是食物
int NextIsFood(pSnakenode next , pSnake ps);
//下一个位置是食物,吃掉食物 
void IsFood(pSnakenode next, pSnake ps);
//下一个位置不是食物
void NoFood(pSnakenode next, pSnake ps);
//判断贪吃蛇是否撞墙
void KillByWall(pSnake ps);
//判断贪吃蛇是否撞到自己
void KillBySelf(pSnake ps);
//游戏结束
void GameOver(pSnake ps);void KeyFun();


Snake.c

#include"Snake.h"//设置光标位置
void SetPos(int x, int y)
{HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);COORD pos = { x,y };SetConsoleCursorPosition(houtput, pos);
}//欢迎界面打印
void WelcomeToGame()
{//设置光标位置SetPos(40, 15);printf("欢迎进入贪吃蛇小游戏\n");SetPos(42, 20);system("pause");system("cls");//清理屏幕SetPos(20, 11);printf("请使用↑ 、 ↓ 、 ← 、 → 来控制贪吃蛇的移动,按F3加速、F4减速 ");SetPos(20, 13);printf("加速可以获得更多的分数");SetPos(20, 15);system("pause");system("cls"); //清理屏幕
}
//地图绘制
void CreatMap()
{//上int i = 0;for (i = 0; i < 29; i++){wprintf(L"%lc", WALL);}//下SetPos(0, 26);for(i = 0; i < 29; i++){wprintf(L"%lc", WALL);}//左for (i = 1; i < 26; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右for (i = 1; i < 26; i++){SetPos(56, i);wprintf(L"%lc", WALL);}//system("pause");
}
//创建贪吃蛇
void InitSnake(pSnake ps)
{//创建蛇的身体pSnakenode pcur = NULL;int i = 0;int x, y;//蛇初始位置do{x = rand() % 31 + 4; //x: 4 - 34y = rand() % 20 + 2; //y: 1 - 25} while (x % 2 != 0);for (i = 0; i < 5; i++){pcur = (pSnakenode)malloc(sizeof(Snakenode));if (pcur == NULL){perror("InitSnake()::malloc()");return;}pcur->next = NULL;//pcur->x = SNAKE_X + i * 2;//pcur->y = SNAKE_Y;pcur->x = x + i * 2;pcur->y = y;//头插到贪吃蛇链表中if (ps->psnake == NULL) //链表为空{ps->psnake = pcur;}else{pcur->next = ps->psnake;ps->psnake = pcur;}}//输出蛇的初始位置pcur = ps->psnake;while (pcur){SetPos(pcur->x, pcur->y);wprintf(L"%lc", SNAKENODE);pcur = pcur->next;}//初始化贪吃蛇的信息ps->dir = RIGHT; //蛇的方向ps->pfood = NULL; //指向食物 --NULL  ps->state = OK;  //状态ps->food_scores = 10; //每个食物的得分ps->all_scores = 0;  //总分ps->sleep_time = 200;//速度,即休息时间 单位是毫秒//getchar();
}
//创建食物
void CreatFood(pSnake ps)
{int x, y;//随机生成坐标 x , y //x 2-54//y 1-25again:do {x = rand() % 53 + 2;y = rand() % 25 + 1;} while (x % 2 != 0);//x y 不能与贪吃蛇身体重复pSnakenode pcur = ps->psnake;while (pcur){if (x == pcur->x && y == pcur->y){goto again;}pcur = pcur->next;}	pSnakenode food = (pSnakenode)malloc(sizeof(pSnakenode));if (food == NULL){perror("CreatFood()::malloc");return;}food->x = x;food->y = y;food->next = NULL;ps->pfood = food;SetPos(x, y);wprintf(L"%lc", FOOD);//getchar();
}//游戏初始化
void GameStart(pSnake ps)
{//设置窗口名称大小,隐藏光标system("title 贪吃蛇");system("mode con cols=100 lines=33");//隐藏光标HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(houtput, &CursorInfo);//获得有关指定控制台屏幕缓冲区的光标大小和可见的信息CursorInfo.bVisible = false; //隐藏控制台光标SetConsoleCursorInfo(houtput, &CursorInfo);//设置控制台光标状态	//打印欢迎界面和功能介绍WelcomeToGame();//绘制地图CreatMap();//创建贪吃蛇InitSnake(ps);//创建食物CreatFood(ps);//system("pause");
}
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
//暂停
void Pause()
{while (1){Sleep(200);if (KEY_PRESS(VK_SPACE))break;}
}
//判断下一个位置是不是食物
int NextIsFood(pSnakenode next, pSnake ps)
{return (next->x == ps->pfood->x && next->y == ps->pfood->y);
}
//下一个位置是食物
void IsFood(pSnakenode next, pSnake ps)
{//把下一个位置的节点头插到贪吃蛇中next->next = ps->psnake;ps->psnake = next;//打印贪吃蛇pSnakenode cur = ps->psnake;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", SNAKENODE);cur = cur->next;}ps->all_scores += ps->food_scores;CreatFood(ps);//SetPos(ps->pfood->x, ps->pfood->y);//wprintf(L"%lc", FOOD);
}
//下一个位置不是食物
void NoFood(pSnakenode next, pSnake ps)
{//把下一个位置的节点头插到贪吃蛇中next->next = ps->psnake;ps->psnake = next;pSnakenode cur = ps->psnake;while (cur->next->next != NULL){SetPos(cur->x, cur->y);wprintf(L"%lc", SNAKENODE);cur = cur->next;}SetPos(cur->next->x, cur->next->y);wprintf(L"%ls", L"  ");free(cur->next);cur->next = NULL;
}//贪吃蛇的移动
void SnakeMove(pSnake ps)
{pSnakenode next = (pSnakenode)malloc(sizeof(Snakenode));if (next == NULL){perror("SnakeMove():malloc()");exit(1);}switch (ps->dir){case UP:next->x = ps->psnake->x;next->y = ps->psnake->y - 1;break;case DOWN:next->x = ps->psnake->x;next->y = ps->psnake->y + 1;break;case LEFT:next->x = ps->psnake->x - 2;next->y = ps->psnake->y;break;case RIGHT:next->x = ps->psnake->x + 2;next->y = ps->psnake->y;break;}//判断下一个位置是不是食物if (NextIsFood(next, ps)){IsFood(next, ps);}else {NoFood(next, ps);}
}//
void Printgame(pSnake ps) {SetPos(60, 15);printf("请使用↑ 、 ↓ 、 ← 、 → 来控制贪吃蛇");SetPos(60, 16);printf("按F3加速、F4减速 ");SetPos(60, 18);printf("加速可以获得更高的分数 ");SetPos(60, 20);printf("ESC:退出游戏  space:暂停 ");SetPos(60, 10);printf("当前总得分:%d", ps->all_scores);SetPos(60, 12);printf("当前每个食物得分:%d", ps->food_scores);SetPos(60, 22);printf("努力学习的小廉");
}
//判断贪吃蛇是否撞墙
void KillByWall(pSnake ps)
{if (ps->psnake->x == 0 || ps->psnake->x == 56|| ps->psnake->y == 0 || ps->psnake->y == 26)ps->state = KILL_WALL;
}
//判断贪吃蛇是否撞到自己
void KillBySelf(pSnake ps)
{pSnakenode pcur = ps->psnake->next;while (pcur){if (pcur->x == ps->psnake->x && pcur->y == ps->psnake->y){ps->state = KILL_SELF;break;}pcur = pcur->next;}
}
//游戏运行
void GameRun(pSnake ps)
{do{Printgame(ps);//判断按键是否被按过if (KEY_PRESS(VK_UP) && ps->dir != DOWN){ps->dir = UP;}else if(KEY_PRESS(VK_DOWN) && ps->dir != UP){ps->dir = DOWN;}else if (KEY_PRESS(VK_LEFT) && ps->dir != RIGHT) {ps->dir = LEFT;}else if (KEY_PRESS(VK_RIGHT) && ps->dir != LEFT) {ps->dir = RIGHT;}else if (KEY_PRESS(VK_SPACE)) //空格 -- 暂停{Pause();}else if (KEY_PRESS(VK_ESCAPE)) //游戏正常退出{ps->state = NORMAL_END;break;}else if (KEY_PRESS(VK_F3)){if (ps->sleep_time >= 100){ps->sleep_time -= 50;ps->food_scores += 5;//设定食物分数最高25}}else if (KEY_PRESS(VK_F4)){if (ps->sleep_time < 300){ps->sleep_time += 100;ps->food_scores -= 5;//⼀个⻝物分数最低是5分}}Sleep(ps->sleep_time);//贪吃蛇的移动SnakeMove(ps);//判断贪吃蛇是否撞墙KillByWall(ps);//判断贪吃蛇是否撞到自己KillBySelf(ps);} while (ps->state == OK);}
//游戏结束
void GameOver(pSnake ps)
{SetPos(8, 12);switch(ps->state){case KILL_WALL:wprintf(L"Sorry,game over because you hit the wall !\n");break;case KILL_SELF:wprintf(L"Sorry,game over because you hit youself !\n");break;case NORMAL_END:wprintf(L"Game exits normally !");break;}//释放贪吃蛇的节点内存pSnakenode pcur = ps->psnake;while (pcur){pSnakenode del = pcur;pcur = pcur->next;free(del);}ps->psnake = NULL;
}void KeyFun()
{while (_kbhit()){int key = _getch();}
}

test.c

#include"Snake.h"void test()
{Snake snake = { 0 };int ch = 0;do{ch = 0;system("cls");//游戏初始化GameStart(&snake);//游戏运行GameRun(&snake);//游戏结束GameOver(&snake);KeyFun();SetPos(30, 20);wprintf(L"再来一局吗? (Y/N)");ch = getchar();while (getchar() != '\n');} while (ch == 'Y' || ch == 'y');SetPos(0, 27);
}
int main()
{//本地化setlocale(LC_ALL, "");srand((unsigned int)time(NULL));test();//KeyFun();return 0;
}

制作不易,如果本篇内容对你有帮助,可以一键三连支持一下!!!

        如有错误的地方,也请各位大佬们指出纠正

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/bicheng/45558.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

论文阅读【时间序列】TimeMixer (ICLR2024)

【时间序列】TimeMixer (ICLR2024) 原文链接&#xff1a;TIMEMIXER: DECOMPOSABLE MULTISCALE MIXING FOR TIME SERIES FORECASTING 代码仓库&#xff1a;https://github.com/kwuking/TimeMixer 符号定义 符号含义P用于预测的历史序列长度&#xff08;seq_len&#xff09;F预测…

权力之望账号怎么注册 权力之望注册游戏账号教程

不会吧不会吧&#xff0c;这款新的MMORPG游戏&#xff0c;权力之望&#xff0c;马上就要上线啦。支援PC 及行动装置跨平台游玩的MMORPG《权力之望》以Unity 引擎研发&#xff0c;利用动态捕捉、3D 扫描技术呈现细腻的游戏画面。本作主打高自由度的武器选择成长与后续的战斗类型…

分割——半自动打标签工具ISAT详细安装及使用教程

&#x1f4aa; 专业从事且热爱图像处理&#xff0c;图像处理专栏更新如下&#x1f447;&#xff1a; &#x1f4dd;《图像去噪》 &#x1f4dd;《超分辨率重建》 &#x1f4dd;《语义分割》 &#x1f4dd;《风格迁移》 &#x1f4dd;《目标检测》 &#x1f4dd;《暗光增强》 &a…

C语言中的数组:掌握数据的有序集合【一维数组,二维数组,字符串数组,直方图打印,计算全排列,字符数组常用函数】

目录 C语言中的数组&#xff1a;掌握数据的有序集合【一维数组&#xff0c;二维数组&#xff0c;字符串数组】一维数组一维数组的创建数组的七种初始化完全初始化&#xff1a;部分初始化&#xff1a;字符数组的初始化&#xff1a;自动初始化为0&#xff1a;使用memset函数初始化…

[C++]初识C++(命名空间,命名空间使用,函数重载,缺省参数等)

&#x1f496;&#x1f496;&#x1f496;欢迎来到我的博客&#xff0c;我是anmory&#x1f496;&#x1f496;&#x1f496; 又和大家见面了 欢迎来到C探索系列 作为一个程序员你不能不掌握的知识 先来自我推荐一波 个人网站欢迎访问以及捐款 推荐阅读 如何低成本搭建个人网站…

c++之类和对象上

目录 1. 类的定义 2. 访问限定符 3. 类域 4. 实例化 5. 对象的大小 6. 为什么要内存对齐 7. this指针 1. 类的定义 • class为定义类的关键字&#xff0c;Stack为类的名字&#xff0c;{}中为类的主体&#xff0c;注意类定义结束时后⾯分号不能省 略。类体中内容称为类的…

(篇一)走进FreeRtos—认识FreeRtos

【温故知新篇】 【在现在的工作中一直在使用FreeRtos&#xff0c;但是对它的了解还是不够深入&#xff0c;现在由浅入深&#xff0c;分模块学习和记录&#xff0c;最后将自己基于FreeRtos开发项目实践】 首先我们将这个系统当作一座庄园&#xff0c;开始第一步走进庄园&#…

【备战秋招】——算法题目训练和总结day4

【备战秋招】——算法题目训练和总结day4&#x1f60e; 前言&#x1f64c;Fibonacci数列我的题解思路分享代码分享 单词搜索我的题解思路分享代码分享 杨辉三角我的题解思路分享代码分享 总结撒花&#x1f49e; &#x1f60e;博客昵称&#xff1a;博客小梦 &#x1f60a;最喜欢…

ubuntu重装系统后,安装cuda,cudnn

一、 先安装驱动&#xff0c;如果驱动安装不成功&#xff0c;会影响桌面&#xff0c;再重装系统还来得及&#xff0c;尝试了很多方法&#xff0c;还是用系统安装最靠谱&#xff1a; 首先进入Ubuntu搜索栏目&#xff0c;找到软件更新器的一个图标&#xff0c;点开之后是这样的。…

开发个人Ollama-Chat--5 模型管理 (二)

开发个人Ollama-Chat–5 模型管理 (二) ChatGPT 这是该项目的最终效果&#xff0c;使用ollama的open-webui进行人与机器的对话功能&#xff0c;对话的后端服务则完全对接自己开发的Go项目。 如何实现呢&#xff1f;则通过这篇文章&#xff0c;一一给大家剖析后端的原理及功能…

mmaction2的GPU环境配置记录RTX3090,cuda12.2,ubuntu22.04版本

1、配置镜像源 最重要的一个步骤,先看下镜像源地址,如果镜像源有问题,所有的包安装都会有问题 镜像源地址获取地址:选择对应的ubuntu版本号,将里面的镜像源地址复制出来,更新到服务器 ubuntu | 镜像站使用帮助 | 清华大学开源软件镜像站 | Tsinghua Open Source Mirro…

牛客小白月赛98

骰子魔术 jackle 会拿出一枚骰子&#xff0c;骰子的表面分别写上了从 1∽5001\backsim 5001∽500 的数字&#xff0c;朋友会随便说一个 1∽5001\backsim 5001∽500 之间的点数&#xff0c;jackle 都能保证百分之百的掷出这个点数。 当然 jackle 有备而来&#xff0c;他准备了 …

LeetCode分发糖果(贪心思路分析)

题目描述 贪心思路 思路及解法 我们可以将「相邻的孩子中&#xff0c;评分高的孩子必须获得更多的糖果」这句话拆分为两个规则&#xff0c;分别处理。 左规则&#xff1a;当 ratings[i−1]<ratings[i] 时&#xff0c;i 号学生的糖果数量将比 i−1 号孩子的糖果数量多。 …

实现win10多用户同时远程连接登陆(详细实验步骤版)

目录 目的环境实验步骤&#xff08;在需要被远程的win10上&#xff09;1. 开启远程桌面服务2. 编辑组策略实现多用户登录3. 安装RDPWrap实现多用户同时登录 测试 目的 默认情况下&#xff0c;win10 专业版只支持本地或远程只能同时存在一个连接。windows server支持多连接。 比…

【unity笔记】十、Obi绳索插件使用

一. 创建绳索 1.1 新建蓝图 在Assets中右键选择创建->Obi->Rope Blueprint&#xff0c;其属性如图所示 1.2 Obi solver 在场景下创建一个obi solver对象&#xff0c;在该对象下再创建Obi Rope对象。 随后将蓝图拖到Obi Rope对象下的Obi Rope组件&#xff0c;即可看到…

MES系统与其他系统的集成:提升制造业智能化的关键路径

在当今高度竞争的制造业环境中&#xff0c;企业对于生产效率、质量控制、资源优化以及快速响应市场变化的需求日益迫切。制造执行系统&#xff08;MES&#xff09;作为连接企业计划层与车间控制层的重要桥梁&#xff0c;其在提升生产透明度和优化生产流程方面发挥着不可替代的作…

【SQL】如何用SQL写透视表

【背景】 报表中有一大需求是透视表,目前有很多分析类应用也搭载了此类功能,那么我们能不能直接用SQL做透视表呢? 【分析】 BI类软件将透视表功能做在了前端,但是数据本身还是存储在数据库中,所以必然有方法可以用SQL直接实现透视表。 【心法】 透视表是任意选取一个…

2024最新6月泛二级域名秒收泛目录(二级域名泛站群)

5月免费版本无后台 无更新功能不自动引蜘蛛 2024年5月最新泛程序&#xff0c;秒收秒排&#xff01;&#xff08;泛型程序&#xff09; - 虚良SEO博客 新曾功能&#xff1a; 后台管理 蜘蛛统计 域名添加 一键强引蜘蛛 蜘蛛统计 识别真假蜘蛛 全自动引蜘蛛 域名要求 …

Vue3-15 表单的绑定

在Vue 3中&#xff0c;表单输入绑定是实现动态数据双向绑定的重要部分&#xff0c;它让开发者可以轻松地管理和响应用户输入。本文将详细介绍如何在Vue 3中利用v-model指令以及一些特定修饰符来处理不同类型的表单输入。 响应式表单数据 首先&#xff0c;我们定义了一个响应式…

Linux /etc/profile 详解

概述 Linux是一个多用户的操作系统。每个用户登录系统后&#xff0c;都会有一个专用的运行环境。通常每个用户默认的环境都是相同的&#xff0c;这个默认环境实际上就是一组环境变量的定义。用户可以对自己的运行环境进行定制&#xff0c;其方法就是修改相应的系统环境变量&…