C++基于EasyX制作贪吃蛇游戏(五)第三版文档

本文首发于我的个人博客www.colourso.top,欢迎来访。

继续完善贪吃蛇,改用面向对象的思想完成代码,引入界面UI以及排行榜。

上接 C++基于EasyX制作贪吃蛇游戏(三)第二版文档 继续更新制作贪吃蛇游戏的一些相关设计。

程序展示

以下是B站视频

上面视频不能播放请移步:https://www.bilibili.com/video/BV1fZ4y1T7xo/

改用面向对象

原先两版程序都是使用的面向过程方式编写的,函数以及全局变量在整个文件之中飘……,本次决定改用面向对象的方式重写代码,毕竟挺缺少面向对象的练习,可能写出来的代码不是很好,但是我会尽量去完善的。

改用面向对象之后,我会尽力将绘制与数据计算这两者分开,不让两者混杂在一个函数内。所以重写的代码会改变以前两个版本的代码,不过核心流程还是一样的。

公共数据 common.h

//蛇的节点半径
#define SNAKE_RADIU 9
//食物的半径
#define FOOD_RADIU 8
//蛇的节点宽度
#define SNAKE_WIDTH 20
//背景颜色,黑色
#define BG_COLOR 0

//方向的枚举
enum class Dir { DIR_UP = 1, DIR_RIGHT = 2, DIR_DOWN = 3, DIR_LEFT = 4 };

//点的结构体
struct Point {
	int x;
	int y;

	Point() :x(-1), y(-1) {}
	Point(int dx, int dy) :x(dx), y(dy) {}
	Point(const Point& point) :x(point.x), y(point.y) {}

	bool operator==(const Point& point)
	{
		return (this->x == point.x) && (this->y == point.y);
	}

};

//记录游玩信息
struct PlayerMsg
{
	int id;
	int score;
	int len;
	std::string r_time;	//记录时间

	PlayerMsg()
	{
		id = 99;
		score = 0;
		len = 0;
		r_time = "";
	}
};

struct SortPlayerMsg 
{
	bool operator()(const PlayerMsg &msg1, const PlayerMsg &msg2)
	{
		if (msg1.score == msg2.score)
		{
			return msg1.r_time > msg2.r_time;
		}
		else return msg1.score > msg2.score;
	}
};

公共数据头文件,定义以及存储一些常用的数据结构。

Dir是枚举方向类。

Point是点的结构体,重载了==操作符, 便于两个点集的比较。

PlayerMes是用来存储游玩信息。SortPlayerMsg重载了()操作符, 便于两个PlayerMessort排序。详情请看:STL专题-sort、reverse

Snake类的设计 —— 贪吃蛇类

class Snake
{
public:
	const int MinSpeed = 1;			//蛇的最小速度
	const int MaxSpeed = 25;		//蛇的最大速度
	const int OrgSpeed = 15;		//蛇的原始速度

private:
	int m_len;						//蛇的长度
	int m_speed;					//蛇的速度
	Dir m_direction;				//蛇的方向
	std::list<Point> m_snakelist;	//蛇的链表
	Point m_tail;					//蛇移动过后的尾部节点,主要用于吃食物

public:
	Snake();
	~Snake();

	int getLen();					//获取长度
	int getSpeed();					//获取速度
	Dir getDirection();				//获取方向
	bool setSpeed(int speed);		//设置速度,设置成功返回true

	void Move();					//移动一节
	void EatFood();					//吃食物
	void ChangeDir(Dir dir);		//改变方向
	void Dead();					//死亡

	bool ColideWall(int left,int top,int right,int bottom);	//碰撞到墙
	bool ColideSnake();										//碰撞到了自身
	bool ColideFood(Point point);							//碰到了食物

	void DrawSnake();				//绘制蛇
	void DrawSnakeHead(Point pos);	//绘制蛇头
	void DrawSnakeNode(Point pos);	//绘制蛇的身体结点
    
    std::list<Point> GetSnakeAllNode();
	
};

贪吃蛇类,开始使用STL中的list作为蛇的链表,不再使用自定义的链表。链表中存储Point类型的值,及节点的横纵坐标。

额外还需要蛇的方向、长度以及速度这几个参数。Point m_tail;参数在EatFood()函数那里进行说明。

三个 public const int 的速度是预先设置好的速度等级,方便之后使用。

  • bool setSpeed(int speed);函数用于改变蛇的速度,如若改变的蛇的速度超过最大值,那就将蛇的速度设置为最大值;最小值同理。如果修改速度成功就返回true
  • void Move();函数向蛇的方向移动一格,蛇的除蛇头以外的全部节点均向前复制一格。对应链表的操作可以用去除链表末尾的节点,复制链表头部的节点再插入头部,然后额外改变头部的值
  • void EatFood(); 函数主要描述蛇吃到食物之后的动作。在本游戏中,我设定蛇吃到食物后,尾部增长一格。因此需要一个变量来保存蛇刚刚走过的尾部节点,即Point m_tail;。蛇吃到食物后,将这个尾部节点加入链表即可。
  • void ChangeDir(Dir dir);改变方向,本来想起函数名为setDir(Dir dir)的,但是名字不太直观就换了。改变方向时,不是同方向或者不是反方向才能改变。
  • void Dead();死亡效果,因为蛇碰撞死后效果不太直观,就用随机函数改变一下各个节点的位置。但是效果很难看。
  • ColideWallColideSnake以及ColideFood来检测蛇的头部有没有碰撞到什么。
  • std::list<Point> GetSnakeAllNode();用于获取蛇的全部结点,主要用于食物生成检测时使用。

Food类的设计 —— 食物类

class Food
{
private:
	Point m_pos;
	bool m_state;

public:
	Food();

	bool getState();
	void setState(bool state);
	Point getPos();				//获取食物坐标

	void Generate(Snake *snake);//产生新的食物

	void DrawFood();

};
  • 两个数据成员:食物位置以及食物状态。
  • Food();构造参数,其内设定了初始的食物位置,之后的位置需要使用Generate函数生成
  • void Generate(Snake *snake);生成食物函数,因为生成食物不能与蛇的节点重合,所以需要蛇的节点信息。

RankList类的设计 —— 排行榜类

class RankList
{
private:
	std::vector<PlayerMsg> m_msg;
	const std::string m_rankfile = "retro";
	const int MAX_RANK = 10;
public:
	RankList();

	void SaveMsg(PlayerMsg msg);
	std::vector<PlayerMsg> getRankList();
	void SaveToRank();

private:
	void WriteTime(PlayerMsg &msg);
	void ReadFile();
	void WriteFile();
};
  • 排行榜类主要作用是存储管理用户游玩结束之后的游戏数据,涉及了读写文件操作。
  • 使用vector来存储用户的游玩数据,上限是10条,即MAX_RANK。也就是排行榜只保存前10名的数据。固定的读写文件名为retro
  • 私有函数中void WriteTime(PlayerMsg &msg);来写入用户达成成绩的时间。ReadFile()读取配置文件数据,存入到vector中。WriteFile()vector中的数据写回配置文件中。
  • 构造函数RankList();中调用ReadFile()来初始化vector
  • void SaveMsg(PlayerMsg msg);是保存用户数据到vector中,如果其排名在10名之外,则不会保存成功。
  • void SaveToRank();是将vector中的数据写回文件,实际调用的是WriteFile()函数。

Game类的设计 —— 游戏控制类

class Game
{
private:
	int m_GameState;			//游戏状态,0在主UI,1在游戏中,2在排行榜,3在游戏规则中
	PlayerMsg m_msg;			//游玩数据
	Snake *m_snake;				//蛇
	Food *m_food;				//食物
	RankList *m_ranklist;		//排行榜

public:
	Game();

	void Init();			//初始化
	void Run();				//控制程序
	void Close();			//关闭程序,释放资源

private:
	void InitData();		//初始化数据

	void PlayGame();		//开始游戏

	void ShowMainUI();		//展示主UI
	void ShowRank();		//排行榜展示
	void ShowRule();		//展示规则界面

	void DrawGamePlay();	//绘制初始游戏界面
	void DrawScore();		//绘制分数
	void DrawSnakeLen();	//绘制长度
	void DrawSpeed();		//绘制速度
	void DrawRunning();		//绘制正在运行
	void DrawPause();		//绘制暂停提示
	void DrawRebegin();		//绘制重新开始
	void DrawGameOver();	//绘制游戏结束

	void ChangeChooseUI(int left, int top, int right, int bottom, int kind);//修改选中的选项颜色
	void ClearRegion(int left, int top, int right, int bottom);		//使用背景色清除指定区域
};

Game类是游戏的控制类,也是游戏的主体,所以融合了上述全部的类。

Game主要被用于主函数调用,所以只有构造函数以及三个函数是public,其余全部private

  • 程序状态m_GameState,标识程序的运行状态,是在主界面?在游戏中?在排行榜中?还是在游戏帮助中,方便控制程序。

  • void Init();初始化,主要是进行图形库的初始化。

  • void Close();结束,主要是图形库释放资源。

  • void Run();用来运行程序,展示UI,等待用户操作。

  • 构造函数Game();主要是初始化一些数据,最主要的是设置程序状态m_GameState为0,以及初始化RankList,便于访问排行榜时可以看到数据。

  • InitData()初始化一些在开始游戏时才需要用到的数据,比如Snake以及Food,重置PlayMsg,防止原来的数据对新开一局的数据产生干扰。

  • PlayGame()则是游戏的控制函数,主要完成游戏中的全部控制,留在下面细说。

  • ChangeChooseUI这个函数主要就是改变选中选项的效果,重新绘制这个按钮的样式,增加程序与用户的交互。

UI设计

相较于之前的两版程序增加了UI,更加方便用户的控制,同时增加了鼠标的点选,更加直接。

例如上图左上角的返回键可以点击。

鼠标点击操作

if ((m_GameState == 2 || m_GameState == 3) && MouseHit()) //在排行榜或者游戏帮助中点击
{
	MOUSEMSG mouse = GetMouseMsg();//获取鼠标点击消息
	if (mouse.mkLButton)			//左键按下
	{
		if (mouse.x >= 20 && mouse.x <= 63 && mouse.y >= 20 && mouse.y <= 43)
		{
			//点击返回选项
			ChangeChooseUI(20, 20, 63, 43, 5);
			Sleep(500);
			
            FlushMouseMsgBuffer();//清空鼠标消息缓冲区。
            
			m_GameState = 0;
			ShowMainUI();
		}
	}
}
  • MouseHit()来检测有没有鼠标点击事件,有的话为true。
  • GetMouseMsg()来获取鼠标点击消息,返回一个MOUSEMSG类型的数据。
  • FlushMouseMsgBuffer()来清空鼠标消息缓冲区,防止残存的消息对其他函数产生干扰。

游戏控制 - PlayGame()

相较于前两版程序,我换用了重绘机制。原版程序使用的是仅消除蛇的尾端,局部擦除与重绘的方式。

但是由于数据运算与绘制的分离,原版的方式不容易实现,于是现在使用的是每一次循环就重新绘制一次游戏界面的方式,也就是最常规的方式。

以下是伪流程:

while(true)
{
	if(检测食物是否存在)
	{
		不存在生成
	}
	
	if(按键检测)
	{
		改变方向或者暂停程序
	}
	
	Move();//移动
	
	if(吃到食物)
	{
		长度增加
		分数增加
		食物状态改变
	}
	
	if(碰撞检测)
	{
		碰撞则死亡
		...
	}
	
	清空区域
	重绘蛇
	
	sleep(200);
	
}

具体的内容可以在函数实现里看到

批量绘图

上述循环完成之后,界面每一次重新绘制都有些不太稳定,有闪烁的情况,这时就需要使用批量绘图。

  • BeginBatchDraw();开始批量绘图,其后的任何绘图操作暂时都不会进行绘制,直到执行 FlushBatchDraw()EndBatchDraw() 才将之前的绘图输出。

  • FlushBatchDraw() 用于执行绘制任务。

  • EndBatchDraw()结束批量绘图模式,并将还没有绘制的图完成绘制。

这三者加入到PlayGame()函数中,保证画面的流畅性。

结束语

至此,面向对象版贪吃蛇程序完成。这版程序主要做了一些事情:

  • 改用面向对象方式编写程序
  • 换用蛇的数据结构为STL的list,操作更加方便。
  • 将数据运算与绘制操作分离
  • 增加UI与用户交互效果
  • 增加排行榜机制,使用了文件读写操作。

一些不足:

  • 食物类的生成算法需要检测蛇的节点保证不覆盖,因此效率可能比较差,实际运行时会有卡顿现象。考虑后续引入多线程解决。
  • UI还是挺难看的……
  • 等待补充……
posted @ 2020-08-05 11:24  Colourso  阅读(653)  评论(0编辑  收藏  举报