C++实现童年经典游戏—贪吃蛇(可穿墙)
1.游戏说明
符号说明:
@表示蛇头,O表示蛇身,* 表示食物。
操作说明:
W、A、S、D 控制方向,长按加速。
游戏机制:
每吃掉一次食物,蛇身就增加一节,同时增加积分;
随着分数的增加,蛇身移动速度会越来越快。
游戏模式:
本游戏包含两种模式,可穿墙和不可穿墙,不可穿墙模式下蛇头碰到边界时游戏结束,可穿墙模式则不会,两种模式下蛇头碰到蛇身都会结束游戏。
2.运行效果
3.完整代码
// 贪吃蛇.cpp
#include <iostream>
#include <vector>
#include <Windows.h>
#include <string.h>
#include <ctime>
#include <conio.h>
#define MAP_WIDTH 50;
#define MAP_HEIGHT 25;
int height, width;//窗口宽高
int food_x, food_y;//食物坐标
int head_x, head_y;//蛇头坐标
int next_x, next_y;//蛇头下一步坐标
int score;//得分
int delta_time;//时间间隔
bool canCross;//能否穿墙
bool invincible = false;//无敌模式,修改为true时启用
HANDLE hDirectionMutex;//方向句柄
enum BlockType {
Blank = ' ', //空白
SnakeBody = 'O',//蛇身
SnakeHead = '@',//蛇头
Food = '*'//食物
};
enum Direction { //蛇移动方向
Up,
Down,
Left,
Right
};
class Block {
private:
unsigned short life;
BlockType type;
public:
Block() { life = 0; type = BlockType::Blank; }
~Block() {}
void setLife(int life) {
if (life >= 0)this->life = (unsigned short)life;
if (life == 0) type = BlockType::Blank;
}
unsigned short getLife() { return life; }
void lifeUp() {
life++;
}
void lifeDown() {
if (life > 0)life--;
if (life == 0)type = BlockType::Blank;
}
void setType(BlockType type) {
this->type = type;
}
char getType() {
return (char)type;
}
};
std::vector<std::vector<Block>>map;//游戏地图
Direction direction;//移动方向
void moveCursor(int x, int y);//移动光标
void hideCursor(void);//隐藏光标
void init();//初始化设置
void initData();//初始化数据
void createFood();//创建食物
void eatFood();//吃食物
void crossWall();//穿墙
bool checkFail(int next_x, int next_y);//检测失败
void paint();//打印
void nextStep();//单步走
void changeDirection(Direction);//修改蛇移动方向
DWORD WINAPI move(LPVOID pram);//蛇移动
DWORD WINAPI dealOp(LPVOID pram);//处理玩家操作
int main()
{
char op;
char prompt[] = "Press anykey to start.\n按任意键开始游戏";
init();
hideCursor();
HANDLE moveThread;
initData();
char mode;
std::cout << "请选择游戏模式:" << "----> 1.不可穿墙 2.可穿墙 <----" << std::endl;
mode = _getch();
while (mode != 49 && mode != 50) {
mode = _getch();
}
if (mode == 50) canCross = true;
paint();
if (!invincible) {
std::cout << prompt;
if (_getch()) {
system("cls");
moveThread = CreateThread(0, 0, move, 0, 0, 0);//线程
}
}
dealOp(NULL);//线程2 map互斥修改
return 0;
}
void moveCursor(int x, int y) {
COORD pos = { x,y };
HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
SetConsoleCursorPosition(hOut, pos);
}
void hideCursor(void) {
CONSOLE_CURSOR_INFO cursor_info = { 1, 0 };
SetConsoleCursorInfo(GetStdHandle(STD_OUTPUT_HANDLE), &cursor_info);
}
void initData() {
delta_time = 500;
score = 0;
direction = Direction::Right;
canCross = false;
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
map[i][j].setLife(0);
map[i][j].setType(BlockType::Blank);
}
}
head_x = (width - 1) / 2;
head_y = (height - 1) / 2;
next_x = head_x;
next_y = head_y;
map[head_y][head_x].setLife(score + 1);
map[head_y][head_x].setType(BlockType::SnakeHead);
createFood();
}
void init() {
system("color 70");
height = MAP_HEIGHT;
width = MAP_WIDTH;
char setWindSize[] = "mode con cols=%d lines=%d";
sprintf_s(setWindSize, setWindSize, width + 2, height + 5);
system(setWindSize);
hDirectionMutex = CreateMutex(0, 0, L"Direction");//方向
for (int i = 0; i < height; i++) {
std::vector<Block> tmp;
for (int j = 0; j < width; j++) {
tmp.push_back(Block());
}
map.push_back(tmp);
}
char title[] = ">>>>>>>>>贪吃蛇<<<<<<<<<";
std::string op1("--------操作方式--------");
std::string op2("W、A、S、D 控制方向,长按加速;P 暂停,Q 退出。");
std::cout << std::string((width - strlen(title)) / 2, ' ') << title << std::endl << std::endl
<< std::string((width - strlen("--------图形说明--------")) / 2, ' ') << "--------图形说明--------" << std::endl
<< std::string((width - strlen("@:蛇头 O:蛇身 * :食物")) / 2, ' ') << "@:蛇头 O:蛇身 * :食物" << std::endl << std::endl
<< std::string((width - op1.size()) / 2, ' ') << op1 << std::endl << std::string((width - op2.size()) / 2, ' ') << op2 << std::endl << std::endl
//<< "游戏规则:每吃掉一次食物,蛇身就增加一节,同时增加积分;随着分数的增加,蛇身移动速度会越来越快。" << std::endl
<< std::string((width - strlen("--------游戏模式--------")) / 2, ' ') << "--------游戏模式--------" << std::endl
<< "本游戏包含两种模式,可穿墙和不可穿墙,可穿墙模式下蛇头碰到边界不会结束游戏,不可穿墙模式下蛇头碰到边界则游戏结束,无论哪个模式,蛇头碰到自己的身体都会结束游戏。"
<< std::endl << std::endl;
}
void createFood() {
unsigned seed = time(0);
srand(seed);
food_x = rand() % MAP_WIDTH;
food_y = rand() % MAP_HEIGHT;
while (map[food_y][food_x].getLife() != 0) {
food_x = rand() % MAP_WIDTH;
food_y = rand() % MAP_HEIGHT;
}
map[food_y][food_x].setLife(1);
map[food_y][food_x].setType(BlockType::Food);
}
void crossWall() {
if (next_x < 0)next_x = width - 1;
if (next_y < 0)next_y = height - 1;
if (next_x >= width) next_x = 0;
if (next_y >= height)next_y = 0;
}
bool checkFail(int next_x, int next_y) {
if (next_x >= 0 && next_x < width && next_y >= 0 && next_y < height) {
if (map[next_y][next_x].getType() == BlockType::SnakeBody)
return true;
else return false;
}
else {
if (!canCross) {
return true; //不可穿墙
}
else {
crossWall();
return false;//可穿墙
}
}
}
void eatFood() {
map[food_y][food_x].lifeDown();
score++;
delta_time -= 10;
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
if (map[i][j].getType() == BlockType::SnakeBody) {
map[i][j].lifeUp();
}
}
}
std::cout << "score: " << score;
}
void paint() {
moveCursor(0, 0);
std::string border(width, '-');
std::cout << '+' << border << '+' << std::endl;
for (int i = 0; i < height; i++) {
std::cout << '|';
for (int j = 0; j < width; j++) {
if (map[i][j].getLife() > 0 && map[i][j].getType() == BlockType::SnakeBody) {
map[i][j].lifeDown();
}
std::cout << map.at(i).at(j).getType();
}
std::cout << '|' << std::endl;
}
std::cout << '+' << border << '+' << std::endl;
}
void nextStep() {
map[head_y][head_x].setType(BlockType::SnakeBody);
head_x = next_x;
head_y = next_y;
if (map[head_y][head_x].getType() == BlockType::Food) {
eatFood();
createFood();
}
map[head_y][head_x].setLife(score + 1);
map[head_y][head_x].setType(BlockType::SnakeHead);
}
DWORD WINAPI move(LPVOID pram) {
while (true) {
Sleep(delta_time);
WaitForSingleObject(hDirectionMutex, INFINITE);
switch (direction) {
case Direction::Up:
next_y -= 1;
break;
case Direction::Down:
next_y += 1;
break;
case Direction::Left:
next_x -= 1;
break;
case Direction::Right:
next_x += 1;
break;
}
if (checkFail(next_x, next_y)) {
system("cls");
std::cout << "游戏结束!你的得分为" << score << std::endl;
break;
}
nextStep();
paint();
ReleaseMutex(hDirectionMutex);
}
return NULL;
}
void changeDirection(Direction next_direction) {
direction = next_direction;
}
DWORD WINAPI dealOp(LPVOID pram) {
while (true) {
if (_kbhit()) {
WaitForSingleObject(hDirectionMutex, INFINITE);
switch (_getch()) {
case 'W': //上
case 'w':
if (direction != Direction::Down) {
changeDirection(Direction::Up);
next_y -= 1;
}
break;
case 'S': //下
case 's':
if (direction != Direction::Up) {
changeDirection(Direction::Down);
next_y += 1;
}
break;
case 'A': //左
case 'a':
if (direction != Direction::Right) {
changeDirection(Direction::Left);
next_x -= 1;
}
break;
case 'D': //右
case 'd':
if (direction != Direction::Left) {
changeDirection(Direction::Right);
next_x += 1;
}
break;
case 'P': //暂停
case 'p':
system("pause");
system("cls");
break;
case 'q': //退出
case 'Q':
return NULL;
}
if (checkFail(next_x, next_y)) {
if (!invincible) {
system("cls");
std::cout << "游戏结束!你的得分为" << score << std::endl;
break;
}
}
nextStep();
paint();
ReleaseMutex(hDirectionMutex);
}
}
return NULL;
}
4.具体实现
(1)初始化界面
声明宏、全局变量height、width,用于保存窗口宽高
#define MAP_WIDTH 50; //宽度50
#define MAP_HEIGHT 25; //高度25
int height, width;//窗口宽高
声明枚举类型BlockType,用@表示蛇头,O表示蛇身,* 表示食物。
enum BlockType {
Blank = ' ', //空白
SnakeBody = 'O',//蛇身
SnakeHead = '@',//蛇头
Food = '*'//食物
};
创建Block地图块类,包含存活周期和地块类型两个属性,定义一个全局的二维数组map,保存游戏场景地图。
class Block {
private:
unsigned short life;
BlockType type;
public:
Block() { life = 0; type = BlockType::Blank; }
~Block() {}
void setLife(int life) {
if (life >= 0)this->life = (unsigned short)life;
if (life == 0) type = BlockType::Blank;
}
unsigned short getLife() { return life; }
void lifeUp() {
life++;
}
void lifeDown() {
if (life > 0)life--;
if (life == 0)type = BlockType::Blank;
}
void setType(BlockType type) {
this->type = type;
}
char getType() {
return (char)type;
}
};
std::vector<std::vector<Block>>map;//游戏场景
在init函数中设置控制台背景颜色、宽高等;对map数组进行初始化,打印游戏菜单。
void init() {
system("color 70");
height = MAP_HEIGHT;
width = MAP_WIDTH;
char setWindSize[] = "mode con cols=%d lines=%d";
sprintf_s(setWindSize, setWindSize, width + 2, height + 5);
system(setWindSize);
hDirectionMutex = CreateMutex(0, 0, L"Direction");//方向
for (int i = 0; i < height; i++) {
std::vector<Block> tmp;
for (int j = 0; j < width; j++) {
tmp.push_back(Block());
}
map.push_back(tmp);
}
char title[] = ">>>>>>>>>贪吃蛇<<<<<<<<<";
std::string op1("--------操作方式--------");
std::string op2("W、A、S、D 控制方向,长按加速;P 暂停,Q 退出。");
std::cout << std::string((width - strlen(title)) / 2, ' ') << title << std::endl << std::endl
<< std::string((width - strlen("--------图形说明--------")) / 2, ' ') << "--------图形说明--------" << std::endl
<< std::string((width - strlen("@:蛇头 O:蛇身 * :食物")) / 2, ' ') << "@:蛇头 O:蛇身 * :食物" << std::endl << std::endl
<< std::string((width - op1.size()) / 2, ' ') << op1 << std::endl << std::string((width - op2.size()) / 2, ' ') << op2 << std::endl << std::endl
//<< "游戏规则:每吃掉一次食物,蛇身就增加一节,同时增加积分;随着分数的增加,蛇身移动速度会越来越快。" << std::endl
<< std::string((width - strlen("--------游戏模式--------")) / 2, ' ') << "--------游戏模式--------" << std::endl
<< "本游戏包含两种模式,可穿墙和不可穿墙,可穿墙模式下蛇头碰到边界不会结束游戏,不可穿墙模式下蛇头碰到边界则游戏结束,无论哪个模式,蛇头碰到自己的身体都会结束游戏。"
<< std::endl << std::endl;
}
(2)初始化数据
声明枚举Direction表示蛇移动方向,Up表示上,Down表示下,Left为左,Right为右
enum Direction { //蛇移动方向
Up,
Down,
Left,
Right
};
声明全局数据:delta_time用于保存蛇的移动间隔时间(ms),score用于保存得分/蛇身长度,direction保存当前移动方向,food_x,food_y为食物坐标,head_x,head_y为蛇头坐标,next_x,next_y为下一步的坐标,canCross是能否穿墙的标记。
Direction direction;//移动方向
int score;//得分
int delta_time;//时间间隔
int food_x, food_y;//食物坐标
int head_x, head_y;//蛇头坐标
int next_x, next_y;//蛇头下一步坐标
bool canCross;//能否穿墙
初始时创建蛇头和食物,蛇头初始位置在地图中心,默认移动方向向右。
void initData() {
delta_time = 500;
score = 0;
direction = Direction::Right;
canCross = false;
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
map[i][j].setLife(0);
map[i][j].setType(BlockType::Blank);
}
}
head_x = (width - 1) / 2;
head_y = (height - 1) / 2;
next_x = head_x;
next_y = head_y;
map[head_y][head_x].setLife(score + 1);
map[head_y][head_x].setType(BlockType::SnakeHead);
createFood();
}
(3)随机生成食物
在非边界、非蛇头、蛇身的地图块中生成食物,食物的存活期设为1。
void createFood() {
unsigned seed = time(0);
srand(seed);
food_x = rand() % MAP_WIDTH;
food_y = rand() % MAP_HEIGHT;
while (map[food_y][food_x].getLife() != 0) {
food_x = rand() % MAP_WIDTH;
food_y = rand() % MAP_HEIGHT;
}
map[food_y][food_x].setLife(1);
map[food_y][food_x].setType(BlockType::Food);
}
(4)打印游戏界面**
遍历二维数组,根据数组元素存储的life和blockType,打印相应的符号,边界符号不占用width、height宽高。每次遍历map时,会将所有snakeBody蛇身类型的地块存活期减1,当减到0时会退化为空白地块,从而实现视觉上的蛇身移动的效果。
void paint() {
moveCursor(0, 0);
std::string border(width, '-');
std::cout << '+' << border << '+' << std::endl;
for (int i = 0; i < height; i++) {
std::cout << '|';
for (int j = 0; j < width; j++) {
if (map[i][j].getLife() > 0 && map[i][j].getType() == BlockType::SnakeBody) {
map[i][j].lifeDown();
}
std::cout << map.at(i).at(j).getType();
}
std::cout << '|' << std::endl;
}
std::cout << '+' << border << '+' << std::endl;
}
(5)吃掉食物
每次吃掉食物后,要将食物块改为蛇身类型,存活期设置为score。
void eatFood() {
map[food_y][food_x].lifeDown();
score++;
delta_time -= 10;
for (int i = 0; i < height; i++) {
for (int j = 0; j < width; j++) {
if (map[i][j].getType() == BlockType::SnakeBody) {
map[i][j].lifeUp();
}
}
}
std::cout << "score: " << score;
}
(6)单步移动
根据方向和蛇头坐标,确定蛇下一步的坐标,将下一步的地图块设置为蛇头类型,将当前蛇头所在地块设为蛇身类型。
void nextStep() {
map[head_y][head_x].setType(BlockType::SnakeBody);
head_x = next_x;
head_y = next_y;
if (map[head_y][head_x].getType() == BlockType::Food) {
eatFood();
createFood();
}
map[head_y][head_x].setLife(score + 1);
map[head_y][head_x].setType(BlockType::SnakeHead);
}
(7)实现穿墙
当蛇头x坐标小于0时,将下一步蛇头x坐标设为地图宽度width-1,当蛇头y坐标小于0时,将下一步蛇头y坐标设为地图高度height-1;当蛇头x坐标大于等于地图宽度width时,将下一步蛇头x坐标设为0,当蛇头y坐标大于地图高度height-1时,将下一步蛇头的y坐标设为0,蛇的移动方向保持不变,这样就实现了穿墙功能。
void crossWall() {
if (next_x < 0)next_x = width - 1;
if (next_y < 0)next_y = height - 1;
if (next_x >= width) next_x = 0;
if (next_y >= height)next_y = 0;
}
(8)失败检测
当处于不可穿墙模式时,蛇头碰到边界或者蛇身,游戏就会失败;当游戏处于可穿墙模式时,蛇头只有碰到蛇身时才会失败。
bool checkFail(int next_x, int next_y) {
if (next_x >= 0 && next_x < width && next_y >= 0 && next_y < height) {
if (map[next_y][next_x].getType() == BlockType::SnakeBody)
return true;
else return false;
}
else {
if (!canCross) {
return true; //不可穿墙
}
else {
crossWall();
return false;//可穿墙
}
}
}
(9)游戏主循环
1)蛇持续移动
每隔delta_time时间就根据蛇移动方向,调用单步移动、失败检测,打印游戏界面等函数。
DWORD WINAPI move(LPVOID pram) {
while (true) {
Sleep(delta_time);
WaitForSingleObject(hDirectionMutex, INFINITE);
switch (direction) {
case Direction::Up:
next_y -= 1;
break;
case Direction::Down:
next_y += 1;
break;
case Direction::Left:
next_x -= 1;
break;
case Direction::Right:
next_x += 1;
break;
}
if (checkFail(next_x, next_y)) {
system("cls");
std::cout << "游戏结束!你的得分为" << score << std::endl;
break;
}
nextStep();
paint();
ReleaseMutex(hDirectionMutex);
}
return NULL;
}
2)响应用户操作
当用户按下键盘时,根据按下的键进一步操作,改变蛇移动方向direction后,立即执行失败检测、单步走、打印游戏界面的函数,使游戏更加跟手,实现长按加速的效果。
void changeDirection(Direction next_direction) {
direction = next_direction;
}
DWORD WINAPI dealOp(LPVOID pram) {
while (true) {
if (_kbhit()) {
WaitForSingleObject(hDirectionMutex, INFINITE);
switch (_getch()) {
case 'W': //上
case 'w':
if (direction != Direction::Down) {
changeDirection(Direction::Up);
next_y -= 1;
}
break;
case 'S': //下
case 's':
if (direction != Direction::Up) {
changeDirection(Direction::Down);
next_y += 1;
}
break;
case 'A': //左
case 'a':
if (direction != Direction::Right) {
changeDirection(Direction::Left);
next_x -= 1;
}
break;
case 'D': //右
case 'd':
if (direction != Direction::Left) {
changeDirection(Direction::Right);
next_x += 1;
}
break;
case 'P': //暂停
case 'p':
system("pause");
system("cls");
break;
case 'q': //退出
case 'Q':
return NULL;
}
if (checkFail(next_x, next_y)) {
if (!invincible) {
system("cls");
std::cout << "游戏结束!你的得分为" << score << std::endl;
break;
}
}
nextStep();
paint();
ReleaseMutex(hDirectionMutex);
}
}
5.总结
本文采用二维数组保存游戏地图,每个地图块是自定义的Block类,其中包含两个属性:存活期life和块符号/块类型blockType,每次打印游戏地图都会遍历二维数组,并将蛇身块的life减一,life为0时自动退化为空白块;蛇每次前进一格,就将下一空白块设为蛇头,life设为最大存活期;此外,每次蛇头吃掉食物后,分数就加一,食物块变为新的蛇头,蛇身块最大life随分数增加而增加,循环这个过程,游戏界面打印后的显示效果上看就是蛇不断移动,吃了食物后蛇身变长的效果。