本文首发于我的个人博客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
重载了()
操作符, 便于两个PlayerMes
的sort
排序。详情请看: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();
死亡效果,因为蛇碰撞死后效果不太直观,就用随机函数改变一下各个节点的位置。但是效果很难看。ColideWall
、ColideSnake
以及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还是挺难看的……
- 等待补充……
请发表评论