C语言实战项目<贪吃蛇>

我们这篇会使用C语言在Windows环境的控制台中模拟实现经典小游戏贪吃蛇 实现基本的功能:

结果如下:

1.一些Win32 API知识

本次实现呢我们会用到一些Win32 API的知识(WIN32 API也就是Microsoft Windows 32位平台的应用程序编程接口):

1)控制窗口大小

我们可以使用cmd命令来设置控制台窗口的长宽:设置控制台窗口的大小,设置控制台窗口的名字:
mode con cols=100 lines=30//设置窗口大小为100列,30行
title 贪吃蛇//设置窗口名称为贪吃蛇

我们在C语言中可以用函数system来执行,入上面这两个,我们可以这么写:

system("mode con cols=100 lines=30");
system("title 贪吃蛇");

2)控制台屏幕上的坐标

Windows API中存在这么一个结构体,叫做COORD,表示⼀个字符在控制台屏幕上的坐标
typedef struct _COORD {SHORT X;SHORT Y;
} COORD, *PCOORD;
给坐标赋值:
COORD pos = { 10, 15 };

3)GetStdHandle

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

4)GetConsoleCursorInfo

检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
BOOL WINAPI GetConsoleCursorInfo(HANDLE hConsoleOutput,PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
实例:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息

5)CONSOLE_CURSOR_INFO

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

typedef struct _CONSOLE_CURSOR_INFO {DWORD dwSize;BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
dwSize,由光标填充的字符单元格的百分比。 此值介于1到100之间。 光标外观会变化,范围从完全填充单元格到单元底部的水平线条。
bVisible,游标的可见性。 如果光标可见,则此成员为 TRUE。
CursorInfo.bVisible = false; //隐藏控制台光标

 

6)SetConsoleCursorInfo

设置指定控制台屏幕缓冲区的光标的大小和可见性。
BOOL WINAPI SetConsoleCursorInfo(HANDLE hConsoleOutput,const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
实例: 
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//影藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态

7)SetConsoleCursorPosition

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

示例:

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

用上面的这些函数我们就可以封装⼀个设置光标位置的函数

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

8)GetAsyncKeyState

GetAsyncKeyState用来获取按键情况

SHORT GetAsyncKeyState(int vKey
);
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
GetAsyncKeyState 的返回值是short类型,在上⼀次调用  GetAsyncKeyState 函数后,如果
返回的16位的short数据中,最高位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬起;如果最低位被置为1则说明,该按键被按过,否则为0
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1 :
示例:
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )

2.游戏的逻辑

在做这个游戏前,我们应该先想好怎么做,游戏每个阶段该做什么.

这里我做了一个图:

3. 贪吃蛇的整体维护和地图绘制

整体维护,我们创建一个结构体来维护整体运行:

typedef struct Snake
{pSnakeNode pSnake;//维护整条蛇的指针,是指向蛇头pSnakeNode pFood;//指向食物的指针int Score;//当前累积的分数int FoodWeight;//一个食物的分数int SleepTime;//蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间越长,蛇的速度越慢enum GAME_STATUS status;//游戏当前的状态enum DIRECTION dir;//蛇当前走的方向
}Snake, * pSnake;

我们顺便将蛇,目标,墙用宽字符定义了

#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'

窗口大小,名字,以及隐藏光标,我们都解决了,现在我们来解决打印贪吃蛇地图的问题

这里不得不讲⼀下控制台窗口的⼀些知识,如果想在控制台的窗口中指定位置输出信息,我们得知道该位置的坐标,所以首先介绍⼀下控制台窗口的坐标知识.控制台窗口的坐标如下所示,横向的是X轴,从左向右依次增长,纵向是Y轴,从上到下依次增长。
在游戏地图上,我们打印墙使用宽字符:□,打印蛇使用宽字符●,打印食物使用宽字符★
普通的字符是占一个字节的,这类宽字符是占用2个字节。
过去C语言并不适合非英语国家(地区)使用。
C语言最初假定字符都是但自己的。但是这些假定并不是在世界的任何地方都适用。

C语言字符默认是采用ASCII编码的,ASCII字符集采用的是单字节编码,且只使用了单字节中的低7 位,最高位是没有使用的,可表示为0xxxxxxxx;可以看到,ASCII字符集共包含128个字符,在英语国家中,128个字符是基本够用的,但是,在其他国家语言中,比如,在法语中,字母上方有注音符号,它就无法用ASCII码表示。于是,⼀些欧洲国家就决定,利用字节中闲置的最高位编入新的符号。比如,法语中的é的编码为130(⼆进制10000010)。这样⼀来,这些欧洲国家使用的编码体系,可以表示最多256个符号。但是,这里又出现了新的问题。不同的国家有不同的字母,因此,哪怕它们都使用256个符号的编码方式,代表的字母却不⼀样。比如,130在法语编码中代表了é,在希伯来语编码中却代表了字⺟Gimel在俄语编码中又会代表另⼀个符号。但是不管怎样,所有这些编码方式中,0--127表⽰的符号是⼀样的,不⼀样的只是128--255的这⼀段。至于亚洲国家的文字,使用的符号就更多了,汉字就多达10万左右。⼀个字节只能表示256种符号,肯定是不够的,就必须使用多个字节表达⼀个符号。比如,简体中文常见的编码方式是 GB2312,使用两个字节表示⼀个汉字,所以理论上最多可以表示 256 x 256 = 65536 个符号。

后来为了使C语言适应国际化,C语⾔的标准中不断加入了国际化的支持。比如:加入和宽字符的类型wchar_t 和宽字符的输入和输出函数,加入<locale.h>头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。

1)<locale.h>头文件

选自<clocale> (locale.h) - C++参考 (cplusplus.com)

2)setlocale 函数

setlocale 函数用于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。
setlocale 的第一个参数可以是前⾯说明的类项中的⼀个,那么每次只会影响⼀个类项,如果第⼀个参数是LC_ALL,就会影响所有的类项。
C标准给第二个参数仅定义了2种可能取值:"C"和" "。
在任意程序执行开始,都会隐藏式执行调用:
setlocale(LC_ALL, "C");
当地区设置为"C"时,库函数按正常方式执行,小数点是⼀个点。
当程序运行起来后想改变地区,就只能显示调用setlocale函数。用" "作为第2个参数,调用setlocale 函数就可以切换到本地模式,这种模式下程序会适应本地环境。比如:切换到我们的本地模式后就支持宽字符(汉字)的输出等。
 setlocale(LC_ALL, " ");//切换到本地环境

在切换到我们的本地环境后,就可以打印宽字符了,

示例:

#include <stdio.h>
#include<locale.h>
int main()
{setlocale(LC_ALL, "");wchar_t ch1 = L'●';char ch2 = '&';wprintf(L"%c", ch1);printf("%c\n", ch2);printf("%c", ch2);return 0;
}

运行结果:

经过对比我们发现⼀个普通字符占⼀个字符的位置 但是打印⼀个宽字符,占用2个字符的位置,那么我们如果要在贪吃蛇中使用宽字符,就得处理好地图上坐标的计算。

3)地图坐标

我们假设实现⼀个棋盘27行,58列的棋盘(行和列可以根据自己的情况修改),再围绕地图画出墙, (图在上面)代码如下:
#define WALL L'□'
void createmap()
{//上SetPos(0, 0);for (int i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//下SetPos(0, 26);for (int i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//左for (int i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右for (int i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);}
}

4.有关蛇的定义

我们创建一个结构体来维护蛇身的节点:

typedef struct SnakeNode
{int x;int y;struct SnakeNode* next;
}SnakeNode, * pSnakeNode;

顺带我们枚举蛇的运动方向

enum DIRECTION
{UP,DOWN,LEFT,RIGHT
};

5.GameStart()

接下来我们开始按照流程来,现在是游戏前的准备:

1)设置窗口大小、设置窗口名字、隐藏光标

void setwindows()
{system("title 贪吃蛇");system("mode con cols=100 lines=30");
}
void hidcursor()
{//获得设备句柄HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//定义控制台相关光标的结构体CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(handle, &CursorInfo);//获取控制台光标信息CursorInfo.bVisible = false;//光标可见性关闭SetConsoleCursorInfo(handle, &CursorInfo);//设置控制台光标状态
}void SetPos(int x, int y)
{//获得设备句柄HANDLE hanlde = GetStdHandle(STD_OUTPUT_HANDLE);//根据句柄设置光标的位置COORD pos = { x, y };//设置控制台光标状态SetConsoleCursorPosition(hanlde, pos);
}

2)欢迎词

前面我们运行的时候都会有欢迎来到XXX,所以我们也做一个

void welcome()
{//欢迎信息SetPos(35, 10);printf("欢迎来到贪吃蛇小游戏\n");SetPos(38, 20);system("pause");system("cls");//功能介绍信息SetPos(15, 10);printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");SetPos(15, 11);printf("加速能得到更高的分数");SetPos(38, 20);system("pause");system("cls");
}

(35,10)是我设计的打印时的光标位置,你也可以自行修改。cls是清屏

3)地图的创建与贪吃蛇的初始化

地图创建前面我讲过了这里就展示一下代码

void createmap()
{//上SetPos(0, 0);for (int i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//下SetPos(0, 26);for (int i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//左for (int i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右for (int i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);}
}

贪吃蛇的初始化,包括蛇的出实话,以及各种信息的初始化。

我们使用单链表,将蛇的身体一节一节连起来,最后用头插把蛇头接上蛇身

代码示例:

	pSnakeNode cur = NULL;int i = 0;for (i = 0; i < 3; i++){cur = (pSnakeNode)malloc(sizeof(SnakeNode));if (cur == NULL){perror("InitSnake():malloc()");return;}cur->x = POS_X + 2 * i;cur->y = POS_Y;cur->next = NULL;//头插法if (ps->pSnake == NULL){ps->pSnake = cur;}else{cur->next = ps->pSnake;ps->pSnake = cur;} }

蛇写好了,我们要打印出来,所以我们可以遍历链表然后打印出整条蛇:

	//打印蛇身cur = ps->pSnake;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}

这样我们的蛇就初始化完了,接下来我们把其他信息也初始化:

//贪吃蛇的其他信息初始化ps->dir = RIGHT;ps->FoodWeight = 10;ps->pFood = NULL;ps->Score = 0;ps->SleepTime = 200;ps->status = OK;

最后把这三段拼在一起就是贪吃蛇的初始化了:

//初始化贪吃蛇
void InitSnake(pSnake ps)
{pSnakeNode cur = NULL;int i = 0;for (i = 0; i < 3; i++){cur = (pSnakeNode)malloc(sizeof(SnakeNode));if (cur == NULL){perror("InitSnake():malloc()");return;}cur->x = POS_X + 2 * i;cur->y = POS_Y;cur->next = NULL;//头插法if (ps->pSnake == NULL){ps->pSnake = cur;}else{cur->next = ps->pSnake;ps->pSnake = cur;} }//打印蛇身cur = ps->pSnake;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//贪吃蛇的其他信息初始化ps->dir = RIGHT;ps->FoodWeight = 10;ps->pFood = NULL;ps->Score = 0;ps->SleepTime = 200;ps->status = OK;
}

4)食物的创建

既然我们完成了蛇的初始化,在游戏没开始的时候食物不也是创建好了吗?,所以我们接下来写事物的创建,但是我们食物的创建存在一定的条件,对吧。

存在以下两个条件:

1.食物不能与蛇重叠

2.食物不能生成在墙上

代码示例:

//创建食物
void Createfood(pSnake ps)
{int x = 0;int y = 0;again:do{x = rand() % 53 + 2;y = rand() % 24 + 1;} while (x % 2 != 0);//坐标和蛇的身体的每个节点的做坐标比较pSnakeNode cur = ps->pSnake;while (cur){if (x == cur->x && y == cur->y){goto again;}cur = cur->next;}//创建食物pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));if (pFood == NULL){perror("CreateFood()::malloc()");return;}pFood->x = x;pFood->y = y;ps->pFood = pFood;SetPos(x, y);wprintf(L"%lc", FOOD);
}

注意考虑空间开辟失败的情况。

4)打印帮助信息

我们在运行游戏时右边有一个帮助信息,这个也是提前打好的

代码示例:

//打印帮助信息
void PrintHelpInfo()
{SetPos(59, 15);printf("1.不要撞到墙");SetPos(59, 16);printf("2.不要撞到自己");SetPos(59, 17);printf("3.用 ↑ . ↓ . ← . → 来控制蛇的移动");SetPos(59, 18);printf("4.F3加速,F4减速,加速能得到更高的分数");SetPos(59, 19);printf("5.ESC退出,space暂停");SetPos(59, 20);printf("Dream_Snowar所作");
}

5)整合

最后我们将代码整合起来就是我们的要的函数

//游戏初始化
void GameStart(pSnake ps)
{setwindows();welcome();hidcursor();createmap();InitSnake(ps);Createfood(ps);PrintHelpInfo();
}

6.GameRun()

这里我们需要完成,蛇的移动,蛇吃食物,各个按键的使用

首先我们写一下函数的结构,以及判断各个按键

代码示例:

void GameRun(pSnake ps)
{		do{//当前的分数SetPos(59, 10);printf("总分:%d\n", ps->Score);SetPos(59, 11);printf("分数倍率:%02d\n", ps->FoodWeight);//检测按键,↑ . ↓ . ← . → . ESC . F3 . F4 . spaceif (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_ESCAPE)){ps->status = ESC;break;}else if (KEY_PRESS(VK_SPACE)){//游戏暂停pause();}else if (KEY_PRESS(VK_F3)){if (ps->SleepTime >= 80){ps->SleepTime -= 30;ps->FoodWeight += 2;}}else if (KEY_PRESS(VK_F4)){if (ps->FoodWeight > 2){ps->SleepTime += 30;ps->FoodWeight -= 2;}}//休眠Sleep(ps->SleepTime);//走一步Snakemove(ps);KillBySelf(ps);KillByWall(ps);} while (ps->status == OK);}

按键检测我们前面已经定义过了

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

1)暂停pause()

void pause()
{while (1){Sleep(100);if (KEY_PRESS(VK_SPACE)){break;}}
}

2)判断下一个坐标是否是食物

int Nextblock(pSnake ps, pSnakeNode pNext)
{if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y){return 1;}else{return 0;}
}

1)是食物

void IsFood(pSnake ps, pSnakeNode pNext)
{pNext->next = ps->pSnake;ps->pSnake = pNext;pSnakeNode cur = ps->pSnake;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}ps->Score += ps->FoodWeight;free(ps->pFood);Createfood(ps);
}

2)不是食物

void NotFood(pSnake ps, pSnakeNode pNext)
{pNext->next = ps->pSnake;ps->pSnake = pNext;pSnakeNode cur = ps->pSnake;while (cur->next->next){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}SetPos(cur->next->x, cur->next->y);printf("  ");free(cur->next);cur->next = NULL;
}

3)是否撞到墙

void KillByWall(pSnake ps)
{if (ps->pSnake->x == 0 ||ps->pSnake->x == 56 ||ps->pSnake->y == 0 ||ps->pSnake->y == 26){ps->status = KILL_BY_WALL;}
}

4)是否撞到自己

//是否撞自己
void KillBySelf(pSnake ps)
{pSnakeNode cur = ps->pSnake->next;//从第二个节点开始while (cur){if (cur->x == ps->pSnake->x && cur->y == ps->pSnake->y){ps->status = KILL_BY_SELF;return;}cur = cur->next;}
}

5)蛇移动

void Snakemove(pSnake ps)
{pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));if (pNext == NULL){perror("CreateFood()::malloc()");return;}pNext->next = NULL;switch (ps->dir){case UP:pNext->x = ps->pSnake->x;pNext->y = ps->pSnake->y - 1;break;case DOWN:pNext->x = ps->pSnake->x;pNext->y = ps->pSnake->y + 1;break;case LEFT:pNext->x = ps->pSnake->x - 2;pNext->y = ps->pSnake->y;break;case RIGHT:pNext->x = ps->pSnake->x + 2;pNext->y = ps->pSnake->y;break;}//判断下一个坐标是都是食物if (Nextblock(ps, pNext)){IsFood(ps, pNext);}else{NotFood(ps, pNext);}
}

7.GameEnd()

完成上面的内容游戏已经可以游玩了,只是缺少了一些结束的提示,但是我们要记得最后将开辟的空间释放。

代码示例:

void GameEnd(pSnake ps)
{SetPos(15, 12);switch (ps->status){case ESC:printf("主动退出游戏,正常退出\n");break;case KILL_BY_WALL:printf("撞墙了,游戏结束\n");break;case KILL_BY_SELF:printf("咬到自己了,游戏结束\n");break;}//释放贪吃蛇的链表资源pSnakeNode cur = ps->pSnake;pSnakeNode del = NULL;while (cur){del = cur;cur = cur->next;free(del);}free(ps->pFood);ps = NULL;
}

8.整合程序

void test()
{//创建贪吃蛇Snake snake = { 0 };GameStart(&snake);//游戏前的初始化GameRun(&snake);GameEnd(&snake);
}
int main()
{setlocale(LC_ALL, "");test();//贪吃蛇游戏的测试SetPos(0, 27);return 0;
}

这样贪吃蛇就完成了

9.代码展示:

//snake.h
#pragma once
#define _CRT_SECURE_NO_WARNINGS
#include<stdio.h>
#include <locale.h>
#include<stdlib.h>
#include<stdbool.h>
#include<Windows.h>
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
//蛇默认的起始坐标
#define POS_X 24
#define POS_Y 5
//检测按键是否按了
#define KEY_PRESS(VK)  ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )//设置控制台窗口
void setwindows();
//隐藏光标
void hidcursor();
//欢迎内容
void welcome();
//创建地图
void createmap();
//光标位置
void SetPos(int x, int y);//游戏的状态
enum GAME_STATUS
{OK = 1,//正常运行ESC, //按了ESC键退出,正常退出KILL_BY_WALL,//撞墙KILL_BY_SELF //撞到自身
};//蛇行走的方向
enum DIRECTION
{UP,DOWN,LEFT,RIGHT
};//蛇身结点的定义
typedef struct SnakeNode
{int x;int y;struct SnakeNode* next;
}SnakeNode, * pSnakeNode;// 贪吃蛇
typedef struct Snake
{pSnakeNode pSnake;//维护整条蛇的指针,是指向蛇头pSnakeNode pFood;//指向食物的指针int Score;//当前累积的分数int FoodWeight;//一个食物的分数int SleepTime;//蛇休眠的时间,休眠的时间越短,蛇的速度越快,休眠的时间越长,蛇的速度越慢enum GAME_STATUS status;//游戏当前的状态enum DIRECTION dir;//蛇当前走的方向
}Snake, * pSnake;//初始化贪吃蛇
void InitSnake(pSnake ps);
//创建食物  
void Createfood(pSnake ps);
//打印帮助信息
void PrintHelpInfo();
//游戏初始化
void GameStart(pSnake ps);
//蛇移动
void Snakemove(pSnake ps);
//打印蛇
void PrintShake(pSnake ps);
//判断下一个坐标是否是食物
int Nextblock(pSnake ps, pSnakeNode pNext);
//是食物
void IsFood(pSnake ps,pSnakeNode pNext);
// 不是食物
void NotFood(pSnake ps, pSnakeNode pNext);
//是否撞自己
void KillBySelf(pSnake ps);
//是否撞墙
void KillByWall(pSnake ps);
//游戏进程
void GameRun(pSnake ps);
//游戏结束的资源释放
void GameEnd(pSnake ps);
//snake.c
#include"shake.h"
void setwindows()
{system("title 贪吃蛇");system("mode con cols=100 lines=30");
}
void hidcursor()
{//获得设备句柄HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//定义控制台相关光标的结构体CONSOLE_CURSOR_INFO CursorInfo;GetConsoleCursorInfo(handle, &CursorInfo);//获取控制台光标信息CursorInfo.bVisible = false;//光标可见性关闭SetConsoleCursorInfo(handle, &CursorInfo);//设置控制台光标状态
}void SetPos(int x, int y)
{//获得设备句柄HANDLE hanlde = GetStdHandle(STD_OUTPUT_HANDLE);//根据句柄设置光标的位置COORD pos = { x, y };//设置控制台光标状态SetConsoleCursorPosition(hanlde, pos);
}void welcome()
{//欢迎信息SetPos(35, 10);printf("欢迎来到贪吃蛇小游戏\n");SetPos(38, 20);system("pause");system("cls");//功能介绍信息SetPos(15, 10);printf("用 ↑ . ↓ . ← . → 来控制蛇的移动,F3是加速,F4是减速");SetPos(15, 11);printf("加速能得到更高的分数");SetPos(38, 20);system("pause");system("cls");
}
//创建地图
void createmap()
{//上SetPos(0, 0);for (int i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//下SetPos(0, 26);for (int i = 0; i <= 56; i += 2){wprintf(L"%lc", WALL);}//左for (int i = 1; i <= 25; i++){SetPos(0, i);wprintf(L"%lc", WALL);}//右for (int i = 1; i <= 25; i++){SetPos(56, i);wprintf(L"%lc", WALL);}
}//初始化贪吃蛇
void InitSnake(pSnake ps)
{pSnakeNode cur = NULL;int i = 0;for (i = 0; i < 3; i++){cur = (pSnakeNode)malloc(sizeof(SnakeNode));if (cur == NULL){perror("InitSnake():malloc()");return;}cur->x = POS_X + 2 * i;cur->y = POS_Y;cur->next = NULL;//头插法if (ps->pSnake == NULL){ps->pSnake = cur;}else{cur->next = ps->pSnake;ps->pSnake = cur;} }//打印蛇身cur = ps->pSnake;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//贪吃蛇的其他信息初始化ps->dir = RIGHT;ps->FoodWeight = 10;ps->pFood = NULL;ps->Score = 0;ps->SleepTime = 200;ps->status = OK;
}
//创建食物
void Createfood(pSnake ps)
{int x = 0;int y = 0;again:do{x = rand() % 53 + 2;y = rand() % 24 + 1;} while (x % 2 != 0);//坐标和蛇的身体的每个节点的做坐标比较pSnakeNode cur = ps->pSnake;while (cur){if (x == cur->x && y == cur->y){goto again;}cur = cur->next;}//创建食物pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode));if (pFood == NULL){perror("CreateFood()::malloc()");return;}pFood->x = x;pFood->y = y;ps->pFood = pFood;SetPos(x, y);wprintf(L"%lc", FOOD);
}//打印帮助信息
void PrintHelpInfo()
{SetPos(59, 15);printf("1.不要撞到墙");SetPos(59, 16);printf("2.不要撞到自己");SetPos(59, 17);printf("3.用 ↑ . ↓ . ← . → 来控制蛇的移动");SetPos(59, 18);printf("4.F3加速,F4减速,加速能得到更高的分数");SetPos(59, 19);printf("5.ESC退出,space暂停");SetPos(59, 20);printf("Dream_Snowar所作");
}//游戏初始化
void GameStart(pSnake ps)
{setwindows();welcome();hidcursor();createmap();InitSnake(ps);Createfood(ps);PrintHelpInfo();
}//判断下一个坐标是否是食物
int Nextblock(pSnake ps, pSnakeNode pNext)
{if (ps->pFood->x == pNext->x && ps->pFood->y == pNext->y){return 1;}else{return 0;}
}//space暂停
void pause()
{while (1){Sleep(100);if (KEY_PRESS(VK_SPACE)){break;}}
}
//打印蛇
void PrintShake(pSnake ps)
{pSnakeNode cur = ps->pSnake;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}
}//是食物
void IsFood(pSnake ps, pSnakeNode pNext)
{pNext->next = ps->pSnake;ps->pSnake = pNext;PrintShake(ps);ps->Score += ps->FoodWeight;free(ps->pFood);Createfood(ps);
}
//不是食物
void NotFood(pSnake ps, pSnakeNode pNext)
{pNext->next = ps->pSnake;ps->pSnake = pNext;pSnakeNode cur = ps->pSnake;while (cur->next->next){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}SetPos(cur->next->x, cur->next->y);printf("  ");free(cur->next);cur->next = NULL;
}
//是否撞自己
void KillBySelf(pSnake ps)
{pSnakeNode cur = ps->pSnake->next;//从第二个节点开始while (cur){if (cur->x == ps->pSnake->x && cur->y == ps->pSnake->y){ps->status = KILL_BY_SELF;return;}cur = cur->next;}
}
//是否撞墙
void KillByWall(pSnake ps)
{if (ps->pSnake->x == 0 ||ps->pSnake->x == 56 ||ps->pSnake->y == 0 ||ps->pSnake->y == 26){ps->status = KILL_BY_WALL;}
}void Snakemove(pSnake ps)
{pSnakeNode pNext = (pSnakeNode)malloc(sizeof(SnakeNode));if (pNext == NULL){perror("CreateFood()::malloc()");return;}pNext->next = NULL;switch (ps->dir){case UP:pNext->x = ps->pSnake->x;pNext->y = ps->pSnake->y - 1;break;case DOWN:pNext->x = ps->pSnake->x;pNext->y = ps->pSnake->y + 1;break;case LEFT:pNext->x = ps->pSnake->x - 2;pNext->y = ps->pSnake->y;break;case RIGHT:pNext->x = ps->pSnake->x + 2;pNext->y = ps->pSnake->y;break;}//判断下一个坐标是都是食物if (Nextblock(ps, pNext)){IsFood(ps, pNext);}else{NotFood(ps, pNext);}
}
//游戏运行
void GameRun(pSnake ps)
{		do{//当前的分数SetPos(59, 10);printf("总分:%d\n", ps->Score);SetPos(59, 11);printf("分数倍率:%02d\n", ps->FoodWeight);//检测按键,↑ . ↓ . ← . → . ESC . F3 . F4 . spaceif (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_ESCAPE)){ps->status = ESC;break;}else if (KEY_PRESS(VK_SPACE)){//游戏暂停pause();}else if (KEY_PRESS(VK_F3)){if (ps->SleepTime >= 80){ps->SleepTime -= 30;ps->FoodWeight += 2;}}else if (KEY_PRESS(VK_F4)){if (ps->FoodWeight > 2){ps->SleepTime += 30;ps->FoodWeight -= 2;}}//休眠Sleep(ps->SleepTime);//走一步Snakemove(ps);KillBySelf(ps);KillByWall(ps);} while (ps->status == OK);}
void GameEnd(pSnake ps)
{SetPos(15, 12);switch (ps->status){case ESC:printf("主动退出游戏,正常退出\n");break;case KILL_BY_WALL:printf("撞墙了,游戏结束\n");break;case KILL_BY_SELF:printf("咬到自己了,游戏结束\n");break;}//释放贪吃蛇的链表资源pSnakeNode cur = ps->pSnake;pSnakeNode del = NULL;while (cur){del = cur;cur = cur->next;free(del);}free(ps->pFood);ps = NULL;
}
//test.c
#include"shake.h"
void test()
{//创建贪吃蛇Snake snake = { 0 };GameStart(&snake);//游戏前的初始化GameRun(&snake);GameEnd(&snake);
}
int main()
{setlocale(LC_ALL, "");test();//贪吃蛇游戏的测试SetPos(0, 27);return 0;
}

C语言的学习结束了,把贪吃蛇作为最后的收官之作是我目前的想法,接下来我重点完成,C语言实践和数据结构的博客的撰写,希望大家多多支持.

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

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

相关文章

【Qt学习笔记】(二)信号和槽

信号和槽 1 信号和槽概述2 信号和槽的使用3 可视化生成槽函数4 自定义信号和槽5 带参数的信号和槽6 信号与槽的连接方式7 信号与槽的断开8 使用 Lambda 表达式来定义槽函数 1 信号和槽概述 在Qt中&#xff0c;用户和控件的每次交互过程称为一个事件。比如"用户点击按钮&q…

实时时钟芯片DS1307单片机C语言驱动程序

实时时钟RTC相关索引 1.单片机RTC及时钟芯片的时间到底从哪一年起始&#xff1f; 2.STM32F103单片机内部RTC实时时钟驱动程序 3.实时时钟芯片DS1302单片机C语言驱动程序 4.实时时钟芯片DS1307单片机C语言驱动程序 一、DS1307简介 DS1307是一款非易失性实时时钟&#xff08;R…

【Java程序设计】【C00209】基于SSM个人求职管理系统(论文+PPT)

基于SSM个人求职管理系统&#xff08;论文PPT&#xff09; 项目简介项目获取开发环境项目技术运行截图 项目简介 这个一个基于SSM的个人求职管理系统&#xff0c;本系统共分为三种权限&#xff1a;管理员、普通管理员、用户 管理员&#xff1a;首页、个人中心、用户管理、管理…

go并发编程-介绍与Goroutine使用

1. 并发介绍 进程和线程 A. 进程是程序在操作系统中的一次执行过程&#xff0c;系统进行资源分配和调度的一个独立单位。B. 线程是进程的一个执行实体,是CPU调度和分派的基本单位,它是比进程更小的能独立运行的基本单位。C.一个进程可以创建和撤销多个线程;同一个进程中的多个…

Go语言的100个错误使用场景(11-20)|项目组织和数据类型

前言 大家好&#xff0c;这里是白泽。 《Go语言的100个错误以及如何避免》 是最近朋友推荐我阅读的书籍&#xff0c;我初步浏览之后&#xff0c;大为惊喜。就像这书中第一章的标题说到的&#xff1a;“Go: Simple to learn but hard to master”&#xff0c;整本书通过分析100…

DevSecOps 参考模型介绍

目录 一、参考模型概述 1.1 概述 二、参考模型分类 2.1 DevOps 组织型模型 2.1.1 DevOps 关键特性 2.1.1.1 模型特性图 2.1.1.2 特性讲解 2.1.1.2.1 自动化 2.1.1.2.2 多边协作 2.1.1.2.3 持续集成 2.1.1.2.4 配置管理 2.1.2 DevOps 生命周期 2.1.2.1 研发过程划分…

leetcode刷题(剑指offer)54.螺旋矩阵

54.螺旋矩阵 给你一个 m 行 n 列的矩阵 matrix &#xff0c;请按照 顺时针螺旋顺序 &#xff0c;返回矩阵中的所有元素。 示例 1&#xff1a; 输入&#xff1a;matrix [[1,2,3],[4,5,6],[7,8,9]] 输出&#xff1a;[1,2,3,6,9,8,7,4,5]示例 2&#xff1a; 输入&#xff1a;ma…

Java基础-集合框架

集合框架&#xff1a; 内存层面可考虑的数据存储容器&#xff1a;数组&#xff0c;集合 数组的特点&#xff1a;长度&#xff0c;存储元素类型确定&#xff0c;既可以放基本数据类型&#xff0c;也可以放引用数据类型 缺点&#xff1a;长度不可变&#xff0c;存储元素特点单…

从零开始 Linux(一):基础介绍与常用指令总结

从零开始 Linux 01. 概念理解 1.1 什么是 Linux&#xff1f; Linux 是一个开源免费的 操作系统&#xff0c;具有很好的稳定性、安全性&#xff0c;且有很强的处理高并发的能力 Linux 的应用场景&#xff1a; 可以在 Linux 下开发项目&#xff0c;比如 JavaEE、大数据、Python…

3D词云图

工具库 tagcanvas.min.js vue3&#xff08;框架其实无所谓&#xff0c;都可以&#xff09; 实现 <script setup> import { onMounted, ref } from vue; import ./tagcanvas.min.js;const updateFlag ref(false);// 词云图初始化 const initWordCloud () > {let …

RabbitMQ快速实战

目录 什么是消息队列&#xff1f; 消息队列的优势 应用解耦 异步提速 削峰填谷 总结 主流MQ产品特点比较 Rabbitmq快速上手 创建用户admin Exchange和Queue Connection和Channel RabbitMQ中的核心概念总结 什么是消息队列&#xff1f; MQ全称Message Queue&#xf…

Spring5深入浅出篇:Spring中ioc(控制反转)与DI(依赖注入)

Spring5深入浅出篇:Spring中ioc(控制反转)与DI(依赖注入) 反转(转移)控制(IOC Inverse of Control) 控制&#xff1a;对于成员变量赋值的控制权 反转控制&#xff1a;把对于成员变量赋值的控制权&#xff0c;从代码中反转(转移)到Spring⼯⼚和配置⽂件中完成好处&#xff1a;…

七、并发工具(上)

一、自定义线程池 1&#xff09;背景&#xff1a; 在 QPS 量比较高的情况下&#xff0c;我们不可能说所有的访问都创建一个线程执行&#xff0c;这会导致内存占用过高&#xff0c;甚至有可能出现 out of memory另外也要考虑 cpu 核数&#xff0c;如果请求超过了cpu核数&#…

【bitonicSort学习】

bitonicSort学习 什么是Bitonic Sort核心 什么是Bitonic Sort https://zhuanlan.zhihu.com/p/53963918 这个是用来并行排序的一个操作 之前学过一些CPU排序&#xff0c;快排 冒泡 归并啥的&#xff0c;有一些能转成并行&#xff0c;有一些不适合 像快排这种二分策略就可以考虑…

Vue3的自定义指令怎么迁移到nuxt3

一、找到Vue3中指令的源码 const DISTANCE 100; // 距离 const ANIMATIONTIME 500; // 500毫秒 let distance: number | null null,animationtime: number | null null; const map new WeakMap(); const ob new IntersectionObserver((entries) > {for (const entrie…

草图导入3d后模型贴材质的步骤?---模大狮模型网

3D模型在导入草图大师后出现混乱可能有多种原因&#xff0c;以下是一些可能的原因和解决方法&#xff1a; 模型尺寸问题&#xff1a;如果3D模型的尺寸在导入草图大师时与画布尺寸不匹配&#xff0c;可能导致模型混乱。解决方法是在3D建模软件中调整模型的尺寸&#xff0c;使其适…

FreeRTOS使用计数信号量进行任务同步与资源管理

FreeRTOS使用计数信号量进行任务同步与资源管理 介绍 在多任务系统中&#xff0c;任务之间的同步和对共享资源的管理是非常重要的。FreeRTOS 提供了丰富的同步机制&#xff0c;其中计数信号量是一种强大的工具&#xff0c;用于实现任务之间的同步和对资源的访问控制。 什么是…

figure方法详解之清除图形内容

figure方法详解之清除图形内容 一 clf():二 clear():三 clear()方法和clf()方法的区别&#xff1a; 前言 Hello 大家好&#xff01;我是甜美的江。 在数据可视化中&#xff0c;Matplotlib 是一个功能强大且广泛使用的库&#xff0c;它提供了各种方法来创建高质量的图形。在 Mat…

unity 拖入文件 窗口大小

目录 unity 拖入文件插件 设置窗口大小 unity 拖入文件插件 GitHub - Bunny83/UnityWindowsFileDrag-Drop: Adds file drag and drop support for Unity standalong builds on windows. 设置窗口大小 file build

Iceberg从入门到精通系列之二十一:Spark集成Iceberg

Iceberg从入门到精通系列之二十一&#xff1a;Spark集成Iceberg 一、在 Spark 3 中使用 Iceberg二、添加目录三、创建表四、写五、读六、Catalogs七、目录配置八、使用目录九、替换会话目录十、使用目录特定的 Hadoop 配置值十一、加载自定义目录十二、SQL 扩展十三、运行时配置…