贪吃蛇
- 游戏介绍
- 1.前置知识
- 1.1 Win32API
- 1.2 控制台程序
- 1.3 坐标系统
- 1.4 GetStdHandle(获取句柄)
- 1.5 CONSOLE_CURSOR_INFO(控制台光标信息)
- 1.6 GetConsoleCursorInfo(获取光标信息)
- 1.7 SetConsoleCursorInfo(设置控制台光标信息)
- 1.8 SetConsoleCursorPosition(设置光标当前位置)
- 1.9 GetAsyncKeyState(获取按键情况)
- 1.10 宽字符
- 1.11 头文件<local.h>与setlocale函数
- 2.游戏设计
- 2.1地图设计
- 2. 2 蛇和食物
- 2.3 游戏各属性的维护
- 3.逻辑实现
- 3.1 类型声明
- 3.2 主程序
- 3.3 地图绘制
- 3.4 欢迎界面及帮助信息
- 3.5 初始化蛇
- 3.6 生成食物
- 3.7 碰撞检测
- 3.8 蛇的移动
- 3.9游戏运行逻辑
- 3.10 结束工作
- 4.完整代码
游戏介绍
贪吃蛇游戏是耳熟能详的益智小游戏,今天我们就用C语言来实现它!
1.前置知识
今天我们实现的贪吃蛇游戏是运行在控制台程序中(如图),在实现前需要了解一些Win32API以及控制台相关的知识.
游戏截图:
1.1 Win32API
API全称:ApplicationProgrammingInterface,API可以简单理解成函数,Win32API也就是Windows32位平台提供给我们的一系列可以帮助应用程序开启视窗、绘制图形等的接口函数,今天我们只需要了解以下函数即可:
GetStdHandle(获取句柄)
GetConsoleCursorInfo(获取光标信息)
CONSOLE_CURSOR_INFO(控制台光标信息)
SetConsoleCursorInfo(设置控制台光标信息)
SetConsoleCursorPosition(设置光标当前位置)
GetAsyncKeyState(获取按键情况)
1.2 控制台程序
我们写C语言程序运行时的黑框框就是控制台程序,使用Win+r键,输入cmd可以打开Windows的控制台程序
使用cmd命令可以设置控制台窗口的宽高和控制台名称
mode con cols=80 lines=30 //设置控制台宽高
title 贪吃蛇 //设置控制台名称
我们也可以使用C语言的system函数来执行上述操作
int main()
{
//设置控制台宽高
system("mode con cols=80 lines=30");
//设置控制台名称
system("title 贪吃蛇");
system("pause");//让程序暂停
return 0;
}
1.3 坐标系统
控制台坐标中每一个普通字符占一个坐标,输出字符时默认从原点开始输出
COORD是WindowsAPI中定义的结构体,表示字符在控制台上坐标位置
typedef struct _COORD {
SHORT X;//横坐标
SHORT Y;//纵坐标
} COORD, *PCOORD;
1.4 GetStdHandle(获取句柄)
GetStdHandle函数用于从设备(标准输入/输出设备)中获取句柄,可以通过句柄操作设备,句柄可以类比成游戏手柄,通过手柄可以操作游戏中的人物.
函数原型:
HANDLE GetStdHandle(DWORD nStdHandle);
参数取值:STD_INPUT_HANDLE(标准输入句柄),STD_OUTPUT_HANDLE(标准输出句柄)
返回值:HANDLE类型
运用实例
HANDLE handle = GetStdHandle(STD_INPUT_HANDLE);
//STD_INPUT_HANDLE其实就是我们运行后的控制台窗口
1.5 CONSOLE_CURSOR_INFO(控制台光标信息)
CONSOLE_CURSOR_INFO是Win32中定义的一个结构体,结构体中包含了控制台光标的信息
typedef struct _CONSOLE_CURSOR_INFO
{DWORD dwSize;BOOL bVisible;
}CONSOLE_CURSOR_INFO,*PCONSOLE_CURSOR_INFO;
dwSize表示光标占字符单元格的百分比,默认为25(百分之25),如图左边部分是光标的大小,右边部分是一个字符单元格
bVisible表示光标的可见性,默认值为true,表示光标可见,如果想隐藏光标,可以将bVisible的值置为false
1.6 GetConsoleCursorInfo(获取光标信息)
获取指定控制台的光标大小和可见性信息,函数原型:
BOOL GetConsoleCursorInfo(HANDLE hConsoleOutput,PCONSOLE_CURSOR_INFO lpConsoleCursorInfo);
第一个参数是设备的句柄,第二个参数是CONSOLE_CURSOR_INFO结构体的指针
运用实例:
//获取设备句柄HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//定义结构体CONSOLE_CURSOR_INFO CursorInfo = { 0 };//获取控制台光标信息GetConsoleCursorInfo(handle, &CursorInfo);
1.7 SetConsoleCursorInfo(设置控制台光标信息)
获取到光标信息后,如果想修改光标的信息,需要先修改再设置,使用SetConsoleCursorInfo函数可以设置光标信息.
函数原型:
BOOL SetConsoleCursorInfo(HANDLE hConsoleOutput,const CONSOLE_CURSOR_INFO*lpConsoleCursorInfo);
参数类型与GetConsoleCursorInfo函数一致,返回值为布尔类型
运用实例:
//获取设备句柄HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//定义结构体CONSOLE_CURSOR_INFO CursorInfo = { 0 };//获取控制台光标信息GetConsoleCursorInfo(handle, &CursorInfo);//修改光标信息CursorInfo.bVisible=false;//隐藏光标//设置光标信息SetConsoleCursorInfo(handle,&CursorInfo);
1.8 SetConsoleCursorPosition(设置光标当前位置)
通过SetConsoleCursorPosition函数可以设置光标当前的坐标位置,有了这个函数,我们就可以在任意坐标打印内容,而不是从原点开始一直打印.
函数原型:
BOOL SetConsoleCursorPosition(HANDLE hConsoleOutput,COORD dwCursorPosition);
第一个参数是句柄,第二个参数是COORD结构体
运用实例:
//封装成设置光标当前位置的函数,x,y表示坐标
void SetPos(short x, short y)
{COORD pos = { x, y };//获取句柄HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//设置标准输出上光标的位置为posSetConsoleCursorPosition(handle, pos);
}
1.9 GetAsyncKeyState(获取按键情况)
GetAsyncKeyState函数可以获取按键情况,它可以检测程序运行时某个按键是否被按过,某个按键是否正在被按下或者抬起.
函数原型:
SHORT GetAsyncKeyState(int vKey);
参数vKey指的是键盘上按键的虚拟键值,返回值是一个16位的short类型的数.如果这个数最高位是1,表示该按键当前状态是按下,如果是0表示当前状态是抬起;如果这个数最低位是1,表示该按键被按过,如果是0表示未按过.
运用实例:
//定义一个宏判断某个键是否被按过
#define CHECK_KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1)?1:0)
1.10 宽字符
游戏的墙体■,蛇身●,食物★等字符,都是宽字符.宽字符占2个字节,普通字符占一个字节.
1.11 头文件<local.h>与setlocale函数
在不同国家和地区,对时间的表示方式可能不同,对金钱的表示也有不同($和¥).有时我们想打印¥符号或者其他中文字符和宽字符时,显示的却是问号或者乱码.为了使程序能够在不同地区适应,因此有了setlocale函数.它可以改变程序运行时的地区
函数原型:
char* setlocale (int category, const char* locale);
切换到本地环境的方法:
setlocale(LC_ALL, "");
使用setlocale函数需要包含头文件local.h
在屏幕上打印宽字符的方法:
#include<stdio.h>
#include<locale.h>
int main()
{setlocale(LC_ALL, "");wprintf(L"%lc", L'■');return 0;
}
2.游戏设计
2.1地图设计
由于墙体和蛇,食物都是宽字符,一个宽字符横坐标占2个,纵坐标占一个.所以我们可以设计一个58(列)x27(行)的地图,在地图最边缘四周打印字符■表示墙.
2. 2 蛇和食物
蛇头我们用◆表示,身体我们用●表示,食物用★表示.使用单链表的数据结构将每个结点链接起来,每个结点保存了该结点的横纵坐标和下一个结点的地址.游戏开始时蛇的长度是5,食物在地图内随机生成,每个食物的类型和蛇身结点的类型一致,方便蛇吃食物后的连接.
2.3 游戏各属性的维护
属性包括蛇移动方向,蛇的移动速度,单个食物的分数,累计分数,游戏状态(正常运行,正常退出,撞墙,撞到自己),还有蛇头结点,食物.这些信息我们用一个结构体保存
3.逻辑实现
3.1 类型声明
蛇身结点
//一个蛇身的结点
typedef struct SnakeNode
{int x;//结点横坐标int y;//结点纵坐标struct SnakeNode* next;//指向下一个结点的指针
}SnakeNode;
typedef struct SnakeNode* pSnakeNode;//指向SnakeNode的指针类型
游戏属性的维护
typedef struct Snake
{pSnakeNode SnakeHead;//蛇头结点pSnakeNode Food;//指向食物的指针int Score;//游戏总分int FoodScore;//一个食物的分数int Speed;//蛇的速度(休眠时间)enum Status status;//游戏当前状态enum Direction dir;//蛇的移动方向
}Snake;//指向snake类型的指针
typedef struct Snake* pSnake;
蛇的移动方向,使用枚举类型
//蛇当前移动方向
enum Direction
{//上下左右UP = 1,DOWN,LEFT,RIGHT
};
游戏状态,使用枚举类型
//游戏状态
enum Status
{OK = 1,//正常运行EXIT,//退出KillByWall,//撞墙KillBySelf//撞到自己
};
蛇结点,食物,墙体
#define FOOD L'★'
#define BODY L'●'
#define WALL L'■'
3.2 主程序
主程序分为3个模块:1.GameStart游戏初始化2.GameRun游戏运行时的逻辑 3.GameEnd游戏结束的工作
GameStart函数的功能:进行游戏的初始化工作,包括:设置控制台的大小,设置控制台名称,打印欢迎界面,打印地图,初始化蛇的移动方向/速度,总分数,单个食物分数,游戏运行的状态等
GameRun函数的功能:实现蛇移动,改变移动方向,按键检测等功能
GameEnd的功能:游戏结束后打印提示信息,释放动态开辟的内存空间
在程序开始前,还需要适配本地环境,防止打印时出现乱码,为了使游戏结束后还能继续选择开始游戏,我们使用do while循环来实现
int main()
{//适配本地环境setlocale(LC_ALL, "");int ch = 0;do{Snake snake = { 0 };//创建贪吃蛇GameStart(&snake);//游戏初始化GameRun(&snake);//游戏运行时的逻辑GameEnd(&snake);//游戏结束后的工作SetPos(20, 15);printf("输入Y.重新开始 N.退出");ch = getchar();getchar();} while (ch=='Y'||ch=='y');SetPos(0,27);return 0;
}
3.3 地图绘制
绘制逻辑很简单,最上面一行从(0,0)坐标开始打印■字符.最下面一行从(0,25)坐标开始打印.最左边,横坐标都是0,纵坐标从0开始,每打印一次纵坐标加1.最右边同理.
每次打印前都需要定位一次坐标,为了避免代码重复,坐标定位可以封装成函数:
//控制台坐标定位
void SetPos(int x, int y)
{HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//获取句柄//根据句柄设置光标坐标COORD pos = { x,y };SetConsoleCursorPosition(handle, pos);
}
绘制地图函数:
//绘制地图
void DrawMap()
{//上SetPos(0, 0);for (int i = 0; i <= 56; i+=2){wprintf(L"%lc", WALL);}//下SetPos(0, 25);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);}}
3.4 欢迎界面及帮助信息
直接定位坐标,然后打印内容
void Welcome()
{//欢迎信息SetPos(40, 15);printf("欢迎来到贪吃蛇小游戏\n");SetPos(40, 20);system("pause");system("cls");SetPos(35, 10);printf("用↑↓←→控制移动方向,F3加速,F4减速");SetPos(35, 11);printf("加速得到更多分");SetPos(40, 20);system("pause");system("cls");
}
//打印帮助信息
void PrintHelpInfo()
{SetPos(65, 13);printf("↑↓←→控制方向,空格暂停,ESC退出,F3加速,F4减速");SetPos(65, 17);printf("版权@vampire-wpre");
}
3.5 初始化蛇
首先创建5个蛇身结点,设置游戏开始时蛇的位置,设置游戏初始属性
void InitSnake(pSnake ps)
{//创建5个初始蛇身结点pSnakeNode cur = NULL;for (int i = 0; i < 5; i ++ ){cur = (pSnakeNode)malloc(sizeof(SnakeNode));assert(cur);cur->x = POS_X + 2 * i;cur->y = POS_Y;cur->next = NULL;if (ps->SnakeHead == NULL){ps->SnakeHead = cur;}else{//头插法cur->next = ps->SnakeHead;ps->SnakeHead = cur;}}//打印初始蛇cur = ps->SnakeHead;SetPos(cur->x, cur->y);wprintf(L"%lc", L'◆');cur = cur->next;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//其他属性ps->dir = RIGHT;ps->Food = NULL;ps->FoodScore = 10;ps->Score = 0;ps->Speed = 200;ps->status = OK;
}
3.6 生成食物
使用rand函数随机生成食物坐标,食物的坐标必须在地图范围内,并且不能和蛇的位置重合,生成好食物后在对应位置打印食物.因为食物也是宽字符,所以食物的横坐标值必须是2的倍数
//创建食物
void CreatFood(pSnake ps)
{//坐标是随机出现的,坐标不能在蛇身体和墙上int x = 0;int y = 0; srand((unsigned int)time(NULL));//设置随机数种子
reset:do{x = rand() % 53 + 2;y = rand() % 24 + 1;} while (x % 2 != 0);//如果横坐标不是2倍数则重新生成坐标pSnakeNode cur = ps->SnakeHead;while (cur)//循环遍历蛇的每一个结点{//如果坐标与蛇重合,使用goto语句重新生成坐标if (x == cur->x && y == cur->y){goto reset;}cur = cur->next;}pSnakeNode pfood = (SnakeNode*)malloc(sizeof(SnakeNode));if (pfood == NULL){return;}//设置坐标pfood->x = x;pfood->y = y;ps->Food = pfood;SetPos(x, y);wprintf(L"%lc", FOOD);
}
3.7 碰撞检测
逻辑很简单,撞墙只需要判断蛇头坐标是否和墙的坐标重合;撞自己则判断蛇头坐标和蛇身每个结点的坐标是否重合,此时需要循环遍历蛇身.
//检测是否撞墙
void CheckKillByWall(pSnake ps)
{if ((ps->SnakeHead->x == 0 || ps->SnakeHead->x == 56) || (ps->SnakeHead->y == 0 || ps->SnakeHead->y == 25)){ps->status = KillByWall;}
}//检测是否撞自己
void CheckKillBySelf(pSnake ps)
{pSnakeNode cur = ps->SnakeHead->next;while (cur){if (cur->x == ps->SnakeHead->x && cur->y == ps->SnakeHead->y){ps->status = KillBySelf;return;}cur = cur->next;}
}
3.8 蛇的移动
蛇移动分为以下几种情况:
1.蛇直走:
此时,直接改变蛇头的坐标即可.根据蛇的属性(status)判断蛇的方向,如果是向上,蛇头的横坐标不变,向上走一步纵坐标的值就-1;如果是向下,蛇头横坐标不变,向上走一步纵坐标的值+1;如果是向左,蛇头纵坐标不变,向左走一步横坐标-2(宽字符占2个位置);如果是向右,纵坐标不变,向右走一步横坐标+2.
2.蛇转弯:
先定义一个临时结点,该结点的位置在蛇头下一步的位置.
判断蛇头在下一步的位置有没有食物,如果有就吃食物,吃完食物释放掉旧的食物并且重新生成新的食物对应总分也增加,头插法将这个食物结点插入蛇身,然后改变蛇头结点(SnakeHead)的指向;如果不是食物,也将该结点头插,改变蛇头结点(SnakeHead)的指向,接着将最后一个结点删除,并且在最后一个结点的位置打印空格.
我们定义IsFood函数判断下一个位置有没有食物,Eat函数实现吃食物的逻辑,Normal函数实现下一个位置不是食物进行的操作,与此同时还要检测一下有没有撞墙和撞自己.
//吃食物
void Eat(pSnake ps, pSnakeNode next)
{next->next = ps->SnakeHead;ps->SnakeHead = next;//打印蛇pSnakeNode cur = ps->SnakeHead;SetPos(cur->x, cur->y);wprintf(L"%lc", L'◆');cur = cur->next;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//增加总分ps->Score += ps->FoodScore;//free旧的食物free(ps->Food);//新建食物CreatFood(ps);
}
//下一个位置不是食物,正常移动
void Normal(pSnake ps, pSnakeNode next)
{//头插法插入nextnext->next = ps->SnakeHead;ps->SnakeHead = next;//释放尾结点pSnakeNode cur = ps->SnakeHead;//循环找尾结点的前一个结点while (cur->next->next != NULL){cur = cur->next;}//尾结点位置打印成空格SetPos(cur->next->x, cur->next->y);printf(" ");//释放尾结点free(cur->next);cur->next = NULL;//打印蛇身cur = ps->SnakeHead;SetPos(cur->x, cur->y);wprintf(L"%lc", L'◆');cur = cur->next;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}
}
//蛇移动
void Move(pSnake ps)
{pSnakeNode next = (pSnakeNode)malloc(sizeof(SnakeNode));//方向改变时,蛇头在下一个坐标的位置,用next结点保存if (next == NULL){return;}next->next = NULL;switch (ps->dir){case UP:next->x = ps->SnakeHead->x;next->y = ps->SnakeHead->y - 1;break;case DOWN:next->x = ps->SnakeHead->x;next->y = ps->SnakeHead->y + 1;break;case LEFT:next->x = ps->SnakeHead->x - 2;next->y = ps->SnakeHead->y;break;case RIGHT:next->x = ps->SnakeHead->x + 2;next->y = ps->SnakeHead->y;break;}//下一个坐标处是否为食物:if (IsFood(ps, next)){//是食物就吃掉Eat(ps,next);}else{//不是食物就正常移动Normal(ps, next);}//检测是否撞墙CheckKillByWall(ps);//检测是否撞自己CheckKillBySelf(ps);
}
3.9游戏运行逻辑
使用do while循环,循环条件是游戏状态,如果游戏状态不是OK则游戏结束.游戏运行时,打印对应的帮助信息,同时进行按键检测,如果某个按键被按了,就修改对应的属性.蛇的移动本质上是:蛇移动一步,系统休眠一段时间,休眠时间越短,蛇移动的越快,休眠时间越长,蛇移动的越慢.
//游戏运行逻辑
void GameRun(pSnake ps)
{//打印帮助信息PrintHelpInfo();do{//当前分数情况:SetPos(65, 10);printf("总分:%02d\n", ps->Score);SetPos(65, 11);printf("每个食物分数:%5d\n", ps->FoodScore);//按键监测↑ ↓ ← → ESC 空格 F3加速 F4减速if (CHECK_KEY_PRESS(VK_UP) && ps->dir != DOWN)//按上键并且当前方向不是向下{ps->dir = UP;}else if (CHECK_KEY_PRESS(VK_DOWN) && ps->dir != UP)//按下键并且当前方向不是向上{ps->dir = DOWN;}else if (CHECK_KEY_PRESS(VK_LEFT) && ps->dir != RIGHT)//按左键并且当前方向不是向右{ps->dir = LEFT;}else if (CHECK_KEY_PRESS(VK_RIGHT) && ps->dir != LEFT)//按右并且当前方向不是向左{ps->dir = RIGHT;}else if (CHECK_KEY_PRESS(VK_ESCAPE))//按ESC退出{ps->status = EXIT;break;}else if (CHECK_KEY_PRESS(VK_SPACE))//按空格暂停,再按一次继续游戏{while (1){Sleep(100);if (CHECK_KEY_PRESS(VK_SPACE)){break;}}}else if (CHECK_KEY_PRESS(VK_F3))//按F3加速,休眠时间变短,食物分数变大{if (ps->Speed >= 40)//最小速度为40{ps->Speed -= 20;ps->FoodScore += 2;}}else if (CHECK_KEY_PRESS(VK_F4))//按F4减速,休眠时间变长,食物分数变小{if (ps->FoodScore > 4){ps->Speed += 20;ps->FoodScore -= 2;}}//休眠一下Sleep(ps->Speed);//蛇移动的逻辑Move(ps);} while (ps->status==OK);
}
3.10 结束工作
游戏结束后,打印结束的原因(撞墙/撞自己/主动退出),同时释放malloc开辟的空间(蛇的每个结点/食物)
void GameEnd(pSnake ps)
{switch (ps->status){case EXIT:SetPos(24, 12);printf("主动退出");break;case KillByWall:SetPos(24, 12);printf("撞墙了!");break;case KillBySelf:SetPos(24, 12);printf("撞到自己了!");break;}//释放malloc开辟的空间pSnakeNode cur = ps->SnakeHead;pSnakeNode del = NULL;while (cur){del = cur;cur = cur->next;free(del);}free(ps->Food);ps->Food = NULL;ps = NULL;
}
4.完整代码
Game.h文件:
#pragma once
#define _CRT_SECURE_NO_WARNINGS 1#include<stdio.h>
#include<stdlib.h>
#include<Windows.h>
#include<locale.h>
#include<stdbool.h>
#include<assert.h>
#include<time.h>#define FOOD L'★' //食物
#define BODY L'●' //身体
#define WALL L'■' //墙//按键监测
#define CHECK_KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1)?1:0)//游戏开始时,蛇起始位置坐标
#define POS_X 24
#define POS_Y 5//一个蛇身的结点
typedef struct SnakeNode
{int x;//结点横坐标int y;//结点纵坐标struct SnakeNode* next;//指向下一个结点的指针
}SnakeNode;
typedef struct SnakeNode* pSnakeNode;//指向SnakeNode的指针类型//游戏状态
enum Status
{OK = 1,//正常运行EXIT,//退出KillByWall,//撞墙KillBySelf//撞到自己
};//蛇当前移动方向
enum Direction
{//上下左右UP = 1,DOWN,LEFT,RIGHT
};//贪吃蛇(整个游戏的维护)
typedef struct Snake
{pSnakeNode SnakeHead;//蛇头结点pSnakeNode Food;//指向食物的指针int Score;//游戏总分int FoodScore;//一个食物的分数int Speed;//蛇的速度(休眠时间)enum Status status;//游戏当前状态enum Direction dir;//蛇的移动方向
}Snake;//指向snake类型的指针
typedef struct Snake* pSnake;//相关函数
void GameStart(pSnake ps);//游戏初始化void Welcome();//打印欢迎界面void DrawMap();//绘制地图void InitSnake(pSnake ps);//初始化蛇void CreatFood(pSnake ps);//创建食物void GameRun(pSnake ps);//游戏运行逻辑void PrintHelpInfo();//打印帮助信息void Move(pSnake ps);//蛇移动int IsFood(pSnake ps, pSnakeNode next);//判断下一步位置处是否为食物void Eat(pSnake ps, pSnakeNode next);//吃食物void Normal(pSnake ps, pSnakeNode next);//正常移动void CheckKillByWall(pSnake ps);//检测是否撞墙void CheckKillBySelf(pSnake ps);//检测是否撞自己void GameEnd(pSnake ps);//游戏结束后的善后工作void SetPos(int x, int y);//控制台光标定位
Game.c文件:
#include"Game.h"//控制台光标定位
void SetPos(int x, int y)
{HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//获取句柄//根据句柄设置光标坐标COORD pos = { x,y };SetConsoleCursorPosition(handle, pos);
}//绘制地图
void DrawMap()
{//上SetPos(0, 0);for (int i = 0; i <= 56; i+=2){wprintf(L"%lc", WALL);}//下SetPos(0, 25);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 Welcome()
{//欢迎信息SetPos(40, 15);printf("欢迎来到贪吃蛇小游戏\n");SetPos(40, 20);system("pause");system("cls");SetPos(35, 10);printf("用↑↓←→控制移动方向,F3加速,F4减速");SetPos(35, 11);printf("加速得到更多分");SetPos(40, 20);system("pause");system("cls");
}//打印版权信息
void PrintHelpInfo()
{SetPos(65, 16);printf("版权@vampire-wpre");}//创建食物
void CreatFood(pSnake ps)
{//坐标是随机出现的,坐标不能在蛇身体和墙上int x = 0;int y = 0; srand((unsigned int)time(NULL));//设置随机数种子
reset:do{x = rand() % 53 + 2;y = rand() % 24 + 1;} while (x % 2 != 0);pSnakeNode cur = ps->SnakeHead;while (cur){if (x == cur->x && y == cur->y){goto reset;}cur = cur->next;}pSnakeNode pfood = (SnakeNode*)malloc(sizeof(SnakeNode));if (pfood == NULL){return;}//设置坐标pfood->x = x;pfood->y = y;ps->Food = pfood;SetPos(x, y);wprintf(L"%lc", FOOD);
}//判断下一个位置是否为食物
int IsFood(pSnake ps, pSnakeNode next)
{if (ps->Food->x == next->x && ps->Food->y == next->y){return 1;}else{return 0;}
}//检测是否撞墙
void CheckKillByWall(pSnake ps)
{if ((ps->SnakeHead->x == 0 || ps->SnakeHead->x == 56) || (ps->SnakeHead->y == 0 || ps->SnakeHead->y == 25)){ps->status = KillByWall;}
}//检测是否撞自己
void CheckKillBySelf(pSnake ps)
{pSnakeNode cur = ps->SnakeHead->next;while (cur){if (cur->x == ps->SnakeHead->x && cur->y == ps->SnakeHead->y){ps->status = KillBySelf;return;}cur = cur->next;}
}//初始化蛇
void InitSnake(pSnake ps)
{//创建5个初始蛇身结点pSnakeNode cur = NULL;for (int i = 0; i < 5; i ++ ){cur = (pSnakeNode)malloc(sizeof(SnakeNode));assert(cur);cur->x = POS_X + 2 * i;cur->y = POS_Y;cur->next = NULL;if (ps->SnakeHead == NULL){ps->SnakeHead = cur;}else{//头插cur->next = ps->SnakeHead;ps->SnakeHead = cur;}}//打印初始蛇cur = ps->SnakeHead;SetPos(cur->x, cur->y);wprintf(L"%lc", L'◆');cur = cur->next;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}//其他属性ps->dir = RIGHT;ps->Food = NULL;ps->FoodScore = 10;ps->Score = 0;ps->Speed = 200;ps->status = OK;
}//吃食物
void Eat(pSnake ps, pSnakeNode next)
{next->next = ps->SnakeHead;ps->SnakeHead = next;//打印蛇pSnakeNode cur = ps->SnakeHead;SetPos(cur->x, cur->y);wprintf(L"%lc", L'◆');cur = cur->next;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}ps->Score += ps->FoodScore;//free旧的食物free(ps->Food);//新建食物CreatFood(ps);
}//下一个位置不是食物,正常移动
void Normal(pSnake ps, pSnakeNode next)
{//头插法插入nextnext->next = ps->SnakeHead;ps->SnakeHead = next;//释放尾结点pSnakeNode cur = ps->SnakeHead;while (cur->next->next != NULL){cur = cur->next;}//尾结点位置打印成空格SetPos(cur->next->x, cur->next->y);printf(" ");//释放尾结点free(cur->next);cur->next = NULL;//打印蛇身cur = ps->SnakeHead;SetPos(cur->x, cur->y);wprintf(L"%lc", L'◆');cur = cur->next;while (cur){SetPos(cur->x, cur->y);wprintf(L"%lc", BODY);cur = cur->next;}
}//蛇移动
void Move(pSnake ps)
{pSnakeNode next = (pSnakeNode)malloc(sizeof(SnakeNode));//方向改变时,蛇头在下一个坐标的位置,用next结点保存if (next == NULL){return;}next->next = NULL;switch (ps->dir){case UP:next->x = ps->SnakeHead->x;next->y = ps->SnakeHead->y - 1;break;case DOWN:next->x = ps->SnakeHead->x;next->y = ps->SnakeHead->y + 1;break;case LEFT:next->x = ps->SnakeHead->x - 2;next->y = ps->SnakeHead->y;break;case RIGHT:next->x = ps->SnakeHead->x + 2;next->y = ps->SnakeHead->y;break;}//下一个坐标处是否为食物:if (IsFood(ps, next)){//是食物就吃掉Eat(ps,next);}else{//不是食物就正常Normal(ps, next);}//检测是否撞墙CheckKillByWall(ps);//检测是否撞自己CheckKillBySelf(ps);}//游戏初始化
void GameStart(pSnake ps)
{//设置控制台属性system("mode con cols=100 lines=30");system("title 贪吃蛇");//隐藏光标HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//获取控制台句柄CONSOLE_CURSOR_INFO cur_info;GetConsoleCursorInfo(handle, &cur_info);//获得控制台光标的信息cur_info.bVisible = false;SetConsoleCursorInfo(handle, &cur_info);//设置//打印欢迎信息Welcome();//地图绘制DrawMap();//初始化蛇InitSnake(ps);//创建食物CreatFood(ps);
}//游戏运行逻辑
void GameRun(pSnake ps)
{//打印帮助信息PrintHelpInfo();do{//当前分数情况:SetPos(65, 10);printf("总分:%02d\n", ps->Score);SetPos(65, 11);printf("每个食物分数:%5d\n", ps->FoodScore);//按键监测↑ ↓ ← → ESC 空格 F3加速 F4减速if (CHECK_KEY_PRESS(VK_UP) && ps->dir != DOWN)//按上键并且当前方向不是向下{ps->dir = UP;}else if (CHECK_KEY_PRESS(VK_DOWN) && ps->dir != UP)//按下键并且当前方向不是向上{ps->dir = DOWN;}else if (CHECK_KEY_PRESS(VK_LEFT) && ps->dir != RIGHT)//按左键并且当前方向不是向右{ps->dir = LEFT;}else if (CHECK_KEY_PRESS(VK_RIGHT) && ps->dir != LEFT)//按右并且当前方向不是向左{ps->dir = RIGHT;}else if (CHECK_KEY_PRESS(VK_ESCAPE))//按ESC退出{ps->status = EXIT;break;}else if (CHECK_KEY_PRESS(VK_SPACE))//按空格暂停,再按一次继续游戏{while (1){Sleep(100);if (CHECK_KEY_PRESS(VK_SPACE)){break;}}}else if (CHECK_KEY_PRESS(VK_F3))//按F3加速,休眠时间变短,食物分数变大{if (ps->Speed >= 40)//最小速度为40{ps->Speed -= 20;ps->FoodScore += 2;}}else if (CHECK_KEY_PRESS(VK_F4))//按F4减速,休眠时间变长,食物分数变小{if (ps->FoodScore > 4){ps->Speed += 20;ps->FoodScore -= 2;}}//休眠一下Sleep(ps->Speed);//走一步Move(ps);//蛇移动的逻辑} while (ps->status==OK);}//游戏善后工作
void GameEnd(pSnake ps)
{switch (ps->status){case EXIT:SetPos(24, 12);printf("主动退出");break;case KillByWall:SetPos(24, 12);printf("撞墙了!");break;case KillBySelf:SetPos(24, 12);printf("撞到自己了!");break;}//释放malloc开辟的空间pSnakeNode cur = ps->SnakeHead;pSnakeNode del = NULL;while (cur){del = cur;cur = cur->next;free(del);}free(ps->Food);ps->Food = NULL;ps = NULL;
}
test.c文件:
#include"Game.h"void test()
{int ch = 0;do{Snake snake = { 0 };//创建贪吃蛇GameStart(&snake);//游戏初始化GameRun(&snake);//游戏运行时的逻辑GameEnd(&snake);//游戏结束后的善后工作SetPos(20, 15);printf("输入Y.重新开始 N.退出");ch = getchar();getchar();} while (ch=='Y'||ch=='y');}int main()
{//适配本地环境setlocale(LC_ALL, "");test();SetPos(0,27);return 0;
}