前言
嗨,我是firdawn,在本章中我们将介绍,贪吃蛇背景,游戏效果,Win32API和贪吃蛇游戏的设计与分析以及代码实现。下面是本章的思维导图,接下来,让我们开始今天的学习吧!
一,游戏背景
二,游戏效果展示
recording-2024-05-26-08-53-02
三,技术要点
实现贪吃蛇需要的技术包括C语⾔函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32API等。
四,Win32API介绍
本次实现贪吃蛇会使⽤到的⼀些Win32API知识,接下来我们就学习⼀下:
Windows这个多作业系统除了协调应⽤程序的执⾏、分配内存、管理资源之外,它同时也是⼀个很⼤的服务中⼼,调⽤这个服务中⼼的各种服务(每⼀种服务就是⼀个函数),可以帮应⽤程序达到开启视窗、描绘图形、使⽤周边设备等⽬的,由于这些函数服务的对象是应⽤程(Application),所以便称之为ApplicationProgrammingInterface,简称API函数。WIN32API也就是MicrosoftWindows32位平台的应⽤程序编程接⼝。
4.1 mode命令
我们平时使用的cmd窗口其实就是控制台程序,如果我们想设置他的行和列,可以使用mode命令。
c
mode con cols=100 lines=30
这时候控制台就是30行100列的了
4.2 title命令
如果我们想修改控制台的名字,可以使用title命令。
c
title 贪吃蛇
这时候控制台的名字就是贪吃蛇。
4.3 控制台屏幕上的坐标COORD
COORD是一个结构体,在系统中被实现,用来接收控制台上的光标坐标信息。
1.参数:它的参数有两个,分别为x坐标和y坐标。
4.4 GetStdHandle
GetStdHandle用来接收标准设备(包括标准输入设备键盘,标准输出设备屏幕,标准错误设备屏幕)的句柄,也就是标识符,相当于一个把手一样,方便我们找到标准设备进行控制。
c
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
上面的代码中,houtput存储了标准输出设备(屏幕)的句柄。
4.5 GetConsoleCursorInfo
GetConsoleCursorInfo用于获取控制台光标大小和可见性的信息,并且将获取到的信息存储CONSOLE_CURSOR_INFO结构的指针指向的对象中。
c
CONSOLE_CURSOR_INFO cursor_info = { 0 };
GetConsoleCursorInfo(houtput, &cursor_info);
这里我们将获取到的控制台光标信息存储到CONSOLE_CURSOR_INFO结构体中。
4.6 CONSOLE_CURSOR_INFO
CONSOLE_CURSOR_INFO用来存储控制台游标的信息,包括光标大小和可见性。
4.7 SetConsoleCursorInfo
SetConsoleCursorInfo用于为指定的控制台屏幕缓冲区设置光标的大小和可见性。
c
CONSOLE_CURSOR_INFO cursor_info = { 0 };
GetConsoleCursorInfo(houtput, &cursor_info);
//cursor_info.bVisible = false;
cursor_info.dwSize = 50;
SetConsoleCursorInfo(houtput, &cursor_info);
上面的代码设置了光标的高度占比为总高度的百分之50。
4.8 SetConsoleCursorPosition
SetConsoleCursorPosition用于设置指定控制台屏幕缓冲区中的光标位置。
c
COORD pos = { 10, 15 };
SetConsoleCursorPosition(houtput, pos);
上面的代码设置了控制台光标的位置在x = 10,y = 15的位置,设置光标位置在贪吃蛇的代码实现中,我们需要经常用到,既然我需要反复用到,那我们就可以封装为一个函数,方便我们后续使用。如下图:
c
void SetCursorPosition(int x, int y)
{
HANDLE houtput = NULL;
houtput = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x,y };
SetConsoleCursorPosition(houtput, pos);
}
4.9 GetAsyncKeyState
GetAsyncKeyState用于获取获取按键情况。
- 它的参数为键盘上按键对应的虚拟键码
- 返回值为short类型,是两个字节,返回值的最高位表示按键当前的状态,1表示按下,0表示抬起;返回值的最低位表示按键被按过了没有,1表示被按过了,0表示没有被按过。
参考:虚拟键码表
在贪吃蛇游戏中,我们需要的是检查一个按键是否被按过,来控制贪吃蛇的移动,因此,我们可以写一个宏来检测按键是否被按过。
c
#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 1)//检查按键是否被按过
上面的宏中,值为1代表按键被按过,0代表按键没有被按过。
五,贪吃蛇游戏设计与分析
5.1 地图
地图包括两大部分,分别是贪吃蛇游戏的引导界面和墙壁的打印。
5.1.1 引导界面1:欢迎界面
5.1.2 引导界面2:游戏玩法介绍
5.2.1 墙壁的打印
5.2 蛇身和食物
蛇身默认开始是5个节点,每个节点用●表示,食物随机出现在屏幕内,用表示★食物。
5.3 数据结构设计
贪吃蛇是用链表来维护的,贪吃蛇由节点组成,一个贪吃蛇节点记录了它的x坐标和y坐标,我们用一个头指针指向贪吃蛇身体节点,然而贪吃蛇还有方向,速度,得分,死亡状态等信息,所以我们可以定义一个贪吃蛇的结构体来管理这个贪吃蛇,贪吃蛇结构体中包含了指向贪吃蛇身体的头结点指针,贪吃蛇的方向,游戏状态,贪吃蛇得分,贪吃蛇的速度,因为每个食物只包含了自己的位置信息,和食物权重,过于简单,所以我们可以将食物的节点指针和生物权重也包含进贪吃蛇结构体中。
5.4 游戏逻辑设计
5.4.1 游戏运行主函数
在游戏运行主函数中,游戏模块由三大部分组成,首先是创建一个贪吃蛇结构体,然后,游戏运行分为游戏开始,游戏运行,游戏结束三部分。
- 在游戏开始函数中,我们会继续贪吃蛇欢迎界面和功能介绍界面的绘制,然后就是地图的打印和贪吃蛇信息的初始化。
- 在游戏运行函数中,我们会绘制帮助信息,编写贪吃蛇爬行功能,检测贪吃蛇状态。
- 在游戏结束函数中,我们会进行贪吃蛇的收尾工作,根据贪吃蛇的状态,显示游戏结束的原因,将贪吃蛇中动态申请的内存释放掉。
六,核心逻辑实现分析
我们会在test.c中实现游戏的主逻辑,在Snake.c中进行贪吃蛇节点和贪吃蛇结构的实现,以及贪吃蛇中需要用到的函数的声明,在Snake.c中我们进行贪吃蛇功能函数的实现。
test.c
c
#define _CRT_SECURE_NO_WARNINGS
#include "Snake.h"
//进行贪吃蛇逻辑的实现
void test()
{
setlocale(LC_ALL, "");//设置本地环境,支持宽字符的打印
srand((unsigned int)time(NULL));
char input = 0;
do {
system("cls");
//创建蛇
pSnake ps = (pSnake)malloc(sizeof(Snake));
if (ps == NULL)
{
perror("test()::malloc");
exit(-1);
}
ps->_pFood = NULL;
ps->_pSnake = NULL;
//游戏开始
//1.设置窗口大小,名字
//2.隐藏光标
//3.打印欢迎界面
//4.打印地图
//5.初始化蛇身
//6.创建食物
//7.设置基础信息
Game_Start(ps);
//游戏运行
Game_Run(ps);
//游戏结束
Game_End(ps);
SetPos(20, 12);
printf("再来一局吗,输入y/n :>");
while (1)
{
if (isalnum(input = getchar()))
{
break;
}
}
while (getchar() != '\n');
} while (input=='Y'||input=='y');
SetPos(0, 26);
}
int main()
{
test();
return 0;
}
Snake.c
c
#define _CRT_SECURE_NO_WARNINGS
#include "Snake.h"
//进行贪吃蛇功能的实现
//设置光标位置
void SetPos(int x, int y)
{
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x,y };
SetConsoleCursorPosition(houtput, pos);
}
//创建墙体
void CreatWall()
{
int i = 0;
//上
SetPos(0, 0);
for (i = 0; i < 58; i+=2)
{
wprintf(L"%c", WALL);
}
//下
SetPos(0, 26);
for (i = 0; i < 58; 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);
}
SetPos(35, 28);
}
//打印欢迎界面
void WelcomeToGame()
{
SetPos(40, 15);
printf("欢迎来到贪吃蛇游戏");
SetPos(40, 25);
system("pause");
system("cls");
SetPos(27, 15);
printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动,F3加速,F4减速");
SetPos(40, 25);
system("pause");
system("cls");
}
//初始化蛇
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");
exit(-1);
}
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;
}
}
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
}
//创建食物
void CreatFood(pSnake ps)
{
if (ps->_pFood == NULL)
{
pSnakeNode pfood = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pfood == NULL)
{
perror("CreatFood()::malloc");
exit(-1);
}
ps->_pFood = pfood;
ps->_pFood->next = NULL;
}
int x = 0;
int y = 0;
//1.食物在地图内
//2.食物的x坐标为2的倍数
//3.食物的坐标和蛇不重合
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;
}
SetPos(x, y);
wprintf(L"%lc", FOOD);
ps->_pFood->x = x;
ps->_pFood->y = y;
}
//游戏开始
void Game_Start(pSnake ps)
{
//1.设置窗口大小,名字
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
//2.隐藏光标
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursor_info;
GetConsoleCursorInfo(houtput, &cursor_info);
cursor_info.bVisible = false;
SetConsoleCursorInfo(houtput, &cursor_info);
//3.打印欢迎界面和功能介绍
WelcomeToGame();
//4.打印地图
CreatWall();
//5.初始化蛇身
InitSnake(ps);
//6.创建食物
CreatFood(ps);
//7.设置基础信息
ps->_Dir = RIGHT;//默认方向向右
ps->_FoodWeight = 10;
ps->_Score = 0;
ps->_Status = OK;
ps->_SleepTime = 200;//走一次停200毫秒
}
//按空格键暂停
void Pause()
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
//有食物,就吃食物
void EatFood(pSnake ps, pSnakeNode pn)
{
//让下一个节点指向蛇头
pn->next = ps->_pSnake;
ps->_pSnake = pn;
ps->_Score += ps->_FoodWeight;
//打印蛇
SetPos(ps->_pSnake->x, ps->_pSnake->y);
wprintf(L"%lc", BODY);
//重新生成食物
CreatFood(ps);
}
//没有食物
void NoFood(pSnake ps, pSnakeNode pn)
{
//让下一个节点指向蛇头
pn->next = ps->_pSnake;
ps->_pSnake = pn;
pSnakeNode cur = ps->_pSnake;
//打印蛇
SetPos(ps->_pSnake->x, ps->_pSnake->y);
wprintf(L"%lc", BODY);
while (cur->next->next != NULL)
{
cur = cur->next;
}
SetPos(cur->next->x, cur->next->y);
printf(" ");//清理最后一个节点的打印
free(cur->next);
cur->next = NULL;
}
//判断是否撞到自己
int KillBySelf(pSnake ps, pSnakeNode pn)
{
pSnakeNode cur = ps->_pSnake->next;
while (cur)
{
if (cur->x == pn->x && cur->y == pn->y)
{
return 1;
}
cur = cur->next;
}
return 0;
}
//蛇的移动
void SnakeMove(pSnake ps)
{
pSnakeNode pn = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pn == NULL)
{
perror("Game_Run()::malloc");
exit(-1);
}
switch (ps->_Dir)
{
case UP:
pn->x = ps->_pSnake->x;
pn->y = ps->_pSnake->y - 1;
break;
case DOWN:
pn->x = ps->_pSnake->x;
pn->y = ps->_pSnake->y + 1;
break;
case LEFT:
pn->x = ps->_pSnake->x-2;
pn->y = ps->_pSnake->y;
break;
case RIGHT:
pn->x = ps->_pSnake->x + 2;
pn->y = ps->_pSnake->y;
break;
}
//判断下一位置是否为食物
if (pn->x == ps->_pFood->x && pn->y == ps->_pFood->y)
{
EatFood(ps,pn);
}
else
{
NoFood(ps,pn);
}
//判断是否撞墙
if (pn->x == 0 || pn->x == 56 || pn->y == 0 || pn->y == 26)
{
ps->_Status = KILLED_BY_WALL;
}
//判断是否撞到自己
if (KillBySelf(ps, pn))
{
ps->_Status = KILLED_BY_SELF;
}
}
//打印帮助信息
void PrintHelpInfo()
{
SetPos(60, 8);
printf("不能穿墙,不能咬到自己");
SetPos(60, 9);
printf("用 ↑ . ↓ . ← . → 分别控制蛇的移动");
SetPos(60, 10);
printf("F3加速,F4减速");
}
//游戏运行
void Game_Run(pSnake ps)
{
PrintHelpInfo();//打印帮助信息
do {
SetPos(60, 13);
printf("总得分:%-2d 单个食物得分:%-2d", ps->_Score, ps->_FoodWeight);
SetPos(60, 20);
printf("本游戏由@firdawn制作");
if (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_SPACE))//空格键
{
Pause();
}
else if (KEY_PRESS(VK_ESCAPE))//ESC键
{
ps->_Status = END_NORMAL;
}
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;
}
}
SnakeMove(ps);
Sleep(ps->_SleepTime);
} while (ps->_Status == OK);
}
//游戏结束
void Game_End(pSnake ps)
{
SetPos(20, 10);
if (ps->_Status == KILLED_BY_SELF)
{
printf("您咬到了自己,游戏结束!");
}
else if (ps->_Status == KILLED_BY_WALL)
{
printf("您撞到了墙上,游戏结束!");
}
else if (ps->_Status == END_NORMAL)
{
printf("您主动退出了游戏,游戏结束!");
}
pSnakeNode cur = ps->_pSnake;
while (cur)
{
pSnakeNode next = cur->next;
free(cur);//释放贪吃蛇节点
cur = next;
}
free(ps->_pFood);//释放食物节点
free(ps);//释放贪吃蛇结构
}
Snake.h
c
#pragma once
#include <stdio.h>
#include<stdlib.h>
#include <Windows.h>
#include <locale.h>
#include <stdbool.h>
#include <time.h>
#include <ctype.h>
#define KEY_PRESS(VK) (GetAsyncKeyState(VK) & 1 ? 1 : 0)
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
#define POS_X 24
#define POS_Y 5
enum DIRECTION//方向
{
UP,
DOWN,
LEFT,
RIGHT
};
enum GAME_STATUS
{
OK,//状态正常
KILLED_BY_WALL,//撞墙
KILLED_BY_SELF,//咬到自己
END_NORMAL//正常结束
};
typedef struct SnakeNode
{
int x;
int y;
struct SnakeNode* next;
}SnakeNode, *pSnakeNode;
typedef struct Snake
{
pSnakeNode _pSnake;//维护蛇的指针
pSnakeNode _pFood;//维护食物的指针
enum DIRECTION _Dir;//蛇头的方向,默认向右
enum GAME_STATUS _Status;//游戏状态
int _Score;//游戏得分
int _FoodWeight;//每个食物的权重,默认为10
int _SleepTime;//每走一步的休眠时间
}Snake,*pSnake;
//设置光标位置
void SetPos(int x, int y);
//创建墙体
void CreatWall();
//打印欢迎界面
void WelcomeToGame();
//初始化蛇
void InitSnake(pSnake ps);
//创建食物
void CreatFood(ps);
//游戏开始
void Game_Start(pSnake ps);
//有食物,就吃食物
void EatFood(pSnake ps, pSnakeNode pn);
//没有食物
void NoFood(pSnake ps, pSnakeNode pn);
//判断是否撞到自己
int KillBySelf(pSnake ps, pSnakeNode pn);
//蛇的移动
void SnakeMove(pSnake ps);
//游戏运行
void Game_Run(pSnake ps);
//游戏结束
void Game_End(pSnake ps);