手把手教你用C++写贪吃蛇

最近女朋友挺无聊的(不是左手,也不是右手),决定用C++写一个贪吃蛇的游戏给他玩玩。顺便更新一下博客,把思路记下来。

相信看这篇博客的人都用过诺基亚的手机,玩过贪吃蛇(不接受反驳)。贪吃蛇的玩法就是一条小蛇在转圈圈儿,同时要去吃食物,每吃一个,自身增加1节长度,直到撞到自己或者撞到墙才算失败。游戏规则是非常的简单。相信各位早就看懂了,那接下来就开始敲代码吧:
在这里插入图片描述
首先在这个游戏中有蛇,有食物还有墙。因此我们需要设计三个类来依次表示他们。

首先我们先在excel中模拟一下贪吃蛇的游戏过程如下:
在这里插入图片描述
我们可以假定用*表示墙,用=表示蛇身,用@表示蛇头,用#表示食物。因此,整个过程我们可以用一个二维数组来维护整个蛇,墙以及食物的状态。先建立一个墙的类,内容如下:

#pragma once
#include<iostream>

class Wall {
public:
	enum
	{
		ROW = 25, COLUMN = 25
	};

	//构造函数
	Wall();

	//根据条件设置点的元素,可能为食物,可能为蛇身可能为蛇头
	void setPoint(int x, int y, char ele);

	//获取某一坐标的元素,用于给需要移动到的下一点判断是否会装上
	char getPoint(int x, int y)const;

	//在屏幕上输出当前状态
	void drawWall();

	//重载绘制方法,此时传入一个参数为得分,并写在第一行
	void drawWall(unsigned int score);

private:
	char gameArr[ROW][COLUMN];  //维护一个二维数组
};

						wall.h

然后对墙这个类里面的基本方法进行实现,内容如下:

#include"Wall.h"

//构造函数,先把墙搭建好,即i=0或i=ROW - 1或j=0或j=COLUMN - 1的地方就为墙用'*'表示,其他地方为待使用区域用' '表示
Wall::Wall() {
	for (int i = 0; i < ROW; ++i)
		for (int j = 0; j < COLUMN; ++j)
			if (i == 0 || j == 0 || i == ROW - 1 || j == COLUMN - 1)
				this->gameArr[i][j] = '*';
			else
				this->gameArr[i][j] = ' ';
}

//根据传入的元素,设置点位的状态,可能是食物'#',也可能是蛇头'@'或蛇身'='
void Wall::setPoint(int x, int y, char ele) {
	this->gameArr[x][y] = ele;
}

//返回点位的元素,用来判断是否会撞上,或者是否吃到了食物
char Wall::getPoint(int x, int y)const {
	return this->gameArr[x][y];
}

//将二维数组中维护的状态输出在屏幕上,同时在右侧加入开发者,玩法等信息
void Wall::drawWall() {
	for (int i = 0; i < ROW; ++i) {
		for (int j = 0; j < COLUMN; j++)
			std::cout << this->gameArr[i][j] << " ";
		if (i == 6)
			std::cout << "开发者: 方人也WJ";
		if (i == 8)
			std::cout << "玩法——a: left | w: up | s:down | d: right";
		std::cout << std::endl;
	}
}

//重载的输出信息,并且通过传入的分数参数计算当前的得分
void Wall::drawWall(unsigned int score) {
	for (int i = 0; i < ROW; ++i) {
		for (int j = 0; j < COLUMN; j++)
			std::cout << this->gameArr[i][j] << " ";
		if (i == 1)
			std::cout << "当前得分:" << (score - 3) * 10; //要减去一开始的时候蛇就有3段即减掉30分
		if (i == 6)
			std::cout << "开发者: 方人也WJ";
		if (i == 8)
			std::cout << "玩法——a: left | w: up | s:down | d: right";
		std::cout << std::endl;
	}
}
					Wall.cpp

整个墙实现了之后基本框架就搭好了,演示图如下:
在这里插入图片描述
接下来就是构造蛇的类了,在初始化一条蛇时需要考虑到给它一定的长度(假设是三段即”==@“),接下来就是把蛇放到维护数据的二维数组中,因此在蛇这个类中需要保存一个wall的引用,用以修改二维数组中的数据。

此外,蛇本身的移动即为对二维数组中内容的修改,假设蛇一开始在([5,4] (蛇身),[5,5] (蛇身),[5,6] (蛇头)),现在蛇向右移动后就会变成([5,5] (蛇身),[5,6] (蛇身),[5,7] (蛇头)),观察这两组数据可以发现,当蛇移动时,只需要把蛇尾剔除,把原来的蛇头变成蛇身,把移动的地方变成蛇头就可以了。因此可采用一个list来维护蛇,在这个list中,每次移动时就把蛇尾pop掉(即pop_front),同时在list末端添加一个蛇头(push_back)。同理,当吃到了一个食物时,就直接在list末尾push一个蛇头即可。因此蛇类设计如下:

#pragma once
#include<list>
#include<algorithm>
#include "Wall.h"
#include "Food.h"

class Snake {
public:
	enum {
		LEFT = 'a',
		RIGHT = 'd',
		UP = 'w',
		DOWN = 's'
	};
	//构造函数,初始化时需要将wall对象传入,构造出的蛇需要存入wall中的二维数组
	Snake(Wall& wall);

	//重新写一个构造函数,需要将food类传入作为内部类
	Snake(Wall& wall, Food& food);

	//弹出当前的蛇尾
	void popSnakeTail();

	//插入新蛇头的位置,传入参数为新蛇头的坐标
	void insertSnakeHead(int x, int y);

	//通过wasd操纵移动蛇,传入的参数为蛇的移动方向,如果可以成功移动,则返回true,否则false
	bool moveSnake(char direction);

	//获取得分接口,用来提供得分
	unsigned int getScore()const;

private:
	Wall& wall;
	//维护一个蛇列表,其中蛇身或蛇头的坐标用pair形式保存
	std::list<std::pair<int, int>> snakeList;

	Food& food;
};
				Snake.h

相应的接口实现如下:

#include "Snake.h"
#include<stdexcept>


//初始化蛇,假设一开始蛇为3段,2段蛇身,1段蛇头
Snake::Snake(Wall& wall, Food& food) : wall(wall), food(food) {
	this->snakeList.push_back(std::make_pair<int, int>(5, 4)); //蛇尾
	this->snakeList.push_back(std::make_pair<int, int>(5, 5)); //蛇身
	this->snakeList.push_back(std::make_pair<int, int>(5, 6)); //蛇头
	this->wall.setPoint(5, 4, '=');
	this->wall.setPoint(5, 5, '=');
	this->wall.setPoint(5, 6, '@');
}

//弹出蛇尾
void Snake::popSnakeTail() {
	std::pair<int, int> snakeTail = this->snakeList.front(); //记录下蛇尾的坐标,用于清除墙中二维数组中的当前蛇尾
	this->snakeList.pop_front();
	this->wall.setPoint(snakeTail.first, snakeTail.second, ' '); //重新写入空格
}

//插入蛇头,再插入蛇头之前,需要将原蛇头位置变为蛇身
void Snake::insertSnakeHead(int x, int y) {
	std::pair<int, int> snakeHead = this->snakeList.back();
	this->wall.setPoint(snakeHead.first, snakeHead.second, '=');  //更新为蛇身
	this->snakeList.push_back(std::pair<int, int>(x, y));  //将新的蛇头坐标保存
	this->wall.setPoint(x, y, '@');
}

bool Snake::moveSnake(char direction) {
	std::pair<int, int> snakeHead = this->snakeList.back(); //先取出蛇头的坐标
	int headX = snakeHead.first;
	int headY = snakeHead.second;
	switch (direction)  //根据方向来判断需要如何修改
	{
	case UP:  //如果向上移动了,那么新的蛇头坐标应该为--headX, headY
		--headX;
		break;
	case DOWN:
		++headX; //向下移动,则新蛇头坐标为++headX, headY
		break;
	case LEFT:
		--headY;
		break;
	case RIGHT:
		++headY;
		break;
	default:
		break;
	}
	//接下来就需要能否成功移动一步
	char space = this->wall.getPoint(headX, headY);
	if (space == ' ') {  //如果要移动到的点为空格,则该空间无任何东西,可正常移动,则需要掐尾改头
		this->popSnakeTail();
		this->insertSnakeHead(headX, headY);
		return true;
	}

	//再加一个判断,如果此时蛇尾正好需要移动,同时蛇头会在这个位置出现,并不会死亡
	/*
	       =
		   @  =   在这种情况下,蛇头往上,蛇尾向右,这种情况不会死亡,但是程序会判定为死亡,因为先撞上,再修改
		   =
	*/
	else if (space == '=') {  //如果下一个要走的点还是蛇身
		this->popSnakeTail();  //先将末尾弹出,再判断,要走的位置是否位空,如果还不是空,则是撞上了
		space = this->wall.getPoint(headX, headY);
		if (space == ' ') {
			this->insertSnakeHead(headX, headY);
			return true;
		}
		else {
			this->insertSnakeHead(headX, headY);
			return false;
		}
		
	}

	else if (space == '#') { //如果是食物,那么蛇身需要加长1,因此不需要再将蛇尾弹出,只需改掉头即可
		this->insertSnakeHead(headX, headY);
		//食物被吃掉之后需要生成新的食物
		this->food.generateFood();
		return true;
	}
	else {  //否则撞墙了或撞到蛇身了
		this->popSnakeTail();
		this->insertSnakeHead(headX, headY);
		return false;
	}
}

//以蛇的长度代替得分,每增加一段即吃掉一个食物,则增加10分
unsigned int Snake::getScore()const {
	return this->snakeList.size();  //每增加一段就加10分并在绘制时输出
}
						Snake.cpp

到目前为止,蛇的类基本已经实现,可以先看一下效果图:
在这里插入图片描述
现在还差一个食物类需要去实现,食物类非常简单,只需要提供一个依靠随机数随机生成一个食物的方法即可,这里不做过多的赘述,直接上代码:

#pragma once
#include<math.h>
#include "Wall.h"


class Food {
public:

	//构造时需要将墙传入,因为每当生成一个食物时,需要更新到墙中
	Food(Wall& wall);

	//生成食物
	void generateFood();
private:
	Wall& wall;
};
					Food.h

上面的generateFood即实现了食物的更新,具体实现方法如下:

#include "Food.h"

Food::Food(Wall& wall) : wall(wall) {}

void Food::generateFood() {
	int foodX = 0;
	int foodY = 0;
	while (true) {
		foodX = rand() % Wall::COLUMN + 1; //食物的x坐标限制在墙内
		foodY = rand() % Wall::ROW + 1; //同理,食物的y坐标也应限制在墙内

		if (this->wall.getPoint(foodX, foodY) == ' ') {  // 只有食物的坐标处无任何东西时,食物生成才算成功,否则重新生成
			this->wall.setPoint(foodX, foodY, '#');
			break;
		}
	}
}
					Food.h

到目前为止,整个贪吃蛇游戏基本就可以跑起来了。静态图如下:
在这里插入图片描述
整个贪吃蛇的所有要素都构建完成了,现在唯一需要的就是能够通过用户按键使得整个蛇跑起来,同时得分能够更新起来。最好还可以设置难度使蛇越跑越快。为了实现上述功能,可以再增加一个玩家类,用来实现上述过程:

#pragma once
#include<conio.h>
#include<Windows.h>
#include "Snake.h"

//引入玩家类
class Player {
public:
	//snake用于移动蛇,wall用于更新画面
	Player(Snake& snake, Wall& wall);

	//玩起来
	void play();
private:
	Snake& snake;
	Wall& wall;
};
						Player.h

接下来最主要的就是实现上面的play接口了:

#include "Player.h"

Player::Player(Snake& snake, Wall& wall) : snake(snake), wall(wall) {}


void Player::play() {
	//判断蛇是否还存活,即游戏是否要结束游戏
	bool isAlive = true;

	//放置一个激活变量,判断游戏是否激活,只有第一次按键为wsd中的一个则激活游戏
	bool isActive = false; 

	//用来记录上一次的按键,如果上一次按键和本次按键正好相反(如上一次d,这一次是a,则认为蛇需要旋转180°,然而这是不可能实现的。 
	//并且刚开始激活游戏时不能按a,否则开局就死
	char preKey = Snake::RIGHT; 

	while (true) {
		char ch = _getch();  //通过键盘获取一个按键值
		do {
			if (ch != Snake::LEFT && ch != Snake::UP && ch != Snake::RIGHT && ch != Snake::DOWN && !isActive)  //如果玩家按的不是asdw键则直接忽略
				break;

			else if (ch != Snake::LEFT && ch != Snake::UP && ch != Snake::RIGHT && ch != Snake::DOWN && isActive)  //如果激活了,但是按键不是wasd其中一个,则认为按键无效
				ch = preKey; //将之前的按键赋值给当前按键

			if (preKey == Snake::RIGHT && ch == Snake::LEFT ||
				preKey == Snake::LEFT && ch == Snake::RIGHT ||
				preKey == Snake::DOWN && ch == Snake::UP ||
				preKey == Snake::UP && ch == Snake::DOWN)
				ch = preKey;  //如果按了反方向,则认为没有按。

			isActive = true; //游戏已激活
			isAlive = snake.moveSnake(ch); //如果游戏失败则游戏终止
			system("cls");  //清屏用于下一次绘制
			wall.drawWall(snake.getScore());  //重新绘制当前状态
			if (!isAlive) {
				std::cout << "GAME OVER!!!" << std::endl;
				break;
			}

			//设置难度,根据蛇的长度来设置难度
			unsigned int difficultLevel = this->snake.getScore();

			if (difficultLevel < 8)  //如果蛇小于8段,则两帧画面刷新时间为800ms
				Sleep(800);
			else if (difficultLevel < 15 && difficultLevel >= 8) //如果蛇小于15段但大于8段则刷新时间为500ms
				Sleep(500);
	
			else if (difficultLevel < 20 && difficultLevel >= 15) //继续加快速度
				Sleep(200);
			
			else if (difficultLevel < 30 && difficultLevel >= 20)
				Sleep(100);
			
			else
				Sleep(50);
			
			preKey = ch;  //记住当前的按键,用于和下一次的按键作对比
		} while (!_kbhit()); //如果没有按键,则认为和上次按键一样
		if (!isAlive)
			break;
	}
}
					Player.cpp

到目前为止,整个贪吃蛇代码就开发完了,接下来就是要初始化一个测试用户了。假设现在有个叫王二蛋的人想玩这个贪吃蛇游戏,因此只需要在main函数中初始化一个叫作王二蛋的变量即可,如下:

#include <iostream>
#include <ctime>
#include "Wall.h"
#include "Snake.h"
#include "Food.h"
#include "Player.h"

int main()
{
    srand(time(NULL)); //随机数种子
    Wall wall;  //初始化一个墙对象
    Food food(wall); //初始化一个食物对象
    Snake snake(wall, food);  //初始化一个蛇对象
    Player wangErdan(snake, wall);  //玩家王二蛋闪亮登场
    food.generateFood();  //第一次玩,需要有一个初始化的食物
    wall.drawWall(snake.getScore());  //绘制第一屏,此时蛇并未激活
    wangErdan.play();  //王二蛋开始玩游戏了
    return 0;
}
					main.cpp

由于没找到实(免)用(费)的录屏软件,这里就不放动态效果图了,感兴趣的小伙伴可以尝试着让蛇转悠起来。
在这里插入图片描述

猜你喜欢

转载自blog.csdn.net/WJ_SHI/article/details/107347142