C++巧用vector容器实现贪吃蛇移动基本原理

本文介绍了如何使用C++和STL向量实现贪吃蛇游戏。通过设计贪吃蛇类,记录蛇头和蛇尾坐标,以及蛇身移动方向的vector,实现了蛇的移动和碰撞检测。文章详细阐述了游戏规则、设计思路、代码实现,包括蛇的移动、界面绘制、食物生成、窗口初始化等关键函数,展示了如何通过向量操作简化游戏逻辑。
摘要由CSDN通过智能技术生成

一、设计思路

贪吃蛇游戏的基本游戏规则如下:

  • 蛇在固定游戏框架区域内移动;
  • 蛇身体行径轨迹会沿着蛇头行径轨迹移动;
  • 框架内随机出现食物,蛇吃掉食物后身体变长,食物被吃掉后系统随机生成下一个食物;
  • 当蛇头碰到蛇身体或障壁时游戏结束。

不难看出,在贪吃蛇游戏代码设计中,难点在于如何实现蛇身体移动轨迹与蛇头保持一致。如图所示:

 从上图不难看出,蛇的移动方式基本遵循“加头去尾”的规则,即蛇头每次朝指定方向前进一个单位,蛇尾朝蛇头行径轨迹跟进一个单位;同时,蛇头每次“吃下”一个食物时,蛇尾“保持不动”而蛇头“前进一个单位”。

在学习了C++的标准模板库(STL)之后,笔者发现:通过设计一个贪吃蛇的类,该类中包含记录蛇头坐标和蛇尾坐标的成员,以及记录蛇身移动方式数据的vector向量成员。当蛇头每次移动一个单位,vector尾部插入一个记录蛇头移动方向的数据,同时vector首部删除一个数据,蛇尾移动前通过读取vector首部数据从而可以按照蛇头移动方向进行移动。而蛇头“吃下”食物时,只需在vector尾部插入记录蛇头移动方向的数据,vector首部不删除数据。

如此一来,仅使用vector向量记录的方式,即可用较少的代码实现贪吃蛇移动。

二、代码实现

下面通过实例代码来描述贪吃蛇游戏设计的基本思路。

程序设计平台:Visual Studio 2022 (需添加easyX插件)

打开Visual Studio 2022,选择添加C++控制台空白项目即可。

下面展示程序代码(附注释说明):

1、类定义

项目中添加.h类文件,添加如下代码:

#include <graphics.h>
#include <vector>
#include "Var.cpp"

using namespace std;
using namespace SnakeVar;

#pragma once

class Snake			//贪吃蛇基类
{
public:
	int HeadX;				//蛇头部坐标
	int HeadY;

	int TailX;				//蛇尾部坐标
	int TailY;

	vector<int> snake;		//设置向量snake表示贪吃蛇蛇身方向数据,每个单位数值范围为[1,4]

	int map[GameWidth][GameHeight] = { 0 };		//游戏区域

	void SetHead(int cx, int cy, int length);		//初始化蛇头位置
};

在相应.cpp 文件中对Snake::SetHead()函数进行定义:

#include "Snake.h"

void Snake::SetHead(int cx, int cy, int length)
{
	HeadX = cx;                //设置蛇头坐标
	HeadY = cy;

	for (int i=0;i<length;i++)        //蛇头朝下length-1个单位均赋值为1代表蛇身
		map[cx][cy+i] = 1; 

	TailX = cx; TailY = cy+length-1;    //最后一个单位所在坐标为蛇尾坐标

	snake.resize(length, 1);            //初始化向量snake容量,数据均赋值为1
}

2、头文件引用及变量声明

#include <graphics.h>        //使用easyX画图及窗口创建功能
#include <conio.h>           //代码中使用getchar(),_getch()及_kbhit()函数需引用
#include <vector>            //使用vector向量
#include <time.h>            //时间函数为随机数提供种子
#include <stdlib.h>          //使用随机函数rand()功能

using namespace std;
using namespace SnakeVar;

//变量声明区
unsigned int snakelength = InitSnakeLen;	//蛇身长度
Snake newsnake;
static Direction Movingflag = DIR_UP;		//初始默认运动方向为向上

3、定义常量及宏

新建一个Var.cpp文件用于定义常量及宏:

namespace SnakeVar
{
	const unsigned int GameWidth = 15;					//游戏区域长宽值
	const unsigned int GameHeight = 15;

	typedef enum { DIR_UP = 1, DIR_DOWN = 2, 
                   DIR_LEFT = 3, DIR_RIGHT = 4 } Direction;	 //枚举移动方向类型

#define BLOCK 30	                 //单个方块像素单位
#define InitSnakeLen 2               //初始蛇长度(包括头部)

}

4、主程序函数声明及定义

在主程序所在Main.cpp文件中对所用函数声明如下:

//函数声明
int Crand(int MinValue, int MaxValue);		//设置范围随机数
void DrawMap();								//绘制贪吃蛇运动范围
void InitGame();							//窗口初始化
void MoveSnake(Direction direction);		//蛇身移动
void SetFood();								//在贪吃蛇运动范围内生成食物
void SetWindowCentered(HWND hwnd);			//窗口显示中心化

下面对程序中使用的函数逐一说明:

(1)随机函数Crand()

int Crand(int MinValue, int MaxValue)
{
	srand((unsigned)time(NULL) + (unsigned)rand());
	int result = (rand() % (MaxValue - MinValue + 1)) + MinValue;
	return result;
}

总所周知,C/C++语言产生随机数是需要“种子”(srand())来辅助的,然而系统默认种子数值为1,因此单纯使用rand()函数生成的随机数具有很强规律性,随机性很差。因此,在实践中通常使用时间函数time()作为种子以提高随机数的随机性。这里通过Crand()函数可以随机在最小值MinValue和最大值MaxValue之间生成随机数,为贪吃蛇的“食物”提供随机坐标位置。

(2)DrawMap()绘制界面

上文在Snake类中定义了二维数组map用于提供界面坐标,map数组中各单位不同数值代表不同含义:数值为0表示空白区域,用黑色方块表示;数值大于0表示该区域为贪吃蛇身体占据部分,用蓝色方块表示;数值小于0表示该点位为“食物”所在位置,用黄色方块表示。最后,根据Snake类中蛇头位置坐标HeadX及HeadY数值定位蛇头位置,用红色方块表示。

程序中每次map数组中发生数值变动,使用DrawMap()函数遍历map数组并重新绘制界面:

void DrawMap()			
{
	setlinestyle(PS_DOT);	                //单元格边框
	for (int x = 0; x < GameHeight; x++)
	{
		for (int y=0;y< GameWidth;y++)
			if (newsnake.map[y][x] == 0)	//数值为0区域为空白区
			{
				setfillcolor(BLACK);
				fillrectangle(y*BLOCK, x*BLOCK, (y+1)*BLOCK, (x+1)*BLOCK);
			}
			else if (newsnake.map[y][x] > 0)		//数值大于0区域为蛇身部分
			{
				setfillcolor(BLUE);
				fillrectangle(y * BLOCK, x * BLOCK, (y + 1) * BLOCK, (x + 1) * BLOCK);
			}
			else if (newsnake.map[y][x] < 0)		//负数部分为食物
			{
				setfillcolor(YELLOW);
				fillrectangle(y * BLOCK, x * BLOCK, (y + 1) * BLOCK, (x + 1) * BLOCK);
			}
	}
	setfillcolor(RED);				        //蛇头绘制为红色
	fillrectangle(newsnake.HeadX * BLOCK, newsnake.HeadY * BLOCK, 
		(newsnake.HeadX + 1) * BLOCK, (newsnake.HeadY + 1) * BLOCK);
}

(3)窗口初始化InitGame()

依据初始高度及初始宽度绘制窗口,并居中显示:

void InitGame()
{
	initgraph(GameWidth * BLOCK, GameHeight * BLOCK, 0);			
	HWND hwnd = GetHWnd();          //获取窗口句柄
	SetWindowCentered(hwnd);		//窗口居中显示
}

(4)移动蛇头MoveSnake()

void MoveSnake(Direction direction)
{
	vector<int>::iterator sp = newsnake.snake.begin();    //迭代器指向向量首部
	unsigned int number;        //传递移动方式数据

	switch (direction)			
	{
	case DIR_UP:			
		number = 1;					//方向数据:向上移动为1,向下移动为2,向左移动为3,向右移动为4
		if (newsnake.HeadY <= 0) exit(0);							//蛇头碰到最顶端时
		if (newsnake.map[newsnake.HeadX][newsnake.HeadY-1]<0)		//当蛇头下一次移动位置上有食物
		{
			snakelength++;											//蛇身长度增加
			newsnake.snake.push_back(number);						//向量尾部插入记录蛇头移动方式的数据
			newsnake.map[newsnake.HeadX][--newsnake.HeadY] = number;	//蛇尾坐标不变,蛇头坐标向当前方向增加一个单位,新坐标位置赋值大于0表示位置被蛇身占据
			SetFood();												//生成下一个食物
		}
		else if (newsnake.map[newsnake.HeadX][newsnake.HeadY - 1]==0)	//蛇头下一次移动位置为空白区域时
		{
			newsnake.snake.erase(sp);		//蛇头每次移动一个单位,向量snake删除向量首部数据并在向量尾部添加蛇头读取的新方向数据
			newsnake.snake.push_back(number);
			newsnake.map[newsnake.HeadX][--newsnake.HeadY] = number;
			switch (newsnake.snake.front())					//蛇尾移动方式需从向量snake首部读取数据
			{
			case 1:
				newsnake.map[newsnake.TailX][newsnake.TailY--] = 0;    //读取数值为1,蛇尾向上移动,同时原坐标位置设置为0
				break;
			case 2:
				newsnake.map[newsnake.TailX][newsnake.TailY++] = 0;    //读取数值为2,蛇尾向下移动,同时原坐标位置设置为0
				break;
			case 3:
				newsnake.map[newsnake.TailX--][newsnake.TailY] = 0;    //读取数值为3,蛇尾向左移动,同时原坐标位置设置为0
				break;
			case 4:
				newsnake.map[newsnake.TailX++][newsnake.TailY] = 0;    //读取数值为4,蛇尾向右移动,同时原坐标位置设置为0
				break;
			}
		}
		else    //当蛇头下一次移动位置上数值大于0,表明蛇头碰到蛇身
		{
			exit(0);    //程序退出
		}
		break;
	case DIR_DOWN:    //其他方向控制代码参考上述代码
		number = 2;
		if (newsnake.HeadY+1 >= GameHeight) exit(0);
		if (newsnake.map[newsnake.HeadX][newsnake.HeadY + 1] < 0)
		{
			snakelength++; 
			newsnake.snake.push_back(number);
			newsnake.map[newsnake.HeadX][++newsnake.HeadY] = number;
			SetFood();
		}
		else if (newsnake.map[newsnake.HeadX][newsnake.HeadY + 1] == 0)
		{
			newsnake.snake.erase(sp);
			newsnake.snake.push_back(number);
			newsnake.map[newsnake.HeadX][++newsnake.HeadY] = number;
			switch (newsnake.snake.front())
			{
			case 1:
				newsnake.map[newsnake.TailX][newsnake.TailY--] = 0;
				break;
			case 2:
				newsnake.map[newsnake.TailX][newsnake.TailY++] = 0;
				break;
			case 3:
				newsnake.map[newsnake.TailX--][newsnake.TailY] = 0;
				break;
			case 4:
				newsnake.map[newsnake.TailX++][newsnake.TailY] = 0;
				break;
			}
		}
		else
		{
			exit(0);
		}
		break;
	case DIR_LEFT:
		number = 3;
		if (newsnake.HeadX <= 0) exit(0);
		if (newsnake.map[newsnake.HeadX-1][newsnake.HeadY] < 0)
		{
			snakelength++; 
			newsnake.snake.push_back(number);
			newsnake.map[--newsnake.HeadX][newsnake.HeadY] = number;
			SetFood();
		}
		else if (newsnake.map[newsnake.HeadX-1][newsnake.HeadY] == 0)
		{
			newsnake.snake.erase(sp);
			newsnake.snake.push_back(number);
			newsnake.map[--newsnake.HeadX][newsnake.HeadY] = number;
			switch (newsnake.snake.front())
			{
			case 1:
				newsnake.map[newsnake.TailX][newsnake.TailY--] = 0;
				break;
			case 2:
				newsnake.map[newsnake.TailX][newsnake.TailY++] = 0;
				break;
			case 3:
				newsnake.map[newsnake.TailX--][newsnake.TailY] = 0;
				break;
			case 4:
				newsnake.map[newsnake.TailX++][newsnake.TailY] = 0;
				break;
			}
		}
		else
		{
			exit(0);
		}
		break;
	case DIR_RIGHT:
		number = 4;
		if (newsnake.HeadX + 1 >= GameWidth) exit(0);
		if (newsnake.map[newsnake.HeadX+1][newsnake.HeadY] < 0)
		{
			snakelength++; 
			newsnake.snake.push_back(number);
			newsnake.map[++newsnake.HeadX][newsnake.HeadY] = number;
			SetFood();
		}
		else if (newsnake.map[newsnake.HeadX+1][newsnake.HeadY] == 0)
		{
			newsnake.snake.erase(sp);
			newsnake.snake.push_back(number);
			newsnake.map[++newsnake.HeadX][newsnake.HeadY] = number;
			switch (newsnake.snake.front())
			{
			case 1:
				newsnake.map[newsnake.TailX][newsnake.TailY--] = 0;
				break;
			case 2:
				newsnake.map[newsnake.TailX][newsnake.TailY++] = 0;
				break;
			case 3:
				newsnake.map[newsnake.TailX--][newsnake.TailY] = 0;
				break;
			case 4:
				newsnake.map[newsnake.TailX++][newsnake.TailY] = 0;
				break;
			}
		}
		else
		{
			exit(0);
		}
		break;
	}
	DrawMap();         //每完成一次移动过程,界面绘制一次
	Sleep(1000-(snakelength-InitSnakeLen)*10);        //移动后延迟时间,蛇身越长,延迟时间越短
}

上文提到,贪吃蛇移动方式简化为蛇头坐标及蛇尾坐标数值变动以及snake向量增、删数据的算法。本代码中,对蛇头下一次移动后动作作如下判定:

  1. 当蛇头下一次移动位置为空白区域时,向snake向量尾部添加数据,同时删除snake向量首部数据;
  2. 当蛇头下一次移动位置上有食物时,仅完成向量尾部添加数据的动作,不删除向量首部数据;
  3. 当蛇头移动到游戏边界时,游戏结束。

具体移动方式处理可参考上述代码注释。

(5)随机生成食物SetFood()

void SetFood()
{
re:	int cx = 0, cy = 0;
	cx = Crand(0, GameWidth-1);
	Sleep(150);					//人为制造时间差,降低X与Y数值相同的几率
	cy = Crand(0, GameHeight-1);
	if (newsnake.map[cx][cy] > 0)	//随机坐标位于蛇身时重新生成
		goto re;
	else newsnake.map[cx][cy] = -1;
}

该段代码中为方便,在判定随机坐标位是否位于蛇身的部分使用了goto语句,不喜欢goto语句的可用do-while循序方式改写。

(6)设置窗口居中SetWindowCentered()

void SetWindowCentered(HWND hwnd)
{
	RECT rect; INT windowX, windowY;

	GetWindowRect(hwnd, &rect);

	windowX = (GetSystemMetrics(SM_CXFULLSCREEN) - (rect.right - rect.left)) / 2;		//设置窗口居中显示位置
	windowY = (GetSystemMetrics(SM_CYFULLSCREEN) - (rect.bottom - rect.top)) / 2;

	SetWindowPos(hwnd, HWND_TOPMOST, windowX, windowY, -1, -1, SWP_NOSIZE | SWP_NOZORDER);

5、main函数部分

主程序段代码如下:

int main(int argc, char* argv[])
{
	InitGame();        //初始化窗口
	newsnake.SetHead(GameWidth/2, GameHeight/2, snakelength);    //初始化蛇头位置
	SetFood();        //设置食物位置
	DrawMap();        //游戏界面绘制

	while (true)        //通过循环代码实现蛇身连续不停运动
	{
		if (_kbhit())
		{
			char ch = _getch();    //使用wsad按键实现上下左右方向控制
			switch (ch)
			{
			case 'w':
				if (Movingflag !=DIR_DOWN) Movingflag = DIR_UP;    //当蛇头向下运动时向上运动按键失效,避免触碰蛇身
				break;
			case 's':
				if (Movingflag != DIR_UP) Movingflag = DIR_DOWN;    //当蛇头向上运动时向下运动按键失效
				break;
			case 'a':
				if (Movingflag != DIR_RIGHT) Movingflag = DIR_LEFT;    //当蛇头向右运动时向左运动按键失效
				break;
			case 'd':
				if (Movingflag != DIR_LEFT) Movingflag = DIR_RIGHT;    //当蛇头向左运动时向右运动按键失效
				break;
			}
		}
		MoveSnake(Movingflag);    //完成一次蛇头运动,然后循环
	}

	getchar();
	return 0;
}

代码编译后,调试效果如下:

内容如有错误,欢迎随时批评指正,谢谢各位。

CJds_snakeView类 Struct list { int x; int y; }; 点结构,x表示横坐标,y表示纵坐标。 成员变量: int start;start=1开始游戏,start=0停止游戏。 int difficulty;表示游戏难度,等于10表示容易,等于6表示普通,等于3表示困难,等于2表示特困。 int last_aspect;表示蛇之前前进的方向,用于判断防止蛇逆行的。 int can_new;表示是否添加新食物。等于1表示产生新的食物,等于0表示产生之后不不要再产生食物。 int count;表示蛇的节数; CRect rct;表示矩形方格; list a[100];保存蛇的长度; list b;用来保存蛇头的坐标位置; int aspect;表示蛇前进的方向 等于0表示蛇向右前进; 等于1表示蛇向下前进; 等于2表示蛇向左前进; 等于3表示蛇向上前进; int xx;表示食物的x坐标; int yy;表示食物的y坐标; 成员函数: void Init();初始化蛇的节数,并确定了节数的坐标位置。首先初始化蛇的前进方向aspect=0是向右前进的,last_aspect=0用来保存蛇当前前进的方向。并且初始化蛇的节数为4节,并指明了它们的坐标位置。can_new=1表示产生一个新的食物。 void aliveordie();判断蛇是死还是活着。有两种情况,一种是蛇自己撞到自己,表示死了,start=0从新开始游戏,另一种是超出了界面(20,20,420,420)撞到了墙start=0也从新开始游戏。 double random(double start,double end);在start和end坐标之间产生随即数; 消息处理事件: (1)void onTimer(UINT nIDEvent);在OnCreate()创建计时器SetTimer(),来触发OnTimer事件,所以要首先销毁时间计时器,然后调用aliveordie()函数判断蛇的生死情况,CString str_count; str_count.Format("分数:%d",count-4)显示蛇的节数,TextOut()指出坐标输出文本,创建设备上下文,和创建位图。然后把位图选择到设备上下文上,填充颜色为白色。 在位图上绘制两个矩形线框: Rectangle(CRect(10,10,440,440)); Rectangle(CRect(20,20,430,430)); 显示位图(BitBlt(0,0,1000,1000,&MenDC,0,0,SRCCOPY)); 判断如果游戏开始(start==1),b.x=a[0].x用b.x来保存蛇头的横坐标;b.y=a[0].y用b.y来保存蛇头的纵坐标。如果aspect==0表示蛇向右前进,aspect=1表示蛇向下前进,aspect==2表示蛇向左前进,aspect==3表示蛇向上前进,a[1].x=b.x;a[1].y=b.y;把蛇头的坐标付给了a[1],说明蛇头改变了前进的方向。绘制蛇头矩形方格,并且设置蛇头的颜色为红色,蛇身是三个黄色的矩形方格。if (a[0].x==xx && a[0].y==yy)判断蛇头的位置与食物的关系,现在的情况说明蛇吃了食物,蛇的节数加一,各个节数的坐标向前进一位。调用random()函数产生随即的食物,然后判断食物随即产生的位置与蛇的位置,如果产生的食物在蛇身体上,要从新调用random()函数随即再产生食物。产生的食物颜色为绿色。int m_timer=SetTimer(1,difficulty*30,NULL);计时间隔,计时间隔的快慢是根据难易程度的不而确定的。销毁位图、销毁设备上下文。 (2)OnKeyDown(UINT nChar, UINT nRepCnt, UINT nFlags);判断蛇当前前进的方向,当aspect返回0,表示蛇前进的方向是向右,当aspect返回1,表示蛇前进的方向是下,当aspect返回2,表示蛇前进的方向是向左,当aspect返回3,表示蛇前进的方向是向上。 (3)点击菜单按钮调用以下函数: OnDifficultyEasy();OnDifficultyHard();OnDifficultyNormal();OnDifficultyVeryhard();OnButtonStart();
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值