目录
知识点:
Win32 API
宽字符的打印
控制台操作:
(1)调整控制台大小
(2)控制台屏幕上的坐标COORD
GetStdHandle
GetConsoleCursorInfo
CONSOLE_CURSOR_INFO
SetConsoleCursorInfo
SetConsoleCursorPositio
SetPos:
GetAsyncKeyState
游戏实现
GameStart
WelcomeToGame
CreateMap
InitSnake
CreateFood
结语:
背景:随着c语言的学习,我们已经可以用c语言来完成一些小项目,贪吃蛇项目是对C语言语法做⼀个基本的巩固,帮助大家查缺补漏。
整个项目设计流程:
设计要求:
(1)贪吃蛇地图绘制
(2)蛇吃食物的功能 (上、下、左、右⽅向键控制蛇的动作)
(3)蛇撞墙死亡
(4)蛇撞自身死亡
(5)计算得分
(6)蛇身加速、减速
(7)暂停游戏
C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等。
前面几个大家都不陌生,为了完成贪吃蛇下面我带大家简要的了解一下Win32 API。下面设计API中的函数大家不必了解它是怎么写的,只要掌握怎么用即可。
Win32 API即为Microsoft 32位平台的应用程序编程接口(Application Programming Interface)。所有在Win32平台上运行的应用程序都可以调用这些函数。(简单来说就是win提供的函数)。
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
在要打印的宽字符前加上L,wprintf(L"%c", WALL);,用wprintf打印,这里用宏定义只是为了下面方便。例如:
贪吃蛇是在屏幕上显示的故我们要控制控制台的大小及一些基本操作。
我们可以在编译器中用system("mode con cols=100 lines=10");语句改变控制台大小。
cols是列,lines是行。
COORD是windowsAPI中定义的⼀个结构体,表示⼀个字符在控制台屏幕上的坐标。
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
用法如下:COORD pos = { 10, 15 };
GetStdHandle 返回的句柄可供需要在控制台中进行读取或写入的应用程序使用。它是一个windows提供的函数。用法如下:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
检索有关指定控制台屏幕缓冲区的游标大小和可见性的信息。用法如下:
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
这个结构体,包含有关控制台光标的信息,包含成员如下
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, * PCONSOLE_CURSOR_INFO;
dwSize:由光标填充的字符单元格的百分比。此值介于1到100之间。光标外观会变化,范围从全填充单元格到单元底部的水平线条。
bVisible:游标的可见性。 如果光标可见,则此成员为TRUE。
设置指定控制台屏幕缓冲区的光标的大小和可见性。
该函数有两个参数,左边是传入GetStdHandle获得的手柄,右边是CONSOLE_CURSOR_INFO控制台光标的指针。
用法如下:
int main()
{
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//影藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
}
效果如下:
我们可以看到光标不见了。
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
用法如下:
int main()
{
COORD pos = { 10, 5 };
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
char ch = getchar();
}
效果如下:
与前面几个函数不同,这个函数由于用的经常且系统没有提供故要自己实现。
实现如下:
先获得输出句柄再进行操作。
//设置光标的坐标
void SetPos(short x, short y)
{
COORD pos = { x, y };
HANDLE hOutput = NULL;
//获取标准输出的句柄(用来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
}
用法:
int main()
{
SetPos(10, 5);
char ch = getchar();
}
效果:
获取按键情况。
如果函数成功,则返回值指定自上次调用 GetAsyncKeyState 以来是否按下了该键,以及该键当前是启动还是关闭。如果设置了最高有效位,则密钥关闭,如果设置了最低有效位,则在上次调用 GetAsyncKeyState 后按下该密钥。
可以看到只需看最低为即可,如果最低为为1表示按键被按过,0表示没被按过。
我们可以用这样的写法来简便(封装)&1判断最低为是否为1.
#define KEY_PRESS(VK) ( (GetAsyncKeyState(VK) & 0x1) ? 1 : 0 )
由于代码较多故分4个模块来讲分别是1. 游戏开始 - 初始化游戏GameStart,2. 游戏运行 - 游戏的正常运行过程GameRun,3. 游戏结束 - 游戏善后(释放资源)GameEnd,4.test-把上面三个模块调用,并实现多次输入。
代码如下:
GameStart里面我们要做好游戏的开始工作,先初始化控制台窗口大小和隐藏屏幕光标,打印欢迎界面,创建地图,初始化蛇身,创建食物用函数封装。
void GameStart(pSnake ps)
{
//控制台窗口设置
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//隐藏屏幕光标
HANDLE hOutput = NULL;
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false;//光标隐藏
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
//打印欢迎界面
WelcomeToGame();
//创建地图
CreateMap();
//初始化蛇身
InitSnake(ps);
//创建食物
CreateFood(ps);
}
打印游戏开始界面,用SetPos设置控制台位置。下面的system("pause");,system("cls");,pause是暂停程序,cls是清楚控制台。可以就可以有两重界面。
void WelcomeToGame()
{
SetPos(40, 14);
printf("欢迎来到贪吃蛇小游戏\n");
SetPos(40, 25);
system("pause");
system("cls");
SetPos(20, 14);
printf("使用 ↑ . ↓ . ← . → . 分别控制蛇的移动, A是加速,B是减速");
SetPos(40, 25);
system("pause");
system("cls");
//char ch = getchar();
}
效果如下:
随便按下一个键。
打印地图,由于要使用到宽字符,故要用wprintf来打印,%c前面要加一个L。
void CreateMap()
{
SetPos(0, 0);
int i = 0;
for (i = 0; i <= 56; i += 2)
{
wprintf(L"%c", WALL);
}
SetPos(0, 26);
for(i=0;i<=56;i+=2)
{
wprintf(L"%c", WALL);
}
for (i = 1; i <= 25; i++)
{
SetPos(0, i);
wprintf(L"%c", WALL);
}
for (i = 1; i <= 25; i++)
{
SetPos(56, i);
wprintf(L"%c", WALL);
}
}
初始化蛇,其实整个游戏的过程都是在维护贪吃蛇。贪吃蛇结构体如下:
pSnakeNode _pSnake;//指向贪吃蛇头结点的指针.
pSnakeNode _pFood;//指向食物结点的指针
int _Score;//贪吃蛇累计的总分
int _FoodWeight;//一个食物的分数
int _SleepTime;//每走一步休息的时间,时间越短,速度越快,时间越长,速度越慢
enum DIRECTION _Dir;//描述蛇的方向
enum GAME_STATUS _Status;//游戏的状态:正常、退出、撞墙、吃到自己
下面两个是枚举类型。
enum DIRECTION//方向
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
enum GAME_STATUS//状态
{
OK,
KILL_BY_WALL,
KILL_BY_SELF,
END_NOMAL
};typedef struct Snake//蛇
{
pSnakeNode _pSnake;
pSnakeNode _pFood;
enum DIRECTION _Dir;
enum GAME_STATUS _Status;
int _Score;
int _FoodWeight;
int _SleepTime;
}Snake, * pSnake;
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 0;
}
cur->next = NULL;
cur->x = POS_X + 2 * i;
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"%c", BODY);
cur = cur->next;
}
ps->_Status = OK;
ps->_Score = 0;
ps->_SleepTime = 200;
ps->_pFood = NULL;
ps->_FoodWeight = 10;
ps->_Dir = RIGHT;
}
创建食物,使用rand随机生成一个数至于为什么要模上53.
可以看到我们设置的控制台大小,游戏界面大小为56x26,这里要特别注意的是,大家可以看看自己编译器后面的控制台窗口,x轴和y轴的比例是1:2,也就是说一个宽字符占一个y即可,而一个宽字符要占两个x。故x的坐标必须为偶数,防止食物和蛇身一半在墙里一半在地图里。
故坐标为:
上 | (0,0) | (56,0) |
下 | (0,26) | (56.26) |
左 | (0,1) | (0,25) |
右 | (56,1) | (56,25) |
void CreateFood(pSnake ps)
{
int x = 0;
int y = 0;
again:
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;
}
pFood->x = x;
pFood->y = y;
ps->_pFood = pFood;
SetPos(x, y);
wprintf(L"%c", FOOD);
//getchar();
}
效果如下蛇先不用看后面会讲。
总结:由于代码量较多为了给大家解释清楚贪吃蛇项目分为两篇文章来描述。
其实写博客不仅仅是为了教大家,同时这也有利于我巩固自己的知识点,和一个学习的总结,由于作者水平有限,对文章有任何问题的还请指出,接受大家的批评,让我改进,如果大家有所收获的话还请不要吝啬你们的点赞收藏和关注,这可以激励我写出更加优秀的文章。