使用C++设计贪食蛇小游戏

说明:所有代码均可在Visual Studio 2013上编译执行。并未测试在其它编译器上编译情况。

游戏规则

        贪食蛇游戏要求玩家控制方向键(或WSAD键)来控制小蛇的前进方向,以使蛇吃掉面板上随即位置上的食物。每次成功吃掉食物后小蛇体长将增加一点,得分增加。当小蛇撞到边界或者蛇头与蛇身相撞时,蛇将挂掉,游戏随之结束。

游戏结构设计

        游戏应当包含初始欢迎界面,游戏界面,游戏结束界面。

        建立一个CGame类,来管理游戏的进度。该类放在Game.h文件中,在该类中可以分别定义NewGame(),PlayGame(),SetGame()和ExitGame()四个函数来控制游戏的各个单元,为了使整个程序看起来更像个游戏,可以采取更加漂亮的界面来展示游戏各部分。

  1. NewGame()函数设定游戏欢迎界面。可以简单地输出了一些方块字符组成的游戏名SNAKE和一句提示“Press any key to start…”。点击任意键后,游戏程序将转入SetGame()中继续执行。可以加上一些动态效果,让提示”Press any keyto start…”不断跳动。
  2. SetGame()中包括游戏的设置内容。可以选择Easy,Normal,Hard三个选项。这三个选项将对应小蛇不同的的移动速度,具体来说将体现在PlayGame()函数中每次循环执行速度。设置完成后,游戏程序将转入PlayGame()继续执行。
  3. PlayGame()函数主体将是一个死循环,因为可将游戏考虑成一个无穷的循环,循环中迭代的每一步都依次进行:判断用户是否输入、然后根据用户输入调整游戏内容(如果没有输入则按默认方式继续执行游戏)、判断是否符合规则(不符合则跳出循环,转入ExitGame()退出游戏)、判断是否需要加分扣分。执行完以上这些步骤后,将进行下一次迭代。当然进行游戏之前,还要执行必要的初始化工作,来显示大体框架和提示信息。
  4. EitGame()中将显示游戏得分,并询问玩家是否再玩一次。这里拼出了一个骷髅头的图案,表示Game Over。

        以上为游戏的主体内容,这四个函数设定了游戏的基本结构,剩余部分将继续考虑细节问题。然后再展示Game.h的细节内容。

建立游戏对象

        先建立一系列类表示游戏对象,其中应包括对游戏对象的处理方式(函数)。分析游戏,可以知道游戏主体是小蛇和食物。

        所有的游戏对象,包括蛇和食物,都是由控制台上的一系列点组成的。因此需要很多处理点对象的方法。可建立Point.h来定义CPoint对象,来简化其他对象的处理。

        Point.h文件内容如下:

#pragma once
#include
#include
using std::cout;
using std::cin;

class CPoint
{
public:
	CPoint(){}
	CPoint(int x, int y)
	{
		this->x = x;
		this->y = y;
	}
	void Plot(HANDLE hOut)
	{
		SetOutputPosition(x, y,hOut);
		cout << "■";
	}
	void Clear(HANDLE hOut)
	{
		SetOutputPosition(x, y, hOut);
		cout << "  ";
	}
	void Clear()
	{
		HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
		cout << " ";
	}
	//静态方法,用于设定输出点在控制台的位置
	static void SetOutputPosition(int x, int y, HANDLE hOut)
	{
		COORD position;
		position.X = x;
		position.Y = y;
		SetConsoleCursorPosition(hOut, position);
	}
	bool operator == (CPoint& point)
	{
		return (point.x==this->x) && (point.y==this->y);
	}
	//改变CPoint对象的位置
	void SetPosition(int x, int y)
	{
		this->x = x;
		this->y = y;
	}
	int GetX(){ return x; };
	int GetY(){ return y; };
private:
	int x, y;
};

        蛇和食物都是由控制台面板上的一系列点来表示的。所以这决定了小蛇和食物的类数据成员将用来存储这些点集的具体信息。可以建立一个文件snake.h和food.h来保存建立的类。

        蛇是由一系列点组成,可以用简单的数组,链表等来存储这些点。考虑一下蛇运动的方式:蛇整体前进一步,可以考虑为蛇头根据输入方向移动一格,然后剩余所有点都移动到它前一个点之前所在的位置上;也可以表示为在蛇头前面根据用户输入方向添加一个点,蛇尾部移除一个点。前者需要处理构成蛇的所有点,后者只需要处理蛇头和蛇尾两个点。很明显,后者效率更高。这里采用第二个移动方案,并相应地采取双端队列来存储点的数据。

        方便起见,可以使用C++ STL(Standard Template Library)中定义的容器类(双端队列)deque来存储点集,使用push_front()和pop_back()来处理蛇的移动过程。

        Snake.h如下所示:

#pragma once
#include
#include "Point.h"
#include
#include "Point.h"
#include "Food.h"
using std::cout;
using std::cin;
using std::deque;

class CSnake
{
public:
	enum moveDirection{ UP, LEFT ,DOWN,RIGHT };	//蛇只有四个移动方向,所以可以定义了一个蛇运动方向的枚举类型
	CSnake()		//蛇初始时将只由两个点元素组成,初始移动方向设定为向右
	{
		snake.push_back(CPoint(18, 16));
		snake.push_back(CPoint(16, 16));
		direction = moveDirection::RIGHT;
	}
	//蛇整体向前移动一步,即更新点集snake
	void move()
	{
		switch (direction)//根据此时的方向来判断该如何移动。移动时为提高效率将只需要处理首尾点元素
		{
		case moveDirection::DOWN:
			snake.push_front(CPoint(snake.begin()->GetX(), snake.begin()->GetY() + 1)); break;
		case moveDirection::LEFT:
			snake.push_front(CPoint(snake.begin()->GetX() - 2, snake.begin()->GetY())); break;
		case moveDirection::RIGHT:
			snake.push_front(CPoint(snake.begin()->GetX() + 2, snake.begin()->GetY())); break;
		case moveDirection::UP:
			snake.push_front(CPoint(snake.begin()->GetX(), snake.begin()->GetY() - 1)); break;
		}
		snake.pop_back();
	}
	bool ChangeDirection(moveDirection direction)
	{
		if ((direction + 2) % 4 == this->direction)	//玩家输入方向与蛇当前移动方向相反将不改变此时蛇的前进方向
			return false;
		else
			this->direction = direction;
		return true;
	}
	//把蛇整个地画出来。为提高效率,该函数应该只在游戏初始化时调用
	void PaintSnake(HANDLE hOut)
	{
		for (CPoint& point : snake)
		{
			CPoint::SetOutputPosition(point.GetX(), point.GetY(), hOut);
			point.Plot(hOut);
		}
	}
	//检查蛇头是否和蛇身部分重合,用于判断是否犯规
	bool HitItself()
	{
		for (CPoint& point : snake)
		{
			if (point == *snake.begin())	
			{
				if (&point == &(*snake.begin()))	//将忽略蛇头与蛇头重合的情况。
					continue;
				else
					return true;
			}
		}
		return false;
	}
	//检查某点是否和蛇身重合,判断随机产生的食物是否放到了蛇身上。
	bool Hit(CPoint& point)
	{
		for (CPoint& pointInSnake : snake)
		{
			if (point == pointInSnake)
				return true;
		}
		return false;
	}
	//检查蛇头是否撞墙,用于判断是否犯规
	bool HitEdge()
	{
		int x = snake.begin()->GetX();
		int y = snake.begin()->GetY();
		if ((x == 0) || (y == 2) || (x == 78) || (y == 24))	//和默认墙的位置进行比较
			return true;
		else
			return false;
	}
	//经常需要处理蛇头和蛇尾,所以定义了这两个函数。
	CPoint& Head()
	{
		return *snake.begin();
	}
	CPoint& Tail()
	{
		return *(snake.end()-1);
	}
	bool Eat(CFood& food)
	{
		//复杂的判断语句,用于判断食物是否在蛇头移动方向上的前方
		int foodx = food.GetPosition().GetX();
		int foody = food.GetPosition().GetY();
		int headx = Head().GetX();
		int heady = Head().GetY();
		bool toEat = ((foodx == headx) && (foody == (heady + 1)) && (direction == moveDirection::DOWN))
			|| ((foodx == headx) && (foody == (heady - 1)) && (direction == moveDirection::UP))
			|| (((foodx + 2) == headx) && (foody == heady) && (direction == moveDirection::LEFT))
			|| (((foodx - 2) == headx) && (foody == heady) && (direction == moveDirection::RIGHT));
		if(toEat)
		{
			snake.push_front(food.GetPosition());
			return true;
		}
		else
			return false;
	}
private:
	deque snake;
	moveDirection direction;
};

        Food.h相比之下就简单得多。用”$$”来在控制台上显示食物。不用“$”的原因是蛇由“”组成,而“”在控制台上在一行上占了两个普通字符的位置(无奈)。Food.h如下所示:

#pragma once
#include
#include "Point.h"
using std::cout;
class CFood
{
public:
	CFood()
	{
		position.SetPosition(20, 20);
	}

	CFood(int x, int y)
	{
		position.SetPosition(x, y);
	}

	void PlaceFood(int x, int y)
	{
		position.SetPosition(x, y);
		HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
		SetConsoleTextAttribute(hOut, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_INTENSITY);
		CPoint::SetOutputPosition(x, y, hOut);
		cout << "$$";
	}

	CPoint& GetPosition()
	{
		return position;
	}

	void Show()
	{
		cout << '(' << position.GetX() << ',' << position.GetY() << ')';
	}
private:
	CPoint position;
};

        下面是Game.h的所有内容。由于要输出很多图案,Game.h略显臃肿:

#pragma once
#include
#include "Snake.h"
#include "Food.h"
#include "Point.h"
#include "conio.h"
#include
#include
using std::endl;
class CSnake;
class CGame
{
public:
	CGame()
	{
		length =80;
		width = 24;
		score = 0;
		exit = false;
	}
	bool Exit(){ return exit; };
	void NewGame()
	{
		//SMALL_RECT rc = { 0, 0, 80 - 1, 25 - 1 }; // 重置窗口位置和大小
		system("cls");
		HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
		SetConsoleTextAttribute(hOut, FOREGROUND_GREEN|FOREGROUND_RED);
		//system("color 81");
		COORD position = { 17, 6 };
		SetConsoleCursorPosition(hOut, position);

		//设置输出“SNAKE”
		SetConsoleTextAttribute(hOut,  FOREGROUND_GREEN | FOREGROUND_INTENSITY);
		
		cout << " ■■■   ■     ■  ■■■  ■    ■ ■■■■";
		SetConsoleTextAttribute(hOut, FOREGROUND_GREEN);
		CPoint::SetOutputPosition(17, 7, hOut);
		cout << "■    ■  ■■   ■ ■    ■ ■   ■  ■";
		CPoint::SetOutputPosition(17, 8, hOut);
		SetConsoleTextAttribute(hOut, FOREGROUND_RED|FOREGROUND_INTENSITY);
		cout << "■        ■ ■  ■ ■    ■ ■  ■   ■";
		CPoint::SetOutputPosition(17, 9, hOut);
		SetConsoleTextAttribute(hOut, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_INTENSITY);
		cout << " ■■■   ■  ■ ■ ■■■■ ■■     ■■■";
		CPoint::SetOutputPosition(17, 10, hOut);
		SetConsoleTextAttribute(hOut, FOREGROUND_BLUE|FOREGROUND_RED|FOREGROUND_INTENSITY);
		cout << "      ■  ■   ■■ ■    ■ ■  ■   ■";
		CPoint::SetOutputPosition(17, 11, hOut);
		SetConsoleTextAttribute(hOut, FOREGROUND_GREEN|FOREGROUND_RED|FOREGROUND_INTENSITY);
		cout << "■    ■  ■     ■ ■    ■ ■   ■  ■";
		CPoint::SetOutputPosition(17, 12, hOut);
		SetConsoleTextAttribute(hOut, FOREGROUND_BLUE|FOREGROUND_INTENSITY);
		cout << " ■■■   ■     ■ ■    ■ ■    ■ ■■■■";

		while (true)
		{
			CPoint::SetOutputPosition(27, 20, hOut);
			cout << "Press any key to start...";

			Sleep(70);
			for (int i = 0; i < 25; i++)
				cout << "\b \b";
			Sleep(70);
			if (_kbhit())
				break;
		}
		cin.get();
	}
	void SetGame()
	{
		char chooseLevel;
		system("cls");
		HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
		SetConsoleTextAttribute(hOut, FOREGROUND_GREEN | FOREGROUND_INTENSITY);
		cout << " ■■■  ■■■■ ■■■ ■■■  ■■  ■     ■  ■■■   ■■■ "<< endl;
		SetConsoleTextAttribute(hOut, FOREGROUND_GREEN | FOREGROUND_INTENSITY);
		cout << "■    ■ ■         ■     ■     ■   ■■   ■ ■    ■ ■    ■"<< endl;
		SetConsoleTextAttribute(hOut, FOREGROUND_RED | FOREGROUND_INTENSITY);
		cout << "■       ■         ■     ■     ■   ■ ■  ■ ■       ■     "<< endl;
		SetConsoleTextAttribute(hOut, FOREGROUND_BLUE | FOREGROUND_GREEN | FOREGROUND_INTENSITY);
		cout << " ■■■  ■■■     ■     ■     ■   ■  ■ ■ ■  ■■  ■■■ "<< endl;
		SetConsoleTextAttribute(hOut, FOREGROUND_BLUE | FOREGROUND_RED | FOREGROUND_INTENSITY);
		cout << "      ■ ■         ■     ■     ■   ■   ■■ ■    ■       ■"<< endl;
		SetConsoleTextAttribute(hOut, FOREGROUND_GREEN | FOREGROUND_RED | FOREGROUND_INTENSITY);
		cout << "■    ■ ■         ■     ■     ■   ■     ■ ■    ■ ■    ■"<< endl;
		SetConsoleTextAttribute(hOut, FOREGROUND_BLUE | FOREGROUND_INTENSITY);
		cout << " ■■■  ■■■■   ■     ■    ■■  ■     ■  ■■■   ■■■ "<< endl;
		CPoint::SetOutputPosition(9, 15, hOut);
		cout << "Chose difficulity : " << endl;
		cout <<"                                           1)Easy" << endl
			<< "                                           2)Normal" << endl
			<< "                                           3)Hard" << endl;
		while (true)
		{
			chooseLevel = _getch();
			if (chooseLevel == '1' || chooseLevel == '2' || chooseLevel == '3')
			{
				level = chooseLevel-'0';
				switch (level)
				{
				case 1:
					speed = 500;
					break;
				case 2:
					speed = 250;
					break;
				case 3:
					speed = 100;
					break;
				}
				break;
			}
		}
	}
	void PaintEdge()
	{
		HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
		SetConsoleTextAttribute(hOut, FOREGROUND_GREEN| FOREGROUND_INTENSITY);
		for (int x = 0; x <= 78; x += 2)
		{
			for (int y = 0; y < 25; y++)
			{
				if ((x == 0) || (y == 0) || (x == 78) || (y == 24))
				{
					CPoint::SetOutputPosition(x, y, hOut);
					cout << "■";
				}
			}
		}

		for (int x = 2; x <= 78; x+=2)
		{
			CPoint::SetOutputPosition(x, 2, hOut);
			cout << "■";
		}
	}
	void ShowState()
	{
		HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
		SetConsoleTextAttribute(hOut, BACKGROUND_GREEN| FOREGROUND_RED | BACKGROUND_INTENSITY | FOREGROUND_INTENSITY);
		CPoint::SetOutputPosition(2, 1, hOut);
		cout << "Difficulity : ";
		switch (level)
		{
		case 1:
			cout << "Easy    "; break;
		case 2:
			cout << "Noraml"; break;
		case 3:
			cout << "Hard    "; break;
		}
		SetConsoleTextAttribute(hOut, FOREGROUND_GREEN | FOREGROUND_INTENSITY);
		cout << "■";
		SetConsoleTextAttribute(hOut, BACKGROUND_GREEN | FOREGROUND_RED | BACKGROUND_INTENSITY | FOREGROUND_INTENSITY);
		cout << "Press WASD to move your snake.";
		SetConsoleTextAttribute(hOut, FOREGROUND_GREEN | FOREGROUND_INTENSITY);
		cout << "■";
		SetConsoleTextAttribute(hOut, BACKGROUND_GREEN | FOREGROUND_RED | BACKGROUND_INTENSITY | FOREGROUND_INTENSITY);
		cout << "Score : " << score<<"           ";

	}
	void UpdateScore(bool eaten)
	{
		if (eaten)
		{
			HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
			SetConsoleTextAttribute(hOut, BACKGROUND_GREEN | FOREGROUND_RED | BACKGROUND_INTENSITY | FOREGROUND_INTENSITY);
			score += 10;
			CPoint::SetOutputPosition(58, 1, hOut);
			cout << "Score : "<< score;
			int spaceUsed=0;

			for (int s = score; s != 0; s /= 10)
				spaceUsed++;
			for (int i = 0; i < 10 - spaceUsed; i++)
				cout << ' ';
		}
	}
	bool DirectionChanged()
	{
		char ch;
		if (_kbhit())
		{
			ch = _getch();
			switch (ch)
			{
			case 'w':case 'W':
				return snake.ChangeDirection(snake.UP);
			case 'a':case 'A':
				return snake.ChangeDirection(snake.LEFT);
			case 's':case 'S':
				return snake.ChangeDirection(snake.DOWN);
			case 'd':case 'D':
				return snake.ChangeDirection(snake.RIGHT);
			}
		}
		return false;
	}
	void RandomFood(CFood& food)
	{
		srand(static_cast(time(NULL)));
		int x, y;
		while (true)
		{
			x = rand() % (length / 2) * 2;
			y = rand() % (width / 2) * 2;
			if ((x <= 2) || (y <= 6) || (x >= 70) || (y >= 20))
				continue;
			if (!snake.Hit(CPoint(x, y)))
				break;
			else
				continue;
		}
		food.PlaceFood(x, y);
	}
	void PlayGame()
	{
		//游戏界面初始化阶段
		system("cls");
		PaintEdge();
		ShowState();
		food.PlaceFood(20,20);	//设置食物初始位置
		HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
		snake.PaintSnake(hOut);
		while (true)
		{
			snake.Tail().Clear(hOut);
			DirectionChanged();
			bool eaten = snake.Eat(food);
			if (!eaten)
				snake.move();
			else
				RandomFood(food);
			UpdateScore(eaten);
			//将当前字体设置为黑底白色
			SetConsoleTextAttribute(hOut, FOREGROUND_INTENSITY | FOREGROUND_RED | FOREGROUND_GREEN | FOREGROUND_BLUE);
			snake.Head().Plot(hOut);
			if (snake.HitEdge() || snake.HitItself())
			{
				Sleep(1000);
				SetConsoleTextAttribute(hOut, FOREGROUND_RED | FOREGROUND_RED);
				snake.Head().Plot(hOut);
				Sleep(3000);
				break;
			}
			Sleep(speed);
		}
	}
	void GameOver()
	{
		int x = 28;
		int y = 3;
		system("cls");
		HANDLE hOut = GetStdHandle(STD_OUTPUT_HANDLE);
		SetConsoleTextAttribute(hOut, FOREGROUND_GREEN | FOREGROUND_INTENSITY);
		CPoint::SetOutputPosition(x, y, hOut);
		cout << "       ■■■■■" ;
		CPoint::SetOutputPosition(x, y+1, hOut);
		cout << "    ■■■■■■■■ " ;
		CPoint::SetOutputPosition(x, y+2, hOut);
		cout << "  ■■■ ■■■ ■■■ " ;
		CPoint::SetOutputPosition(x, y+3, hOut);
		cout << " ■■■    ■    ■■■ ";
		CPoint::SetOutputPosition(x, y+4, hOut);
		cout << "  ■■■  ■■  ■■■";
		CPoint::SetOutputPosition(x, y+5, hOut);
		cout << "   ■■■■■■■■■";
		CPoint::SetOutputPosition(x, y+6, hOut);
		cout << "     ■■■■■■■";
		CPoint::SetOutputPosition(x, y+7, hOut);
		cout << "     ■■ ■■ ■■";
		CPoint::SetOutputPosition(x, y+8, hOut);
		cout << "                        " ;
		CPoint::SetOutputPosition(x, y+9, hOut);
		cout << "     ■■ ■■ ■■ " ;
		CPoint::SetOutputPosition(x, y+10, hOut);
		cout << "      ■■■■■■";
		CPoint::SetOutputPosition(x+7, y + 13, hOut);
		cout << "GAME  OVER";
		CPoint::SetOutputPosition(x + 7, y + 15, hOut);
		cout << "Score : " << score;
		CPoint::SetOutputPosition(x + 4, y + 17, hOut);
		cout << "Play Again?(Y/N)";
		char ch;
		while (true)
		{
			ch = _getch();
			if (ch == 'n' || ch == 'N')
			{
				exit = true;
				break;
			}
			else if (ch == 'y' || ch == 'Y')
				break;
		}
		CPoint::SetOutputPosition(x - 1, y + 19, hOut);
	}
private:
	bool exit;
	int level;
	int speed;
	bool gameOver;
	CSnake snake;
	CFood food;
	int length;	//游戏区域总长度
	int width;	//游戏区域总宽度
	int score;
};

        至此游戏的设计部分已经完成。最后建立一个Source.cpp文件,来运行游戏。游戏可以理解成一个无尽的循环。循环之前是个初始化阶段,循环中进行游戏,执行以此循环后(即玩家玩过一次之后),将根据玩家选择是否重玩游戏来判断这个循环是否需要再次执行。因此,Source.cpp的主体将是一个do-while循环。

       Source.cpp文件内容如下所示:

#include "Game.h"
#include
#include 
#include 
#include 

using std::cout;
using std::cin;
using std::endl;

int main()
{
	SetConsoleTitle(L"贪食蛇"); //用于设定控制台窗口名
	CGame* Game = new CGame();
	Game->NewGame();
	do
	{
		delete Game;
		Game = new CGame();
		Game->SetGame();
		Game->PlayGame();
		Game->GameOver();
	} while (!Game->Exit());
	Sleep(2000);
	return 0;
}

        实际运行结果:

使用C++设计贪食蛇小游戏_第1张图片

        设置界面:

使用C++设计贪食蛇小游戏_第2张图片

        游戏界面,上面是状态和提示:

使用C++设计贪食蛇小游戏_第3张图片

        当蛇头撞墙或者撞到蛇身时会用红色显示,然后才会进入Game Over界面:

使用C++设计贪食蛇小游戏_第4张图片

        Game Over界面:

使用C++设计贪食蛇小游戏_第5张图片

        这个游戏虽然很简单,但是确实说明了一般游戏的结构。游戏其实就是一个无尽的循环。

感想:

        1.写代码时一定要认真。我刚开始把一个Y写成了X,由于存在这样的函数,所以这并没有语法问题,但很明显会让游戏出现一些奇怪的bug。这花了我数小时来发现这个简单的拼写问题。

        2.程序效率问题。我刚开始对蛇移动方法的设定和上面所说的并不相同。我在CSnake类中定义了两个函数分别用来彻底清除控制台上的现实的蛇和依次显示构成蛇的所有点。然后在CGame::PlayGame()的循环的每一次迭代中都要重新彻底清除上一次迭代时蛇的画面,然后填充上蛇移动之后的画面。但是后来进行游戏时发现,当蛇身比较长时,每移动一步,由于输出光标的原因,就造成蛇身不断闪动,当选择Hard难度模式时,闪动非常明显,影响了整体体验。为解决这个问题,我采用每次移动时只处理蛇头和蛇尾的方法,来提高效率。结果就是,蛇移动时几乎无法感觉到光标的存在,蛇移动得非常稳定。



2013年12月9日 星期一


欢迎转载,请注明出处:点击打开链接


你可能感兴趣的:(使用C++设计贪食蛇小游戏)