zoukankan      html  css  js  c++  java
  • 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还是挺难看的……
    • 等待补充……
  • 相关阅读:
    四种解释
    CFA知识框架
    VMWare 开源 Octant,可视化的 Kubernetes 工作负载仪表板
    从贝叶斯方法谈到贝叶斯网络
    06-STM32+W5500+AIR202基本控制篇-实现功能1,功能2和功能4服务器搭建-购买云服务器(电脑)(.Linux系统)
    06-STM32+W5500+AIR202基本控制篇-实现功能1,功能2和功能4服务器搭建-购买云服务器(电脑)(.Windows系统)
    05-STM32+W5500+AIR202基本控制篇-功能5-Android和微信小程序扫描二维码绑定GPRS,并通过MQTT实现485,422通信和继电器控制(微信小程序)
    05-STM32+W5500+AIR202基本控制篇-功能4-Android和微信小程序扫描二维码绑定GPRS,并通过MQTT实现485,422通信和继电器控制(Android)
    04-STM32+W5500+AIR202基本控制篇-功能2-Android和微信小程序使用MulticastBind绑定W5500,并通过MQTT实现485,422通信和继电器控制(Android)
    数据库基础开源学习教程-android 使用 litepal 操作本地数据库
  • 原文地址:https://www.cnblogs.com/colourso/p/13438687.html
Copyright © 2011-2022 走看看