目录
1.游戏实现演示
2.实现功能
3.游戏架构
4.游戏预准备
5.游戏逻辑分析
6.参考代码
目录
1.游戏实现演示

2.实现功能
●贪吃蛇地图绘制
●蛇吃食物的功能
●蛇撞墙死亡
●蛇吃到自己死亡
●计算得分
●蛇身加速、减速
●暂停、退出游戏
3.游戏架构

4.游戏预准备
在上一篇文章中我们讨论了win32API,可以利用win32API来实现对窗口的控制,像窗口大小、设置窗口名字、获取光标位置等信息由于在上一章中已讲到,因此本章不在提及。
4.1地图
我们打印地图使用宽字符,这样可以使地图更具美感。我们打印墙体使用‘▢’,打印蛇身使用‘●’,打印食物使用‘⨁’。宽字符类型为wchar_t,而要想使用宽字符,我们可以引入<locale.h>头文件,其中提供了了允许程序员针对特定地区调整程序行为的函数。
4.1.1<locale.h>本地化
<locale.h>提供的函数用于控制C标准库中对于不同地区会产生不一样行为的部分。
在标准中,依赖地区的部分有以下几项:
●数字量的格式
●货币量的格式
●字符集
●日期和时间的表示形式
4.1.2类项
通过修改地区,程序可以改变它的行为来适应世界的不同区域。但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望改变的。所以C语言支持针对不同的类项进行修改,下面的一个宏,指定一个类项:
●LC_COLLATE:影响字符串比较函数strcoll()和strxfrm()。
●LC_CTYPE:影响字符处理函数的行为。
●LC_MONETARY:影响货币格式。
●LC_NUMERIC:影响printf ()的数字格式。
●LC_TIME:影响时间格式strftime()和wcsftime()。
●LC_ALL:针对所有类项修改,将以上所有类别设置为给定的语言环境。
4.1.3setlocale函数
char* setlocale(int category,const char * locale);
setloacle函数用于修改当前地区,可以针对一个类项修改,也可以针对所有类项。
setlocale的第一个参数是前面的一个类项,第二个参数仅定义2种可能取值:“C”(正常模式)和“ ”(本地模式)。
在任意程序执行开始,都会隐式执行调用:
setlocale(LC_ALL,"C");
当我们切换到本地模式后就可以支持宽字符的输出
setlocale(LC_ALL," ")//切换到本地环境
4.1.4宽字符的打印
宽字符的字面量必须加上前缀“L”,否则c语言会把字面量当作窄字符类项处理。前缀“L”在单引号前面,表示宽字符,对应我printf()的占位为%lc;在双引号前面,表示宽字符串,对应wprintf()的占位符为%ls。
示例:
#include <stdio.h>
#include <locale.h>
int main(){
setlocale(LC_ALL," ");
wchar_t ch1=L"宽";
wchar_t ch2=L"字";
wchar_t ch3=L"符";
wprintf(L"%lc"L"%lc"L"%lc",ch1,ch2,ch3);
return 0;
}
输出“宽字符”,其中每个字符占两格(x轴方向)
4.1.5地图坐标
我们假设实现一个棋盘27行,58列棋盘,在地图中画出墙。

4.2蛇身和食物
初始化状态,假设蛇长为5,蛇身的每个节点是"●",在一固定位,入(24,25)处开始出现蛇,连续5个结点。
注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的一个节点出现在墙体中;食物在墙体内随机生成一个坐标(x坐标必须为2的倍数),且坐标不能和蛇的身体重合,然后打印“⨁”。
4.3数据结构设计
在游戏运行过程中,蛇每次吃一个食物,蛇的身体会变长一节,如果我们使用链表存储蛇的信息,那么蛇的每一节就是链表中的每个节点。每个节点存储蛇身节点在地图上的坐标。
蛇节点链表:
typedef struct SnakeNode{
int x;
int y;
struct SnakeNode *next ;
}SnakeNode,*pSnakeNode;
要管理整条贪吃蛇,我们继续封装一个Snake的结构来维护整条贪吃蛇:
typedef struct Snake{
SnakeNode * _snake;//维护整条蛇的指针
SnakeNode *_food;//维护食物的指针
Dirction _dir;//蛇头方向,默认向右
Status _sta;//游戏状态
int _foodwight;//默认每个食物10分
int _score;//游戏当前获得分数
int _sleeptime;//每走一步休眠时间
}Snake,*pSnake;
蛇的方向,可以一一列举,使用枚举
//蛇的方向
enum Dirction{
UP=1,
DOWN,
LEFT,
RIGHT
};
游戏状态,可以一一列举,使用枚举
enum Status{
OK,
KILL_BY_WALL,
KILL_BY_SELF,
END_NORAML
};
5.游戏主逻辑
程序开始就设置程序支持本地模式,然后进入游戏主逻辑。
主逻辑分为3个过程:
●Gamestart:完成游戏的初始化
●GameRun:完成游戏运行逻辑的实现
●GameEnd:完成游戏结束的说明,实现资源释放
#include "snake.h"
void Game(){
srand((unsigned int)time(NULL));
char ch;
do{
Snake ps{0};
GameStart(&ps);
GameRun(&ps);
GameEnd(&ps);
SetPos(20,15);
printf("是否再来一局:Y/N");
ch=getchar();
getchar();//清理'\n'
}while(ch=='Y'||ch=='y');
SetPos(20,26);
}
int main(){
setlocale(LC_ALL," ");
SetConsoleOutputCP(CP_UTF8);//防止输出中文时乱码
Game();
return 0;
}
5.1GameStart
本模块功能:
●控制台窗口大小和名字
●光标的隐藏
●打印欢迎界面
●创建地图
●初始化蛇身
●创建食物
void GameStart(pSnake ps){
//设置窗口大小
system("mode con cols=100 lines=30");
system("title snake");//设置窗口名字
HANDLE hOutput=GetStdHandle(STD_OUTPUT_HANDLE);//获得标准输出句柄
//隐藏光标
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput,&CursorInfo);//获取控制台光标信息
CursorInfo.bVisible=false;//隐藏控制台光标
SetConsoleCursorInfo(hOutput,&CursorInfo);//设置控制台光标状态
Welcome();// 打印欢迎界面
SetMap();
InitSnake(ps);
SetFood(ps);
}
5.1.1打印欢迎页面
定位光标位置的函数上文已讲过,文本不在赘述
void SetPos(short x,short y){
COORD pos={x,y};
HANDLE hOutput=NULL;
hOutput=GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出光标坐标为pos
SetConsoleCursorPosition(hOutput,pos);
}
游戏开始前的功能提醒:
void Welcome(){
SetPos(34,13);
printf("欢迎来到贪吃蛇小游戏");
SetPos(30,20);
system("pause");
system("cls");
SetPos(30,13);
printf("用↑,ↆ,←,→,分别控制蛇的移动,F3加速,F4减速");
SetPos(30,14);
printf("加速能获得更高的分数");
SetPos(30,20);
system("pause");
system("cls");
}


5.1.2创建地图
创建地图就是将墙打印出来,因为宽字符打印,使用wprintf函数,打印格式串前使用L。
打印地图的关键是要计算好坐标,才能在想要的位置打印墙体。
墙体打印的宽字符:
#define WALL L'■'
void SetMap(){
SetPos(0,0);
for(int i=0;i<58;i+=2) wprintf(L"%lc",WALL);
SetPos(0,26);
for(int i=0;i<58;i+=2) wprintf(L"%lc",WALL);
for(int i=1;i<26;i++){SetPos(0,i);wprintf(L"%lc",WALL);}
for(int i=1;i<26;i++){SetPos(56,i);wprintf(L"%lc",WALL);}
}
5.1.3初始化蛇身
蛇最开始长度为5节,每节对应链表的一个节点,蛇身的每个节点都有自己的坐标。
创建5个节点,然后将每个节点存放在链表中进行管理。创建完蛇身后,将蛇的每一节打印在屏幕上。
●蛇的初始位置从(24,5)开始
●游戏状态是:OK
●蛇的移动速度:200毫秒
●蛇的默认方向:RIGHT
●初始成绩:0
●每个食物的分数:10
蛇身打印宽字符:
#define BODY L'●'

效果:
5.1.4创建食物
●先随机生成食物的坐标
x坐标必须是2的倍数
食物的坐标不能和蛇身的每个节点坐标重复
●创建食物节点,打印食物
食物打印的宽字符:
#define FOOD L'★'
5.2GameRun
游戏运行期间,右侧打印帮助信息,提示玩家,坐标开始位置(64,15)
根据游戏状态检查游戏是否继续,若状态为OK,游戏继续,否则游戏结束。
若游戏继续,检查按键情况,确定蛇的下一步方向和加减速情况,是否暂停或退出游戏。
需要用到的按键虚拟值:
●上:VK_UP
●下:VK_DOWN
●左:VK_LEFT
●右:VK_RIGHT
●空格:VK_SPACE
●ESC:VK_ESCAPE
●F3:VK_F3
●F4:VK_F4

5.2.1KEY_PRESS
为了方便,检测按键状态的函数我们封装了一个宏:
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1)?1:0)
5.2.2HelpInfo
void HelpInfo(){
SetPos(64,15);
printf("不能穿墙,不能咬到自己");
SetPos(64,16);
printf("用用↑,ↆ,←,→,分别控制蛇的移动");
SetPos(64,17);
printf("F3为加速,F4为减速");
SetPos(64,18);
printf("ESC:退出游戏,space:暂停游戏");
}
5.2.3SnakeMove
先创建下一个节点,根据移动方向和蛇头坐标,判断蛇移到下一个位置的坐标。
确定了下一个位置后,看下一个位置是否是食物(Nest_is_Food),是食物就吃掉食物(EatFood),若不是食物,则做相应处理(NoFood)
而在蛇移动后,判断此次移动是否会造成撞墙(Kill_by_Wall)或吃到自己 (Kill_by_Self),影响游戏状态。

5.2.3.1Next_is_Food
bool Next_is_Food(pSnakeNode pNextNode,pSnake ps){
return (pNextNode->x==ps->_food->x&&pNextNode->y==ps->_food->y);
}
5.2.3.2EatFood
void EatFood(pSnakeNode pNextNode,pSnake ps){
pNextNode->next=ps->_snake;
ps->_snake=pNextNode;
pSnakeNode cur=ps->_snake;
//打印蛇
while(cur){
SetPos(cur->x,cur->y);
wprintf(L'%c',BODY);
cur=cur->next;
}
ps->_score+=ps->_foodwight;
free(ps->_food);//释放掉此节点
SetFood(ps);//重新生成食物
}
5.2.3.3NoFood
将下一个节点头插进蛇身,并将蛇身最后一个节点打印为空格,释放掉最后一个节点。
void NoFood(pSnakeNode pNextNode,pSnake ps){
pNextNode->next=ps->_snake;
ps->_snake=pNextNode;
pSnakeNode cur=ps->_snake;
while(cur->next->next!=NULL){
SetPos(cur->x,cur->y);
wprintf(L'%c',BODY);
cur=cur->next;
}
SetPos(cur->next->x,cur->next->y);
printf(" ");
free(cur->next);
cur->next=NULL;
}
注:一定要将最后一个节点释放并置为NULL,否则肯能会发生越界
5.2.3.4Kill_by_Wall
只需检查蛇头是否和墙坐标重合
void Kill_by_Wall(pSnake ps){
if(ps->_snake->x==0||ps->_snake->x==56
||ps->_snake->y==0||ps->_snake->y==30){
ps->_sta=KILL_BY_WALL;
}
}
5.2.3.5Kill_by_Self
只需检查蛇头是否和蛇身坐标重合
void Kill_by_Self(pSnake ps){
pSnakeNode cur=ps->_snake;
while(cur){
if(cur->x=ps->_snake->x&&cur->y==ps->_snake->y){
ps->_sta=KILL_BY_SELF;
break;
}
}
}
5.3GameEnd
当游戏状态不再是OK时,打印游戏死亡原因,并释放节点

6.参考代码
完整代码,三个文件实现
snake_test.cpp

snake.h



snake.cpp










911

被折叠的 条评论
为什么被折叠?



