C++实现童年经典游戏—贪吃蛇(可穿墙)

文章详细介绍了如何使用C++编程语言实现一个经典的贪吃蛇游戏,包括初始化界面、数据、食物生成、打印游戏界面、吃食物、单步移动、穿墙功能、失败检测以及游戏主循环等关键步骤。游戏有两种模式:可穿墙和不可穿墙,增加了游戏的趣味性和挑战性。
摘要由CSDN通过智能技术生成

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随分数增加而增加,循环这个过程,游戏界面打印后的显示效果上看就是蛇不断移动,吃了食物后蛇身变长的效果。
在这里插入图片描述
请添加图片描述

  • 2
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值