这篇博客会是对学习C语言成果的检测,为了实现贪吃蛇小游戏,我们用到的“工具”有:C语言函数、枚举、结构体、动态内存管理、预处理指令、链表、Win32 API等。
目录
1.简易版游戏效果
关于本人实现的贪吃蛇 ,有四个展示页面:
1.1欢迎界面
1.2游戏规则提示页面
1.3游戏进行页面
1.4游戏结束页面
2.游戏需要实现的内容
- 贪吃蛇地图绘制
- 蛇吃食物的功能(上、下、左、右⽅向键控制蛇的动作)
- 蛇撞墙死亡
- 蛇撞自身死亡
- 计算得分
- 蛇身加速、减速
- 暂停游戏
实现环境:Windows环境的控制台
3.Win32 API介绍
3.1Win32 API
简单来说就是包含在一个附加名为DLL的动态连接库文件中的一类函数。
Windows 这个多作业系统除了协调应用程序的执行、分配内存、管理资源之外, 它同时也是一个很大的服务中心,调用这个服务中心的各种服务(每一种服务就是一个函数),可以帮应用程序达到开启视窗、描绘图形、使用周边设备等目的,由于这些函数服务的对象是应用程序(Application),所以便称之为 Application Programming Interface,简称 API 函数。WIN32 API也就是Microsoft Windows 32位平台的应用程序编程接口。
3.2控制台程序
平常我们运行起来的黑框程序其实就是控制台程序
我们可以使用cmd命令中的mode命令来设置控制台窗口的长宽:
设置控制台窗口的大小,30行,100列
mode con cols=100 lines=30
使用title命令设置控制台窗口名字: 贪吃蛇
title 贪吃蛇
如果在修改过程中发现无法修改窗口大小,此时需要将控制台程序的默认终端应用程序改为“让Windows决定”,如果还是无法修改(有些电脑无法做到,比如我的),则再将其改为Windows控制台主机
3.3控制台屏幕上的坐标COORD
COORD 是Windows API中定义的一个结构体,表示一个字符在控制台屏幕幕缓冲区上的坐标,坐标系 (0,0) 的原点位于缓冲区的顶部左侧单元格。 横着向右是x轴,竖着向下是y轴。
以下是COORD类型结构体的声明:
typedef struct _COORD {
SHORT X;
SHORT Y;
} COORD, *PCOORD;
3.4游戏实现是可能用到的Win32 API 函数
3.4.1GetStdHandle
GetStdHandle是一个Windows API函数。它用于从一个特定的标准设备(标准输入、标准输出或标准错误)中取得一个句柄(用来标识不同设备的数值),使用这个句柄可以操作设备。
可以理解为我们通过这个函数获取对标准输入、标准输出或标准错误的控制权,这个权杖就是存放函数返回值的指针。
3.4.1.1函数格式
HANDLE GetStdHandle(DWORD nStdHandle);
3.4.1.2使用样例
例如想要获取对标准输出的控制权。
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
3.4.2GetConsoleCursorInfo
检索有关指定控制台屏幕缓冲区的光标大小和可见性的信息
3.4.2.1函数格式
BOOL WINAPI GetConsoleCursorInfo(
HANDLE hConsoleOutput,
PCONSOLE_CURSOR_INFO lpConsoleCursorInfo
);
3.4.2.1.1 CONSOLE_CURSOR_INFO
包含有关控制台光标的信息的结构体
typedef struct _CONSOLE_CURSOR_INFO {
DWORD dwSize;
BOOL bVisible;
} CONSOLE_CURSOR_INFO, *PCONSOLE_CURSOR_INFO;
3.4.2.2使用样例
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
3.4.3SetConsoleCursorInfo
设置指定控制台屏幕缓冲区的光标的大小和可见性
3.4.3.1函数格式
BOOL WINAPI SetConsoleCursorInfo(
HANDLE hConsoleOutput,
const CONSOLE_CURSOR_INFO *lpConsoleCursorInfo
);
3.4.3.2使用样例
HANDLE hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//影藏光标操作
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(hOutput, &CursorInfo);//获取控制台光标信息
CursorInfo.bVisible = false; //隐藏控制台光标
SetConsoleCursorInfo(hOutput, &CursorInfo);//设置控制台光标状态
3.4.4SetConsoleCursorPosition
设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调用SetConsoleCursorPosition函数将光标位置设置到指定的位置。
3.4.4.1函数格式
BOOL WINAPI SetConsoleCursorPosition(
HANDLE hConsoleOutput,
COORD pos
);
3.4.4.2使用样例
COORD pos = { 10, 5};
HANDLE hOutput = NULL;
//获取标准输出的句柄(⽤来标识不同设备的数值)
hOutput = GetStdHandle(STD_OUTPUT_HANDLE);
//设置标准输出上光标的位置为pos
SetConsoleCursorPosition(hOutput, pos);
3.4.5GetAsyncKeyState
获取按键情况
GetAsyncKeyState的返回值是short类型,在上一次调用 GetAsyncKeyState 函数后,如果返回的16位的short数据中:
- 如果最高位是1,说明按键的状态是按下
- 如果最高是0,说明按键的状态是抬起
- 如果最低位被置为1,则说明该按键被按过,反之为0
3.4.5.1函数格式
SHORT GetAsyncKeyState(int vKey);
vKey是虚拟密钥代码(下面为链接)
https://learn.microsoft.com/zh-cn/windows/win32/inputdev/virtual-key-codes
展示本游戏要用到的虚拟密钥代码 :
VK_SPACE | 0x20 | 空格键 |
VK_F3 | 0x72 | F3 键 |
VK_F4 | 0x73 | F4 键 |
VK_ESCAPE | 0x1B | ESC 键 |
VK_LEFT | 0x25 | LEFT ARROW 键 |
VK_UP | 0x26 | UP ARROW 键 |
VK_RIGHT | 0x27 | RIGHT ARROW 键 |
VK_DOWN | 0x28 | DOWN ARROW 键 |
3.4.5.2使用样例
#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");
}
else if (KEY_PRESS(0x31))
{
printf("1\n");
}
else if (KEY_PRESS(0x32))
{
printf("2\n");
}
else if (KEY_PRESS(0x33))
{
printf("3\n");
}
else if (KEY_PRESS(0x34))
{
printf("4\n");
}
else if (KEY_PRESS(0x35))
{
printf("5\n");
}
}
return 0;
}
3.5其余相关知识点
3.5.1<locale.h>本地化
<locale.h>提供的函数用于控制C标准库中对于不同的地区会产生不一样行为的部分。
在标准中,依赖地区的部分有以下几项:
• 数字量的格式
• 货币量的格式
• 字符集
• 日期和时间的表示形式
3.5.1.1类项
通过修改地区,程序可以改变它的行为来适应世界的不同区域。
但地区的改变可能会影响库的许多部分,其中一部分可能是我们不希望修改的。所以C语言支持针对不同的类项进行修改,下面的一个宏, 指定一个类项:
- LC_COLLATE:影响字符串比较函数 strcoll() 和 strxfrm() 。
- LC_CTYPE:影响字符处理函数的行为。
- LC_MONETARY:影响货币格式。
- LC_NUMERIC:影响 printf() 的数字格式。
- LC_TIME:影响时间格式 strftime() 和 wcsftime() 。
- LC_ALL:针对所有类项修改,将以上所有类别设置为给定的语言环境。
3.5.1.2setlocale函数
3.5.1.2.1函数格式
char* setlocale (int category, const char* locale);
setlocale函数用于修改当前地区,可以针对⼀个类项修改,也可以针对所有类项。 setlocale的第一个参数可以是前面说明的类项中的一个,那么每次只会影响一个类项,如果第一个参数是LC_ALL,就会影响所有的类项。 C标准给第二个参数仅定义了2种可能取值:"C"(正常模式)和" "(本地模式)。
3.5.1.2.2使用样例
切换到我们的本地模式后就支持宽字符(汉字)的输出
setlocale(LC_ALL, " ");//切换到本地环境
3.5.2宽字符的打印
过去C语言并不适合非英语国家(地区)使用。 C语言最初假定字符都是单字节的。但是这些假定并不是在世界的任何地方都适用。为了使C语言适应国际化,C语言的标准中不断加入了国际化的支持。比如:加入了宽字符的类型 wchar_t 和宽字符的输入和输出函数,加入了头文件,其中提供了允许程序员针对特定地区(通常是国家或者说某种特定语言的地理区域)调整程序行为的函数。
宽字符的字面量必须加上前缀“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'★';
wprintf(L"%lc\n", ch1);
wprintf(L"%lc\n", ch2);
return 0;
}
4.游戏流程设置
程序开始就设置程序支持本地模式,
然后进入游戏的主逻辑。 主逻辑分为3个过程:
- 游戏开始:完成游戏的初始化
- 游戏运行:完成游戏运行逻辑的实现
- 游戏结束:完成游戏结束的说明,实现资源释放
5.游戏实现代码
5.1 Snake.h
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include"tool.h"
#include"locale.h"
#include<Windows.h>
#include<stdbool.h>
#include"vld.h"
#define ROW 60
#define CON 30
#define SROW 20//蛇的初始列
#define SCON 5//蛇的初始行
#define WALL L'囧'
#define SNAKE L'●'
#define FOOD L'★'
#define KEY_PRESS(vk) (GetAsyncKeyState(vk) & 1)
enum State
{
OK = 1,//正常运行
PAUSE,//暂停
ESC,//正常退出
KILL_BY_SELF,//撞到自己
KILL_BY_WALL//撞墙
};
enum Dir {
UP=1,//上
DOWN,//下
LEFT,//左
RIGHT//右
};
typedef struct SnakeNode {
int x;
int y;//坐标
struct SnakeNode* next;
}SnakeNode,*pSnakeNode;
typedef struct Snake {
pSnakeNode phead;
enum State _state;//蛇状态
int _score;//得分
int food_score;//食物分数
SnakeNode _food;//食物节点
enum Dir dir;//方向
int _sleep;//睡眠时间
}Snake;
//函数声明
void InitGame(Snake* psnake);//初始化游戏
void Welcome();//欢迎界面
void MapInit();//打印地图
void SnakeInit(Snake*snake);//初始化贪吃蛇
void Game(Snake* snake);//游戏运行
void PutFood(Snake* snake);//放置食物
void Pause();//暂停
//释放链表节点
void Relese(pSnakeNode phead);
5.2 Snake.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Snake.h"
#include"tool.h"
//初始化游戏
void InitGame(Snake*psnake)
{
//初始化界面
system("mode con cols=80 lines=40");
system("title 贪吃蛇");
//隐藏光标
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO cursorinfo;
GetConsoleCursorInfo(houtput, &cursorinfo);//获取控制台光标信息
cursorinfo.bVisible = false;
SetConsoleCursorInfo(houtput, &cursorinfo);
//初始化贪吃蛇信息
psnake->dir = RIGHT;
psnake->food_score = 10;
psnake->phead = NULL;
psnake->_score = 0;
psnake->_state = OK;
psnake->_sleep = 200;
}
//欢迎界面
void Welcome()
{
//打印欢迎信息
setpos(30, 19);
printf("欢迎来到贪吃蛇小游戏!\n");
setpos(32, 20);
system("pause");
system("cls");
//打印规则
setpos(16, 19);
printf("用↑ . ↓ . ← . →控制蛇的移动,F3加速,F4减速");
setpos(16, 20);
printf("不同速度得分不同");
setpos(28, 21);
system("pause");
system("cls");
}
//打印地图
void MapInit()
{
int i;
//上
setpos(0, 0);
for (i = 0; i < ROW; i += 2)
{
wprintf(L"%lc", WALL);
}
//下
setpos(0, 29);
for (i = 0; i < ROW; i += 2)
{
wprintf(L"%lc", WALL);
}
//左
for(i=1;i<CON-1;i++)
{
setpos(0, i);
wprintf(L"%lc", WALL);
}
//右
for (i = 1; i < CON - 1; i++)
{
setpos(58, i);
wprintf(L"%lc", WALL);
}
}
//初始化贪吃蛇
void SnakeInit(Snake*snake)
{
int i = 0;
pSnakeNode cur = snake->phead;
for (i = 0; i < 5; i++)
{
cur = (pSnakeNode)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("malloc cur");
return;
}
cur->x = SROW+i*2;
cur->y = SCON;
//头插
cur->next = snake->phead;
snake->phead = cur;
}
PrintNode(snake);
}
//放置食物
void PutFood(Snake* snake)
{
pSnakeNode food = (pSnakeNode)malloc(sizeof(SnakeNode));
if (food == NULL)
{
perror("malloc food");
return;
}
again:
do
{
food->x = rand() % 56 + 1;
food->y = rand() % 28 + 1;
food->next = NULL;
} while (food->x % 2 != 0);//
pSnakeNode cur = snake->phead;
//食物不能在蛇身上
while (cur)
{
if (cur->x == food->x && cur->y == food->y)
goto again;
cur = cur->next;
}
//赋值
snake->_food = *food;
//打印
setpos(snake->_food.x, snake->_food.y);
wprintf(L"%lc", FOOD);
free(food);
}
//暂停
void Pause()
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE) == 1)
break;
}
}
//游戏运行
void Game(Snake* snake)
{
//显示规则
setpos(62, 8);
printf("规则:");
setpos(62, 9);
printf("1.不要撞墙");
setpos(62, 10);
printf("2.不要撞到自己");
setpos(62, 11);
printf("3.F3加速");
setpos(62, 12);
printf("4.F4减速");
setpos(62, 13);
printf("5.空格暂停");
setpos(62, 14);
printf("6.Esc退出");
//放置食物
PutFood(snake);
//蛇移动(更新数据)
while (snake->_state == OK)
{
setpos(62, 5);
printf("当前食物分值:%2d", snake->food_score);
setpos(62, 6);
printf("当前得分:%d", snake->_score);
pSnakeNode pnext = (pSnakeNode)malloc(sizeof(SnakeNode));
if (pnext == NULL)
{
perror("malloc pnext fail");
return;
}
//按键判断
//方向判断
if (KEY_PRESS(VK_UP) == 1 && snake->dir != DOWN)
{
snake->dir = UP;
}
else if (KEY_PRESS(VK_DOWN) == 1 && snake->dir != UP)
{
snake->dir = DOWN;
}
else if (KEY_PRESS(VK_LEFT) == 1 && snake->dir != RIGHT)
{
snake->dir = LEFT;
}
else if (KEY_PRESS(VK_RIGHT) == 1 && snake->dir != LEFT)
{
snake->dir = RIGHT;
}
else if (KEY_PRESS(VK_SPACE) == 1)//判断暂停
{
Pause();
}
else if (KEY_PRESS(VK_ESCAPE) == 1)//判断退出
{
snake->_state = ESC;
setpos(26, 14);
printf("已退出");
break;
}
else if (KEY_PRESS(VK_F3) == 1)//加速
{
if(snake->_sleep>80)
{
snake->_sleep -= 50;
snake->food_score += 2;
}
}
else if (KEY_PRESS(VK_F4) == 1)//减速
{
if (snake->food_score > 2)
{
snake->_sleep += 50;
snake->food_score -= 2;
}
}
//刷新蛇身信息
if (snake->dir == UP)
{
pnext->x = snake->phead->x;
pnext->y = snake->phead->y - 1;
}
else if (snake->dir == DOWN)
{
pnext->x = snake->phead->x;
pnext->y = snake->phead->y + 1;
}
else if (snake->dir == LEFT)
{
pnext->x = snake->phead->x - 2;
pnext->y = snake->phead->y;
}
else if (snake->dir == RIGHT)
{
pnext->x = snake->phead->x + 2;
pnext->y = snake->phead->y;
}
//状态判断
//吃到食物
if (pnext->x == snake->_food.x && pnext->y == snake->_food.y)
{
pnext->next = snake->phead;
snake->phead = pnext;
PrintNode(snake);
PutFood(snake);
snake->_score += snake->food_score;
}
else {
//没有食物
pnext->next = snake->phead;
snake->phead = pnext;
pSnakeNode cur = snake->phead;
while (cur->next->next)
{
cur = cur->next;
}
setpos(cur->next->x, cur->next->y);
printf(" ");
free(cur->next);
cur->next = NULL;
PrintNode(snake);
}
//撞墙
if (pnext->x == 0 || pnext->x == 58 || pnext->y == 0 || pnext->y == 29)
{
snake->_state = KILL_BY_WALL;
setpos(26, 14);
printf("您撞墙了");
}
//撞自己
pSnakeNode cur = snake->phead->next;
while (cur)
{
if (snake->phead->x == cur->x && snake->phead->y == cur->y)
{
snake->_state = KILL_BY_SELF;
setpos(26, 14);
printf("您撞上了自己");
break;
}
cur = cur->next;
}
Sleep(snake->_sleep);
}
}
//释放链表节点
void Relese(pSnakeNode phead)
{
pSnakeNode cur=phead;
while (phead)
{
cur = phead;
phead = phead->next;
free(cur);
}
//出函数后要置空phead
}
5.3 tool.h
#pragma once
#include<Windows.h>
#include<stdbool.h>
//前置声明
typedef struct SnakeNode* pSnakeNode;
typedef struct Snake Snake;
//函数声明
//定位光标
void setpos(int x,int y);
//打印节点
void PrintNode(Snake* snake);
5.4 tool.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"tool.h"
#include"Snake.h"
//定位光标
void setpos(int x,int y){
HANDLE houtput = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos;
pos.X = x;
pos.Y = y;
SetConsoleCursorPosition(houtput, pos);
}
//打印节点
void PrintNode(Snake* snake)
{
pSnakeNode cur = snake->phead;
while(cur)
{
setpos(cur->x, cur->y);
wprintf(L"%lc", SNAKE);
cur = cur->next;
}
}
5.5 main.c
#define _CRT_SECURE_NO_WARNINGS 1
#include"Snake.h"
void test(Snake* snake) {
//显示欢迎,提示页面
Welcome();
//初始化地图
MapInit();
//初始化贪吃蛇
SnakeInit(snake);
//游戏运行
Game(snake);
//结束善后
//释放链表节点
Relese(snake->phead);
snake->phead = NULL;
setpos(22, 15);
}
int main()
{
int a;
//初始化本地环境
setlocale(LC_ALL, "");
do
{
a = 0;
system("cls");
//初始化游戏
srand((unsigned int)time(NULL));
Snake snake = { 0 };
InitGame(&snake);
//游戏逻辑
test(&snake);
printf("是否再来一局(1.Yes/0.No):");
scanf("%d", &a);
} while (a == 1);
setpos(0, 30);
return 0;
}
--------------------------------------------------------------------------------------------------------------------------------
好啦,关于贪吃蛇小游戏的实现讲解就先到这里啦,看完的小伙伴记得关注支持一下博主哦~
有问题欢迎在下方提问~