本文中,我们将使用链表和一些Win32 API的知识来实现贪吃蛇小游戏
一、功能
(1)游戏载入界面
(2)地图的绘制
(3)蛇身的移动和变长
(4)食物的生成
(5)死亡判定
(6)计算得分
(7)退出游戏和暂停游戏
实现贪吃蛇小游戏,我们需要创建3个文件来实现不同的部分
- Snake.c:游戏函数的实现
- Snake.h:结构体定义、头文件引用、宏定义和函数声明
- test.c:主函数的实现
二、Snake.h
#pragma once
#include <stdio.h>
#include <stdlib.h>
#include <Windows.h>
#include <stdbool.h>
#include <locale.h>
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)
#define WALL L'□'
#define BODY L'●'
#define FOOD L'★'
//蛇的初始位置
#define POS_X 24
#define POS_Y 5
enum GAME_STATUS
{
RUN,
ESC,
KILL_BY_WALL,
KILL_BY_SELF,
};
enum DIRECTION
{
UP,
DOWN,
LEFT,
RIGHT,
};
//蛇⾝节点
typedef struct SnakeNode
{
short x;
short y;
struct SnakeNode* next;
}SnakeNode;
typedef struct Snake
{
SnakeNode* pSnake;//指向蛇头
SnakeNode* Food;
int SleepTime;
int Score;
int FoodScore;
enum GAME_STATUS Status;
enum DIRECTION Dir;
}Snake;
void GameStart(Snake* ps);
void GameRun(Snake* ps);
void GameEnd(Snake* ps);
void SetPos(short x, short y);//设置光标坐标
void WelcomeToGame();//欢迎界面
void CreateMap();//创建地图
void InitSnake(Snake* ps);//初始化蛇
void CreateFood(Snake* ps);//生成食物
void PrintHelpInfo();//打印教程
void SnakeMove(Snake* ps);//蛇身移动
void EatFood(Snake* ps, SnakeNode* next);//移动后吃到食物
void NotEatFood(Snake* ps, SnakeNode* next);//移动后没吃到食物
void KillByWall(Snake* ps);//检测是否撞墙
void KillBySelf(Snake* ps);//检测是否撞到自己
三、Snake.c
在Snake.c中,我们将整个游戏拆分成游戏前的准备、游戏运行中和游戏结束后三部分
首先要在头文件中定义蛇的节点等相关信息
enum GAME_STATUS //游戏状态的枚举
{
RUN, //游戏正常运行中
ESC, //正常退出游戏
KILL_BY_WALL, //撞到墙导致游戏结束
KILL_BY_SELF, //撞到自己导致游戏结束
};
enum DIRECTION //蛇身方向的枚举
{
UP,
DOWN,
LEFT,
RIGHT,
};
//蛇⾝节点
typedef struct SnakeNode
{
short x; //x轴坐标
short y; //y轴坐标
struct SnakeNode* next;
}SnakeNode;
typedef struct Snake
{
SnakeNode* pSnake; //指向蛇头
SnakeNode* Food; //指向食物
int SleepTime; //蛇身运动的速度
int Score; //总分数
int FoodScore; //每个食物的分数
enum GAME_STATUS Status; //游戏运行的状态
enum DIRECTION Dir; //蛇身方向
}Snake;
接下来就可以开始设计游戏前的准备程序了
2.1 游戏前的准备
为了美观,我们可以使用一些cmd命令来设置控制台窗口的长宽等信息
控制台窗口实际上是有坐标的,也就是有行和列的
最左上角的位置的坐标为(0,0),像这样的一个字符高为1 宽为1
所以我们可以通过cmd命令将控制台设置为30行,100列,并将标题改成贪吃蛇
void GameStart(Snake* ps)
{
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);//获得设备句柄
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(handle, &CursorInfo); //获得光标信息
CursorInfo.bVisible = false; //设置光标为不可见
SetConsoleCursorInfo(handle, &CursorInfo); //保存光标信息
//欢迎界面
WelcomeToGame();
//创建地图
CreateMap();
//初始化蛇
InitSnake(ps);
//生成食物
CreateFood(ps);
}
将剩下的四个功能分别分装在四个函数中
(1)欢迎界面
void WelcomeToGame()//欢迎界面
{
SetPos(45, 10);
printf("欢迎来到贪吃蛇小游戏");
SetPos(45, 15);
system("pause");
system("cls");
SetPos(35, 10);
printf("用 ↑.↓.←.→ 控制蛇的行动,F3为加速,F4为减速");
SetPos(35, 11);
printf("加速能得到更多的分数");
SetPos(45, 15);
system("pause");
system("cls");
}
其中,SetPos函数是用来设置光标坐标的,因为在控制台中printf中的内容会从光标的位置开始打印。
void SetPos(short x, short y) // 设置光标坐标
{
COORD pos = {x, y};
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(handle, pos);
}
完成后的效果如下:
(2)创建地图
创建地图时需要用到宽字符的打印,宽字符也就是高为1 宽为2的字符,需要用wprintf函数打印
void CreateMap() // 创建地图
{
SetPos(0, 0);
int i = 0;
for (i = 0; i < 58; i += 2)
{
SetPos(i, 0);
wprintf(L"%lc", L'□');
}
for (i = 0; i < 58; i += 2)
{
SetPos(i, 26);
wprintf(L"%lc", L'□');
}
for (i = 1; i < 26; i++)
{
SetPos(0, i);
wprintf(L"%lc", L'□');
}
for (i = 1; i < 26; i++)
{
SetPos(56, i);
wprintf(L"%lc", L'□');
}
}
使用循环把墙体打印出来即可
我们也可以用宏定义将 L'□' 替换
#define WALL L'□'
(3)初始化蛇
void InitSnake(Snake *ps) // 初始化蛇
{
SnakeNode *cur = ps->pSnake = NULL;
for (int i = 0; i < 5; i++)
{
cur = (SnakeNode *)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnake:malloc fail");
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;
}
}
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->Food = NULL;
ps->SleepTime = 200;
ps->Score = 0;
ps->FoodScore = 10;
ps->Status = RUN;
ps->Dir = RIGHT;
}
对Snake结构体中的各项信息进行初始化即可,主要难点在于蛇身的创建
这里的蛇身和蛇的初始位置也用了宏定义
#define BODY L'●'
//蛇的初始位置
#define POS_X 24
#define POS_Y 5
(4)生成食物
void CreateFood(Snake *ps) // 生成食物
{
int x = 0;
int y = 0;
SnakeNode *cur = ps->pSnake;
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 == 1);
while (cur)
{
if (cur->x == x || cur->y == y)
{
goto again;
break;
}
cur = cur->next;
}
ps->Food = (SnakeNode *)malloc(sizeof(SnakeNode));
if (ps->Food == NULL)
{
perror("CreateFood:malloc fail");
return;
}
ps->Food->x = x;
ps->Food->y = y;
SetPos(x, y);
wprintf(L"%lc", FOOD);
}
随机生成食物要用到rand函数,并且食物不能跑出地图外,我们要注意一下细节
1.宽字符占两格,所以x必须为偶数
2.食物要保持在地图内部,x轴范围为2~54,也就是(0~52)+2,y轴同理
3.食物不能和蛇身重合
这里的食物也用了宏定义
#define FOOD L'★'
2.2 游戏运行中
这一部分需要包含分数等信息的打印和按键输入的判定等功能
这里将按键输入的判断封装为了宏
#define KEY_PRESS(VK) ((GetAsyncKeyState(VK) & 0x1) ? 1 : 0)
GetAsyncKeyState的返回值是short类型,在上一次调用完该函数后,如果返回的16位的short数据中最高位是1,说明按键的状态为按下,最高位是0则为抬起
void GameRun(Snake* ps)
{
PrintHelpInfo();//打印教程
do
{
//打印信息
SetPos(60, 10);
printf("当前得分:%2d", ps->Score);
SetPos(60, 11);
printf("当前速度:%3d", (400 - ps->SleepTime) / 40);
SetPos(60, 12);
printf("每个食物得分:%2d", ps->FoodScore);
//判断按键
if (KEY_PRESS(VK_UP) && ps->Dir != DOWN)
{
ps->Dir = UP;
}
if (KEY_PRESS(VK_DOWN) && ps->Dir != UP)
{
ps->Dir = DOWN;
}
if (KEY_PRESS(VK_LEFT) && ps->Dir != RIGHT)
{
ps->Dir = LEFT;
}
if (KEY_PRESS(VK_RIGHT) && ps->Dir != LEFT)
{
ps->Dir = RIGHT;
}
if (KEY_PRESS(VK_ESCAPE))
{
ps->Status = ESC;
break;
}
if (KEY_PRESS(VK_SPACE))
{
pause();
}
if (KEY_PRESS(VK_F3))
{
if (ps->SleepTime > 40)
{
ps->SleepTime -= 40;
ps->FoodScore += 2;
}
}
if (KEY_PRESS(VK_F4))
{
if (ps->FoodScore > 2)
{
ps->SleepTime += 40;
ps->FoodScore -= 2;
}
}
Sleep(ps->SleepTime);
//蛇身移动
SnakeMove(ps);
} while (ps->Status == RUN);
}
(1)打印教程
void PrintHelpInfo() // 打印教程
{
SetPos(60, 16);
printf("请按空格键开始游戏");
SetPos(60, 17);
printf("不能撞到墙上或者撞到自己");
SetPos(60, 18);
printf("用 ↑.↓.←.→ 控制蛇的行动");
SetPos(60, 19);
printf("F3为加速,F4为减速");
SetPos(60, 20);
printf("ESC:退出游戏 space:暂停游戏");
}
效果如下
(2)暂停游戏
void pause() // 暂停游戏
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
这里需要实现按一次空格暂停,再按一次恢复的效果,所以在循环内部还需要设置按键的判定。
(3)蛇身移动
void SnakeMove(Snake *ps) // 蛇身移动
{
SnakeNode *next = (SnakeNode *)malloc(sizeof(SnakeNode));
if (next == NULL)
{
perror("SnakeMove:malloc fail");
return;
}
switch (ps->Dir)
{
case UP:
next->x = ps->pSnake->x;
next->y = ps->pSnake->y - 1;
break;
case DOWN:
next->x = ps->pSnake->x;
next->y = ps->pSnake->y + 1;
break;
case LEFT:
next->x = ps->pSnake->x - 2;
next->y = ps->pSnake->y;
break;
case RIGHT:
next->x = ps->pSnake->x + 2;
next->y = ps->pSnake->y;
break;
}
if (next->x == ps->Food->x && next->y == ps->Food->y)
EatFood(ps, next);
else
NotEatFood(ps, next);
KillByWall(ps);
KillBySelf(ps);
}
malloc一个新节点放到蛇头下一步移动到的位置,替换成新的蛇头。位置的计算根据蛇身的方向来计算坐标即可。
如果蛇的下一步吃到了食物,就不需要尾删,如果没吃到食物则需要尾删一次并且抹除尾巴
并且每一步移动都需要判定是否撞墙或撞到自己
(4)移动后吃到食物的情况
void EatFood(Snake *ps, SnakeNode *next) // 移动后吃到食物
{
next->next = ps->pSnake;
ps->pSnake = next;
SetPos(next->x, next->y);
wprintf(L"%lc", BODY);
ps->Score += ps->FoodScore;
free(ps->Food);
ps->Food = NULL;
CreateFood(ps);
}
将原先的蛇头和新节点相连,更新蛇头的地址并且打印出新蛇头,不要忘了吃到食物后加分,还要创建一个新的食物
(5)移动后没吃到食物的情况
void NotEatFood(Snake *ps, SnakeNode *next) // 移动后没吃到食物
{
next->next = ps->pSnake;
ps->pSnake = next;
SnakeNode *cur = ps->pSnake;
while (cur->next->next)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
SetPos(cur->next->x, cur->next->y);
printf(" ");
free(cur->next);
cur->next = NULL; // 不置空会出现问题
}
还是将原先的蛇头和新节点链接,但是这次需要把尾部的节点删除并抹除尾节点
(6)检测是否撞墙
void KillByWall(Snake *ps) // 检测是否撞墙
{
if (ps->pSnake->x == 0 ||
ps->pSnake->x == 56 ||
ps->pSnake->y == 0 ||
ps->pSnake->y == 25)
{
ps->Status = KILL_BY_WALL;
}
}
检测蛇头的坐标和墙的坐标是否重合即可
(7)检测是否撞到自己
void KillBySelf(Snake *ps) // 检测是否撞到自己
{
SnakeNode *cur = ps->pSnake->next;
while (cur)
{
if (ps->pSnake->x == cur->x && ps->pSnake->y == cur->y)
{
ps->Status = KILL_BY_SELF;
}
cur = cur->next;
}
}
检测蛇头的坐标是否和蛇身任一节点重合即可
2.3 游戏结束后
void GameEnd(Snake *ps)
{
SetPos(24, 12);
switch (ps->Status)
{
case KILL_BY_SELF:
printf("你撞到了自己");
break;
case KILL_BY_WALL:
printf("你撞到墙了");
break;
case ESC:
printf("游戏退出中...");
break;
}
SnakeNode *cur = ps->pSnake;
while (ps->pSnake)
{
ps->pSnake = ps->pSnake->next;
free(cur);
cur = ps->pSnake;
}
}
根据游戏的状态打印不同的信息,并且释放蛇身的内存空间
2.4 完整代码
#include "snake.h"
void SetPos(short x, short y)//设置光标坐标
{
COORD pos = { x,y };
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(handle, pos);
}
void WelcomeToGame()//欢迎界面
{
SetPos(45, 10);
printf("欢迎来到贪吃蛇小游戏");
SetPos(45, 15);
system("pause");
system("cls");
SetPos(35, 10);
printf("用 ↑.↓.←.→ 控制蛇的行动,F3为加速,F4为减速");
SetPos(35, 11);
printf("加速能得到更多的分数");
SetPos(45, 15);
system("pause");
system("cls");
}
void CreateMap()//创建地图
{
SetPos(0, 0);
int i = 0;
for (i = 0; i < 58; i += 2)
{
SetPos(i, 0);
wprintf(L"%lc", WALL);
}
for (i = 0; i < 58; i += 2)
{
SetPos(i, 26);
wprintf(L"%lc", WALL);
}
for (i = 1; i < 26; i++)
{
SetPos(0, i);
wprintf(L"%lc", WALL);
}
for (i = 1; i < 26; i++)
{
SetPos(56, i);
wprintf(L"%lc", WALL);
}
}
void InitSnake(Snake* ps)//初始化蛇
{
SnakeNode* cur = ps->pSnake = NULL;
for (int i = 0; i < 5; i++)
{
cur = (SnakeNode*)malloc(sizeof(SnakeNode));
if (cur == NULL)
{
perror("InitSnake:malloc fail");
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;
}
}
while (cur)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
ps->Food = NULL;
ps->SleepTime = 200;
ps->Score = 0;
ps->FoodScore = 10;
ps->Status = RUN;
ps->Dir = RIGHT;
}
void CreateFood(Snake* ps)//生成食物
{
int x = 0;
int y = 0;
SnakeNode* cur = ps->pSnake;
again:
do
{
x = rand() % 53 + 2;
y = rand() % 25 + 1;
} while (x % 2 == 1);
while (cur)
{
if (cur->x == x || cur->y == y)
{
goto again;
break;
}
cur = cur->next;
}
ps->Food = (SnakeNode*)malloc(sizeof(SnakeNode));
if (ps->Food == NULL)
{
perror("CreateFood:malloc fail");
return;
}
ps->Food->x = x;
ps->Food->y = y;
SetPos(x, y);
wprintf(L"%lc", FOOD);
}
void GameStart(Snake* ps)
{
system("mode con cols=100 lines=30");
system("title 贪吃蛇");
HANDLE handle = GetStdHandle(STD_OUTPUT_HANDLE);
CONSOLE_CURSOR_INFO CursorInfo;
GetConsoleCursorInfo(handle, &CursorInfo);
CursorInfo.bVisible = false;
SetConsoleCursorInfo(handle, &CursorInfo);
WelcomeToGame();
CreateMap();
InitSnake(ps);
CreateFood(ps);
}
void PrintHelpInfo()//打印教程
{
SetPos(60, 16);
printf("请按空格键开始游戏");
SetPos(60, 17);
printf("不能撞到墙上或者撞到自己");
SetPos(60, 18);
printf("用 ↑.↓.←.→ 控制蛇的行动");
SetPos(60, 19);
printf("F3为加速,F4为减速");
SetPos(60, 20);
printf("ESC:退出游戏 space:暂停游戏");
}
void pause()//暂停游戏
{
while (1)
{
Sleep(200);
if (KEY_PRESS(VK_SPACE))
{
break;
}
}
}
void EatFood(Snake* ps, SnakeNode* next)//移动后正好吃到食物
{
next->next = ps->pSnake;
ps->pSnake = next;
SetPos(next->x, next->y);
wprintf(L"%lc", BODY);
ps->Score += ps->FoodScore;
free(ps->Food);
ps->Food = NULL;
CreateFood(ps);
}
void NotEatFood(Snake* ps, SnakeNode* next)//移动后没吃到食物
{
next->next = ps->pSnake;
ps->pSnake = next;
SnakeNode* cur = ps->pSnake;
while (cur->next->next)
{
SetPos(cur->x, cur->y);
wprintf(L"%lc", BODY);
cur = cur->next;
}
SetPos(cur->next->x, cur->next->y);
printf(" ");
free(cur->next);
cur->next = NULL;//亲测不置空会出现问题
}
void KillByWall(Snake* ps)//检测是否撞墙
{
if (ps->pSnake->x == 0 ||
ps->pSnake->x == 56 ||
ps->pSnake->y == 0 ||
ps->pSnake->y == 25)
{
ps->Status = KILL_BY_WALL;
}
}
void KillBySelf(Snake* ps)//检测是否撞到自己
{
SnakeNode* cur = ps->pSnake->next;
while (cur)
{
if (ps->pSnake->x == cur->x && ps->pSnake->y == cur->y)
{
ps->Status = KILL_BY_SELF;
}
cur = cur->next;
}
}
void SnakeMove(Snake* ps)//蛇身移动
{
SnakeNode* next = (SnakeNode*)malloc(sizeof(SnakeNode));
if (next == NULL)
{
perror("SnakeMove:malloc fail");
return;
}
switch (ps->Dir)
{
case UP:
next->x = ps->pSnake->x;
next->y = ps->pSnake->y - 1;
break;
case DOWN:
next->x = ps->pSnake->x;
next->y = ps->pSnake->y + 1;
break;
case LEFT:
next->x = ps->pSnake->x - 2;
next->y = ps->pSnake->y;
break;
case RIGHT:
next->x = ps->pSnake->x + 2;
next->y = ps->pSnake->y;
break;
}
if (next->x == ps->Food->x && next->y == ps->Food->y)
EatFood(ps, next);
else
NotEatFood(ps, next);
KillByWall(ps);
KillBySelf(ps);
}
void GameRun(Snake* ps)
{
PrintHelpInfo();
do
{
//打印信息
SetPos(60, 10);
printf("当前得分:%2d", ps->Score);
SetPos(60, 11);
printf("当前速度:%3d", (400 - ps->SleepTime) / 40);
SetPos(60, 12);
printf("每个食物得分:%2d", ps->FoodScore);
//判断按键
if (KEY_PRESS(VK_UP) && ps->Dir != DOWN)
{
ps->Dir = UP;
}
if (KEY_PRESS(VK_DOWN) && ps->Dir != UP)
{
ps->Dir = DOWN;
}
if (KEY_PRESS(VK_LEFT) && ps->Dir != RIGHT)
{
ps->Dir = LEFT;
}
if (KEY_PRESS(VK_RIGHT) && ps->Dir != LEFT)
{
ps->Dir = RIGHT;
}
if (KEY_PRESS(VK_ESCAPE))
{
ps->Status = ESC;
break;
}
if (KEY_PRESS(VK_SPACE))
{
pause();
}
if (KEY_PRESS(VK_F3))
{
if (ps->SleepTime > 40)
{
ps->SleepTime -= 40;
ps->FoodScore += 2;
}
}
if (KEY_PRESS(VK_F4))
{
if (ps->FoodScore > 2)
{
ps->SleepTime += 40;
ps->FoodScore -= 2;
}
}
Sleep(ps->SleepTime);
//蛇身移动
SnakeMove(ps);
} while (ps->Status == RUN);
}
void GameEnd(Snake* ps)
{
SetPos(24, 12);
switch (ps->Status)
{
case KILL_BY_SELF:
printf("你撞到了自己");
break;
case KILL_BY_WALL:
printf("你撞到墙了");
break;
case ESC:
printf("游戏退出中...");
break;
}
SnakeNode* cur = ps->pSnake;
while (ps->pSnake)
{
ps->pSnake = ps->pSnake->next;
free(cur);
cur = ps->pSnake;
}
}
四、test.c
#include "snake.h"
void Test()
{
srand((unsigned int)time(NULL));
char ch = 0;
do
{
Snake snake;
GameStart(&snake);
GameRun(&snake);
GameEnd(&snake);
if (snake.Status == ESC)
break;
SetPos(24, 13);
printf("再来一局吗?(Y/N):");
scanf(" %c", &ch);
} while (ch == 'Y' || ch == 'y');
SetPos(0, 27);
}
int main()
{
setlocale(LC_ALL, "chs");
Test();
return 0;
}