大家好,今天我带着大家从0构建起贪吃蛇项目的高楼大厦~
要实现这个游戏,我们需要实现哪些功能呢?
实现基本的功能:
• 贪吃蛇地图绘制
• 蛇吃⻝物的功能(上、下、左、右⽅向键控制蛇的动作)
• 蛇撞墙死亡
• 蛇撞⾃⾝死亡
• 计算得分
• 蛇⾝加速、减速
• 暂停游戏
Win32API
调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程序达到开启
视窗、描绘图形、使⽤周边设备等⽬的。
控制台程序
平时我们运行起来的黑框程序其实就是控制台程序。
cmd命令来设置控制台窗⼝的⻓宽:例如设置控制台窗口的大小为30行,100列。
mode con cols=100 lines=30
也可以设置控制台窗口的名字:
title 贪吃蛇
这些能在控制台窗⼝执⾏的命令,也可以调⽤C语⾔函数system来执⾏。例如:
#include <stdio.h> int main() { system("mode con cols=100 lines=30"); //设置cmd窗⼝名称 system("title 贪吃蛇"); return 0; }
控制台屏幕上的坐标COORD
COORD是WindowsAPI中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕幕缓冲区上的坐标,坐标系(0,0)。
COORD类型的声明
1. typedef struct _COORD { 2. SHORT X; 3. SHORT Y; 4. } COORD, *PCOORD;
给坐标赋值
COORD pos = { 10, 15 };
GetStdHandle
GetStdHandle是⼀个WindowsAPI函数。它⽤于从⼀个特定的标准设备(标准输⼊、标准输出或标准错误)中取得⼀个句柄(⽤来标识不同设备的数值),使⽤这个句柄可以操作设备。
HANDLE GetStdHandle(DWORD nStdHandle);
例子:
1. HANDLE hOutput = NULL; 2. //获取标准输出的句柄(⽤来标识不同设备的数值) 3. hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标⼤⼩和可⻅性的信息
BOOL WINAPI GetConsoleCursorInfo( HANDLE hConsoleOutput, PCONSOLE_CURSOR_INFO lpConsoleCursorInfo ); PCONSOLE_CURSOR_INFO 是指向 CONSOLE_CURSOR_INFO 结构的指针,该结构接收有关主机游标
例子:
HANDLE hOutput = NULL; //获取标准输出的句柄(⽤来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE); CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
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; //隐藏控制台光标
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);//设置控制台光标状态 SetConsoleCursorPosition 我们将想要设置的坐标信息放在COORD类型的pos中,调⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置。 BOOL WINAPI SetConsoleCursorPosition( HANDLE hConsoleOutput, COORD pos }; 例子: COORD pos = { 10, 5}; HANDLE hOutput = NULL; //获取标准输出的句柄(⽤来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE); //设置标准输出上光标的位置为pos SetConsoleCursorPosition(hOutput, pos);
SetPos:封装⼀个设置光标位置的函数
//设置光标的坐标 void SetPos(short x, short y) { COORD pos = { x, y }; HANDLE hOutput = NULL; //获取标准输出的句柄(⽤来标识不同设备的数值) hOutput = GetStdHandle(STD_OUTPUT_HANDLE); //设置标准输出上光标的位置为pos SetConsoleCursorPosition(hOutput, pos); }
GetAsyncKeyState
获取按键情况
SHORT GetAsyncKeyState( int vKey; }
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态。
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
#include <stdio.h> #include <windows.h> int main() { while (1) { if (KEY_PRESS(0x30)) { printf("0\n"); } } }
设计地图
窗⼝的坐标如下所⽰,横向的是X轴,从左向右依次增⻓,纵向是Y轴,从上到下依次增⻓。
C语⾔的标准中不断加⼊了国际化的⽀持。⽐如:加⼊了宽字符的类型
wchar_t 和宽字符的输⼊和输出函数,加⼊了<locale.h>头⽂件,其中提供了允许程序员针对特定
地区(通常是国家或者说某种特定语⾔的地理区域)调整程序⾏为的函数。
<locale.h>本地化
类项
LC_COLLATE:影响字符串⽐较函数 strcoll() 和 strxfrm() 。
• LC_CTYPE:影响字符处理函数的⾏为。
• LC_MONETARY:影响货币格式。
• LC_NUMERIC:影响 printf() 的数字格式。
• LC_TIME:影响时间格式 strftime() 和 wcsftime() 。
• LC_ALL:针对所有类项修改,将以上所有类别设置为给定的语⾔环境。
setlocale函数
char* setlocale (int category, const char* locale);
C标准给第⼆个参数仅定义了2种可能取值:"C"(正常模式)和" "(本地模式)。
比如切换为本地模式:
setlocale(LC_ALL, " ");//切换到本地环境
宽字符的打印
宽字符的字⾯量必须加上前缀“L”,否则C语⾔会把字⾯量当作窄字符类型处理。前缀“L”在单引
号前⾯,表⽰宽字符,对应 wprintf() 的占位符为 %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'特'; wchar_t ch4 = L'★'; printf("%c%c\n", 'a', 'b'); wprintf(L"%lc\n", ch1); wprintf(L"%lc\n", ch2); wprintf(L"%lc\n", ch3); wprintf(L"%lc\n", ch4); return 0; }
地图坐标
我们假设实现⼀个棋盘27⾏,58列的棋盘(⾏和列可以根据⾃⼰的情况修改),再围绕地图画出墙
蛇⾝和⻝物
初始化状态,假设蛇的⻓度是5,蛇⾝的每个节点是●,在固定的⼀个坐标处,⽐如(24,5)处开始出现
蛇,连续5个节点。
注意:蛇的每个节点的x坐标必须是2个倍数,否则可能会出现蛇的⼀个节点有⼀半⼉出现在墙体中,另外⼀般在墙外的现象,坐标不好对⻬。
关于⻝物,就是在墙体内随机⽣成⼀个坐标(x坐标必须是2的倍数),坐标不能和蛇的⾝体重合,然后打印★。
数据结构设计
在游戏运⾏的过程中,蛇每次吃⼀个⻝物,蛇的⾝体就会变⻓⼀节,如果我们使⽤链表存储蛇的信
息,那么蛇的每⼀节其实就是链表的每个节点。每个节点只要记录好蛇⾝节点在地图上的坐标就⾏,
所以蛇节点结构如下:
typedef struct SnakeNode { int x; int y; struct SnakeNode* next; }SnakeNode, * pSnakeNode;
封装snake结构维护贪吃蛇
typedef struct Snake { pSnakeNode _pSnake;//维护整条蛇的指针 pSnakeNode _pFood;//维护⻝物的指针 enum DIRECTION _Dir;//蛇头的⽅向,默认是向右 enum GAME_STATUS _Status;//游戏状态 int _Socre;//游戏当前获得分数 int _foodWeight;//默认每个⻝物10分 int _SleepTime;//每⾛⼀步休眠时间 }Snake, * pSnake;
蛇的方向
enum DIRECTION { UP = 1, DOWN, LEFT, RIGHT };
游戏状态
/游戏状态 enum GAME_STATUS { OK,//正常运⾏ KILL_BY_WALL,//撞墙 KILL_BY_SELF,//咬到⾃⼰ END_NOMAL//正常结束 };
游戏设计流程
核⼼逻辑实现分析
游戏主逻辑
设置程序⽀持本地模式,然后进⼊游戏的主逻辑
# define _CRT_SECURE_NO_WARNINGS #include <locale.h> void test() { int ch = 0; srand((unsigned int)time(NULL));//生成随机数 do { Snake snake = { 0 };//创建结构体 GameStart(&snake); GameRun(&snake); GameEnd(&snake); SetPos(20, 15);//设定光标位置 printf("再来⼀局吗?(Y/N):"); ch = getchar();//接收用户的输入值 getchar();//清理屏幕 } while (ch == 'Y'); SetPos(0, 27);//重新设定光标位置 } int main() { //修改当前地区为本地模式,为了⽀持中⽂宽字符的打印 setlocale(LC_ALL, ""); //测试逻辑 test(); return 0; }
游戏开始(GameStart)
这个模块完成游戏的初始化任务:
• 控制台窗⼝⼤⼩的设置
• 控制台窗⼝名字的设置
• ⿏标光标的隐藏
• 打印欢迎界⾯
• 创建地图
• 初始化第蛇
• 创建第⼀个⻝物
# define _CRT_DEFINE_NO_WARNINGS void GameStart(pSnake ps) { //设置控制台窗⼝的⼤⼩,30⾏,100列 //mode 为DOS命令 system("mode con cols=100 lines=30"); //设置cmd窗⼝名称 system("title 贪吃蛇"); //获取标准输出的句柄(⽤来标识不同设备的数值) HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE); //影藏光标操作 CONSOLE_CURSOR_INFO CursorInfo; GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息 CursorInfo.bVisible = false; //隐藏控制台光标 SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态 //打印欢迎界⾯ WelcomeToGame(); //打印地图 CreateMap(); //初始化蛇 InitSnake(ps); //创造第⼀个⻝物 CreateFood(ps); }
打印欢迎界⾯
void WelcomeToGame() { SetPos(40, 15); printf("欢迎来到贪吃蛇⼩游戏"); SetPos(40, 25);//让按任意键继续的出现的位置好看点 system("pause"); system("cls"); SetPos(25, 12); printf("⽤ ↑ . ↓ . ← . → 分别控制蛇的移动, F3为加速,F4为减速\n"); SetPos(25, 13); printf("加速将能得到更⾼的分数。\n"); SetPos(40, 25);//让按任意键继续的出现的位置好看点 system("pause"); system("cls"); }
创建地图
#define WALL L'□'
初始化蛇⾝
void InitSnake(pSnake ps) { pSnakeNode cur = NULL; int i = 0; //创建蛇⾝节点,并初始化坐标 //头插法 for (i = 0; i < 5; i++) { //创建蛇⾝的节点 cur = (pSnakeNode)malloc(sizeof(SnakeNode)); if (cur == NULL) { perror("InitSnake()::malloc()"); return; } //设置坐标 cur->next = NULL; cur->x = POS_X + i * 2; cur->y = POS_Y; //头插法 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->_SleepTime = 200; ps->_Socre = 0; ps->_Status = OK; ps->_Dir = RIGHT; ps->_foodWeight = 10; }
创建第⼀个⻝物
先随机⽣成⻝物的坐标
◦ x坐标必须是2的倍数
◦ ⻝物的坐标不能和蛇⾝每个节点的坐标重复
• 创建⻝物节点,打印⻝物
#define FOOD L'★'
void CreateFood(pSnake ps) { int x = 0; int y = 0; again: //产⽣的x坐标应该是2的倍数,这样才可能和蛇头坐标对⻬。 do { x = rand() % 53 + 2; y = rand() % 25 + 1; } while (x % 2 != 0); pSnakeNode cur = ps->_pSnake;//获取指向蛇头的指针 //⻝物不能和蛇⾝冲突 while (cur) { if (cur->x == x && cur->y == y) { goto again; } cur = cur->next; } pSnakeNode pFood = (pSnakeNode)malloc(sizeof(SnakeNode)); //创建⻝物 if (pFood == NULL) { perror("CreateFood::malloc()"); return; else { pFood->x = x; pFood->y = y; SetPos(pFood->x, pFood->y); wprintf(L"%c", FOOD); ps->_pFood = pFood; } }
游戏运⾏
KEY_PRESS
检测按键状态,我们封装了⼀个宏
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK)&0x1) ? 1 : 0)
PrintHelpInfo
void PrintHelpInfo() { //打印提⽰信息 SetPos(64, 15); printf("不能穿墙,不能咬到⾃⼰\n"); SetPos(64, 16); printf("⽤↑.↓.←.→分别控制蛇的移动."); SetPos(64, 17); printf("F3 为加速,F4 为减速\n"); SetPos(64, 18); printf("ESC :退出游戏.space:暂停游戏."); }
蛇⾝移动(SnakeMove)
void SnakeMove(pSnake ps) { //创建下⼀个节点 pSnakeNode pNextNode = (pSnakeNode)malloc(sizeof(SnakeNode)); if (pNextNode == NULL) { perror("SnakeMove()::malloc()"); return; } //确定下⼀个节点的坐标,下⼀个节点的坐标根据,蛇头的坐标和⽅向确定 switch (ps->_Dir) { case UP: { pNextNode->x = ps->_pSnake->x; pNextNode->y = ps->_pSnake->y - 1; } break; case DOWN: { pNextNode->x = ps->_pSnake->x; pNextNode->y = ps->_pSnake->y + 1; } break; case LEFT: { pNextNode->x = ps->_pSnake->x - 2; pNextNode->y = ps->_pSnake->y; } break; } //如果下⼀个位置就是⻝物 if (NextIsFood(pNextNode, ps)) { EatFood(pNextNode, ps); } else//如果没有⻝物 { NoFood(pNextNode, ps); } KillByWall(ps); KillBySelf(ps); }