在windows的控制台实现贪吃蛇小游戏
欢迎来到博主的文章
博主id:代码小豪
前言:看懂这篇文章需要具有C语言基础,还要对单链表具有一定的理解。如果你只是想要试玩这个游戏,可以直接在文章末尾找到源码
由于实现贪吃蛇需要调用Win32 API函数,这些函数我会简单介绍,不涉及底层(主要是博主能力不够哈哈)。这篇文章的注意讲解在于贪吃蛇的算法
文章目录
- 贪吃蛇的算法
- 坐标
- “蛇”的节点
- 蛇的移动
- 蛇的速度
- 游戏呈现
- 控制台指令
- Win32api
- 控制台光标
- 键盘读取
- 贪吃蛇源码
- 头文件
- 定义文件
- 执行文件
贪吃蛇的算法
坐标
这是一个windows控制台,其中白色的点叫做光标。
如过想要控制台输出字符,字符就会出现在光标的位置。这些光标是可以通过坐标控制的,左上角的第一个坐标为(0,0),x轴向右,y轴向下。坐标点随轴依次递增。
如下:
假设现在有个光标在点(x,0)处,让光标的y值依次递增并输出字符’1’。呈现的效果是这样的
这个’1’像不像一个向下移动的贪吃蛇呢?
“蛇”的节点
我们将蛇身的坐标(x和y)作为数据项。通过链表的形式将蛇连接起来。
typedef int _pos;
typedef struct SnakeNode//蛇身的数据类型
{
_pos x;//横坐标
_pos y;//纵坐标
struct SnakeNode* next;
}SnakeNode, * PSnakeNode;
贪吃蛇中的食物也要有相应的坐标,因此食物的数据类型也可以用上述的结构体类型。
每个节点(不论是蛇身还是食物)都有其对应的坐标,通过遍历整个链表,每访问一个节点,就在该节点对应的坐标(存储的x值,y值)打印相应的物体。
蛇的移动
蛇头作为参考点,根据玩家的方向控制,在相应的坐标处生成一个新的节点,使用头插法将新生成的节点插入蛇头的位置,并将末尾的节点释放掉。
以向下移动为例,在蛇头(x,y)向下的位置(x,y+1)的位置生成新的节点,并将末尾节点释放掉。
这样子就能做到蛇的“移动”。
蛇的速度
在移动的程序当中添加一个“时停”的函数,比如Sleep函数,当程序执行Sleep函数时会暂停指定的时间,然后继续执行其他操作
while (1)
{
phead=SnakeMove(phead);
Sleep(1000);
}
这里的Sleep函数是库函数,定义在头文件<windows.h>
游戏呈现
这里给大家看看博主贪吃蛇实现如何
QQ录屏20240208220229
控制台指令
通过指令可以对控制台进行操作,比如输入指令“cls”可以清除当前控制台屏幕。
system("cls");
执行这段代码时会清理控制台屏幕的当前信息
system("pause");
暂停操作
system("mode con cols=120 lines=30");
对控制台的窗口大小进行修改
system("title 贪吃蛇");
修改标题
Win32api
贪吃蛇的算法已经讲解完,现在来介绍一下使用到的其他函数
控制台光标
我将操作控制台光标的程序封装成了一个函数,只需要输入坐标,控制台光标就会移动到该坐标上
void SetPos(int x, int y)//设置光标位置
{
HANDLE cursorhandle = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x,y };
SetConsoleCursorPosition(cursorhandle, pos);
}
GetStdHanDle函数用来获取控制台的句柄
COORD:是WindowsAPI中定义的⼀个结构体,表⽰⼀个字符在控制台屏幕上的坐标
SetConsoleCursorPosition:设置指定控制台屏幕缓冲区中的光标位置,我们将想要设置的坐标信息放在COORD类型的pos中,调⽤SetConsoleCursorPosition函数将光标位置设置到指定的位置
这段代码的原理如下:
先获得控制台的句柄,在创建一个“坐标”的变量,然后使用函数将光标移动到指定坐标的位置
键盘读取
程序需要接收玩家的按键操作,此时需要用到一个读取虚拟键码的函数——GetAysncKeyState
将键盘上每个键的虚拟键值传递给函数,函数通过返回值来分辨按键的状态
如果返回的16位的short数据中,最⾼位是1,说明按键的状态是按下,如果最⾼是0,说明按键的状态是抬起;
如果最低位被置为1则说明,该按键被按过,否则为0。
如果我们要判断⼀个键是否被按过,可以检测GetAsyncKeyState返回值的最低值是否为1
可以将这个函数做成一个宏,少些点代码
#define KEY_PRESS(vk) (GetAsyncKeyState(vk)&0x1?1:0)
比如判断是否按下s键,可以找到s键对应的虚拟键码
if (KEY_PRESS(0x53))
{
printf("press s");
}
这样子就会执行按下这个键后对应的操作
所有的键位对应的虚拟键码可以在这个网站上找到
虚拟键码
贪吃蛇源码
这里附上贪吃蛇的源代码,大家可以根据自己的思路设计自己的贪吃蛇游戏
贪吃蛇代码被博主用一个头文件,和两个源文件进行封装。其中头文件用来声明函数和数据类型,一个源文件用来定义函数类型,还有一个源文件用来测试案例。
头文件
#define _CRT_SECURE_NO_WARNINGS 1
#pragma once
#include<stdio.h>
#include<stdlib.h>
#include<windows.h>
#include<locale.h>
#include<stdbool.h>
#include<assert.h>
#include<time.h>
#define WALL L'□'//墙体模型
#define BODY L'●'//蛇身模型
#define FOOD L'★'//食物模型
#define COLS 56 //地图长度
#define ROWS 25 //地图宽度
#define ORIGIN_POS_X 24//初始x坐标
#define ORIGIN_POS_Y 5//初始y坐标
#define ORIGIN_NODE 5//初始蛇长
#define KEY_PRESS(vk) (GetAsyncKeyState(vk)&0x1?1:0)
enum GAME_STATUS//游戏运行状态
{
PLAY = 1,
ESC,
KILL_BY_WALL,
KILL_BY_SELF
};
enum DIRECTION//蛇的移动方向
{
UP = 1,
DOWN,
LEFT,
RIGHT
};
typedef int _pos;
typedef struct SnakeNode//蛇身的数据类型
{
_pos x;//横坐标
_pos y;//纵坐标
struct SnakeNode* next;
}SnakeNode, * PSnakeNode;
typedef struct Snake//记录游戏数据
{
PSnakeNode snake;//蛇的当前位置
PSnakeNode food;//食物的当前位置
int score;//总分
int foodwight;//单个食物得分
int speed;//蛇的速度,由于Sleep函数的特性,这个值越大,蛇的速度越慢
enum GAME_STATUS status;
enum DIRECTION dir;
}snake, * psnake;
void GameStart(psnake snake);//开始流程
void CreateMap(void);//生成地图
void GameMenu(void);//游戏菜单
void CreateSnake(psnake psnake);//生成贪吃蛇
void CreateFood(psnake psnake);//生成食物
void GameRun(psnake psnake);//游戏流程
void SnakeMove(psnake psnake);//移动操作
bool NextIsFood(psnake psnake, PSnakeNode pnext);//得分判定
void EatFoodSuccess(psnake psnake,PSnakeNode pnext);//成功吃到食物
void EatFoodFail(psnake psnake, PSnakeNode pnext);//未成功吃到食物
void KillBySelf(psnake psnake);//碰撞检测
void KillByWall(psnake psnake);//检测是否撞墙
void GameOver(psnake psnake);
void SetPos(int x, int y);
定义文件
#include"snake.h"
void SetPos(int x, int y)//设置光标位置
{
HANDLE cursorhandle = GetStdHandle(STD_OUTPUT_HANDLE);
COORD pos = { x,y };
SetConsoleCursorPosition(cursorhandle, pos);
}
void GameMenu(void)
{
setlocale(LC_ALL, "");//将语言环境设置为中文语言,方便宽字体打印
SetPos(40, 15);
printf("欢迎来到贪吃蛇小游戏");
SetPos(40, 20);
system("pause");
system("cls");
/*介绍游戏规则*/
SetPos(20, 10);
printf("用↑ ↓ ← →控制蛇吞掉食物,F3加速,F4减速");
SetPos(20, 11);
printf("速度越快,得分越高,不要碰到墙壁,也不要吃掉自己哦\n");
SetPos(20, 12);
system("pause");
system("cls");
}
void GameStart(psnake psnake)
{
srand((unsigned int)time(NULL));
system("mode con cols=120 lines=30");//如果地图扩大了,那么请将这里的指令也修改一下
system("title 贪吃蛇");
/*隐藏光标*/
HANDLE cmd_handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO info = { 0 };
GetConsoleCursorInfo(cmd_handle, &info);
info.bVisible = false;
SetConsoleCursorInfo(cmd_handle, &info);
/*游戏主菜单*/
GameMenu();
/*生成地图*/
CreateMap();
/*生成贪吃蛇*/
CreateSnake(psnake);
/*创建食物*/
CreateFood(psnake);
/*函数结束*/
}
void CreateMap(void)
{
_pos i = 0;
//生成地图上层
SetPos(0, 0);
for (i = 0; i <= COLS; i += 2)
wprintf(L"%lc", WALL);
//生成地图下层
SetPos(0, ROWS);
for (i = 0; i <= COLS; i += 2)
wprintf(L"%lc", WALL);
//生成地图左侧
for (i = 1; i < ROWS; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
//生成地图右侧
for (i = 1; i < ROWS; i++)
{
SetPos(COLS, i);
wprintf(L"%lc", WALL);
}
}
void CreateSnake(psnake psnake)
{
int i = 0;
PSnakeNode cur = NULL;//蛇头
PSnakeNode snakehead = NULL;
for (i = 0; i < ORIGIN_NODE; i++)
{
cur = (PSnakeNode)malloc(sizeof(SnakeNode));
assert(cur);
cur->x = ORIGIN_POS_X + 2 * i;
cur->y = ORIGIN_POS_Y;
cur->next = NULL;
//由于蛇身的数据结构是链表,因此用头插法将蛇的节点连接起来
if (!snakehead) {
snakehead = cur;
}
else {
cur->next = snakehead;
snakehead = cur;
}
}
psnake->snake = snakehead;//将初始化好的数据记录在主游戏数据中
/*根据数据在游戏中生成贪吃蛇*/
cur = psnake->snake;
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
/*初始化游戏中的所有数值*/
psnake->dir = RIGHT;
psnake->score = 0;
psnake->speed = 200;
psnake->foodwight = 10;
psnake->status = PLAY;
}
void CreateFood(psnake psnake)
{
_pos x;
_pos y;
again:
do {
x = rand() % (COLS - 3) + 2;
y = rand() % (ROWS - 1) + 1;
} while (x % 2 != 0);
/*检查生成食物的坐标是否合理*/
PSnakeNode cur = psnake->snake;
while (cur)
{
if (cur->x == x && cur->y == y)
goto again;
cur = cur->next;
}
/*生成食物*/
PSnakeNode pfood = (PSnakeNode)malloc(sizeof(SnakeNode));
assert(pfood);
pfood->x = x;
pfood->y = y;
SetPos(pfood->x, pfood->y);
wprintf(L"%lc", FOOD);
psnake->food = pfood;
}
void Printmsg(void)
{
SetPos(COLS + 10, 15);
printf("不能撞墙,不能咬到自己");
SetPos(COLS + 10, 16);
printf("用↑ ↓ ← →控制蛇吞掉食物,F3加速,F4减速");
SetPos(COLS + 10, 20);
printf("关注代码小豪谢谢喵");
}
void pause()
{
SetPos(COLS + 10, ROWS - 1);
printf("按空格继续");
do
{
Sleep(100);
if (KEY_PRESS(VK_SPACE))
break;
} while (1);
SetPos(COLS + 10, ROWS - 1);
printf(" ");
}
void GameRun(psnake psnake)
{
/*游戏内信息*/
Printmsg();
//游戏运行逻辑
do
{
//分数
SetPos(COLS + 10, 10);
printf("总分:%d", psnake->score);
SetPos(COLS + 10, 11);
printf("单个食物得分:%2d",psnake->foodwight);
//检测按键
if (KEY_PRESS(VK_UP) && psnake->dir != DOWN)
psnake->dir = UP;
if (KEY_PRESS(VK_DOWN) && psnake->dir != UP)
psnake->dir = DOWN;
if (KEY_PRESS(VK_LEFT) && psnake->dir != RIGHT)
psnake->dir = LEFT;
if (KEY_PRESS(VK_RIGHT) && psnake->dir != LEFT)
psnake->dir = RIGHT;
if (KEY_PRESS(VK_ESCAPE))
psnake->status = ESC;//退出游戏
if (KEY_PRESS(VK_SPACE)) //暂停游戏
pause();
if (KEY_PRESS(VK_F3))//加速
{
if (psnake->speed > 50)
{
psnake->speed -= 30;
psnake->foodwight += 2;
}
}
if (KEY_PRESS(VK_F4))//减速
{
if (psnake->foodwight > 2)
{
psnake->speed += 30;
psnake->foodwight -= 2;
}
}
Sleep(psnake->speed);
//蛇的移动
SnakeMove(psnake);
} while (psnake->status == PLAY);
}
void SnakeMove(psnake psnake)
{
PSnakeNode pnext = (PSnakeNode)malloc(sizeof(SnakeNode));
assert(pnext);
pnext->next = NULL;
switch (psnake->dir)
{
case UP:
pnext->x = psnake->snake->x;
pnext->y = psnake->snake->y - 1;
break;
case DOWN:
pnext->x = psnake->snake->x;
pnext->y = psnake->snake->y + 1;
break;
case LEFT:
pnext->x = psnake->snake->x-2;
pnext->y = psnake->snake->y ;
break;
case RIGHT:
pnext->x = psnake->snake->x+2;
pnext->y = psnake->snake->y ;
break;
}
/*判断该方向的坐标上是不是食物*/
if (NextIsFood(psnake, pnext))//是食物就吃掉,并增加体型
{
EatFoodSuccess(psnake,pnext);//成功吃到食物
}
else
{
EatFoodFail(psnake,pnext);//未成功吃到食物
}
KillBySelf(psnake);
KillByWall(psnake);
}
bool NextIsFood(psnake psnake, PSnakeNode pnext)
{
if (psnake->food->x == pnext->x && psnake->food->y == pnext->y)//判定成功
return true;
return false;//判定失败
}
void EatFoodSuccess(psnake psnake, PSnakeNode pnext)
{
/*增加体型*/
pnext->next = psnake->snake;
psnake->snake = pnext;
/*在屏幕上生成蛇的新节点*/
SetPos(pnext->x, pnext->y);
psnake->score += psnake->foodwight;//加分
free(psnake->food);
CreateFood(psnake);
}
void EatFoodFail(psnake psnake, PSnakeNode pnext)
{
pnext->next = psnake->snake;
psnake->snake = pnext;
PSnakeNode cur = psnake->snake;
while (cur->next->next)//找到尾结点
{
SetPos(cur->x, cur->y);
cur = cur->next;
wprintf(L"%lc", BODY);
}
SetPos(cur->next->x, cur->next->y);
wprintf(" ");
free(cur->next);
cur->next = NULL;
}
void KillBySelf(psnake psnake)//碰撞判定
{
_pos x = psnake->snake->x;
_pos y = psnake->snake->y;
PSnakeNode cur = psnake->snake->next;
while (cur)
{
if (cur->x == x && cur->y == y)//如果蛇头碰到身体就算死亡
{
psnake->status = KILL_BY_SELF;
return;
}
cur = cur->next;
}
}
void KillByWall(psnake psnake)
{
_pos x = psnake->snake->x;
_pos y = psnake->snake->y;
if (x == 0 ||//判断贪吃蛇是否撞到墙
x >= COLS ||
y == 0 ||
y >= ROWS)
psnake->status = KILL_BY_WALL;
}
void GameOver(psnake psnake)
{
SetPos(30, 15);
switch (psnake->status)
{
case ESC:
system("cls");
printf("退出成功,感谢您的游玩");
SetPos(0, ROWS+2);
exit(EXIT_SUCCESS);
break;
case KILL_BY_SELF:
printf("很抱歉,您被自己吃了");
break;
case KILL_BY_WALL:
printf("很抱歉,您撞到墙了");
break;
}
/*清空动态内存*/
PSnakeNode cur = psnake->snake;
PSnakeNode del = NULL;
while (cur)
{
del = cur;
cur = cur->next;
free(del);
}
free(psnake->food);
psnake = NULL;
}
执行文件
#include"snake.h"
void Test1(void)
{
char ch = 0;
do
{
snake snake = { 0 };
GameStart(&snake);//初始化游戏
GameRun(&snake);//游戏运行
GameOver(&snake);//游戏结束
SetPos(30, 15);
printf("重新游玩?(输入Y):");
ch = getchar();
getchar();
} while (ch == 'Y' || ch == 'y');
system("cls");
SetPos(30, 15);
printf("退出成功,感谢您的游玩");
SetPos(0, ROWS + 2);
}
int main()
{
Test1();
return 0;
}