本帖最后由 Xiaofeng 于 2019-6-5 13:45 编辑
上一个帖子已经介绍了用uFun开发板制作的花里胡哨的“手柄”,这个帖子介绍一下贪吃蛇游戏界面的制作。两者之间采用串口通信,上个帖子介绍了数据发送的格式约定,这里就不在赘述了,详见帖子【uFun开发板评测】贪吃蛇(一):用uFun开发板做游戏手柄。 帖子内用的是开源版Qt,版本为Qt5.11.2 游戏界面及功能略为粗糙,文末提供了源码以及编译完成的程序下载附件。
![](http://image109.360doc.com/DownloadImg/2019/06/0718/163078753_1_20190607063325112.png) 游戏界面.PNG (24.84 KB, 下载次数: 0) 下载附件 保存到相册 前天 12:28 上传 先来分析一下帖子内这款游戏想要实现的功能:
- 首先实现串口通信是必须的。
- 绘制一条能动的贪吃蛇和一个食物是游戏的基础。
- 能判断游戏的结束,分为撞墙死和自闭而死。
- 试图模仿以前小霸王之类的游戏机,uFun做的手柄也能即插即用。
确定功能之后,就要构思怎么去实现了:
- 串口通信可以采用Qt5提供的QSerialPort类轻松实现;
- 为了能动,采用了定时器定时重绘的方法。每次重绘都更新一下贪吃蛇位置,这样贪吃蛇就动起来了;
- 贪吃蛇的躯体可以用一个数组来存储每一节身体的位置坐标,为了方便,采用了Qt自带的QList+QPoint实现;
- 食物被吃了之后需要随机出现,采用了QRandomGenerator随机数生成器来生成食物的坐标;
- 既然已经存储了贪吃蛇身体的坐标,场地的坐标范围也是已知的,只要将贪吃蛇下一个行进的坐标和它们比较就能知道游戏有没有结束;
- 为了实现即插即用,拔掉再插还能用,可以对QSerialPort类的errorOccurred信号进行处理,为了方便,只要串口出错就关闭串口。没有串口连接时就在游戏中不断尝试连接串口,遇到第一个能用的串口直接打开,之后停止尝试。(这里做了一个假设,就是按照常理游戏机上只会插手柄)
其中:
- 位置坐标并不是按像素算的。贪吃蛇的身体是由一个个小长方形构成的,长方形有一定的长和宽,位置坐标是以一个长方形为单位的,游戏界面的大小也是长方形的整数倍;
- #define block_width 15 // 长方形的宽
- #define block_height 15 // 长方形的高
- #define x_count 50 // 游戏界面的宽为(block_width*x_count)
- #define y_count 40 // 游戏界面的高为(block_height*y_count)
>>>程序中定义了一个bool startFlag变量用来标识游戏是否开始。 >>>我尝试着对实现贪吃蛇躯体做了个很不成熟的类封装,有些成员函数都是在编程过程中需要才加上的。
- #ifndef SNAKE_H
- #define SNAKE_H
- #include <QPoint>
- #include <QList>
- // 宏定义四个方向
- #define dir_up 1
- #define dir_down 2
- #define dir_left 3
- #define dir_right 4
- class Snake
- {
- public:
- Snake(QPoint headLocation, int lenth = 3, int cur_dir = dir_right);
- int getLenth(); // 获取贪吃蛇长度
- int getCurDirecton(); // 获取当前前进方向
- QList<QPoint>& getBody(); // 获取贪吃蛇的整个身体,主要在绘制里用
- QPoint getHeadLocation(); // 获取贪吃蛇头部的坐标位置
- bool addBlock(int curDir); // 添加一块身体
- bool makeStep(int dir); // 向某个方向前进一步
- bool isInSnake(QPoint& point); // 判断某一点是否在蛇的身体内
- bool isInSnake(int x, int y); // 判断某一点是否在蛇的身体内
- void tryGo(QPoint* point, int dir); // 尝试走一步,输入一个坐标和前进方向,获取走一步之后的新坐标点
- private:
- int curDirection; // 用来记录当前前进的方向
- QList<QPoint> snake_body; // 用来存储贪吃蛇的身体各单元坐标点
- };
- #endif // SNAKE_H
说明一下其中addBlock()和makeStep()函数,实际上实现过程就是把身体最后一个坐标位置去除,并在列表中加入新的头位置。 >>>串口初始化中主要需要连接处理串口的两个信号:
- 一个是出错信号errorOccurred,主要做的工作是把出错时已经打开的串口关闭,这样程序循环内才会继续尝试打开串口。(在初始化时也会有一次尝试寻找可用的串口);
- 另一个是接收到数据的信号readyRead,其中的数据处理思路和uFun开发板处理思路相同,不再赘述。当接收到信息之后,会把方向暂存在一个变量里,只有当定时器到点重绘时才会真正更新前进方向,也就是说只认重绘前最后一次方向。
- // 帧信息处理过程
- if(ch == '>'){
- // 一帧接收结束
- // 当停在游戏开始界面时,检测到按键就直接置位开始游戏
- if(this->startFlag == false){
- this->startFlag = true;
- if(pTimer->isActive() == false)
- pTimer->start();
- return;
- }
- // A-left D-right W-up S-down
- switch (rcvData.toUtf8().at(0)) {
- case 'A': case 'a':
- if(this->curDirection != dir_right)
- this->preDirection = dir_left;
- break;
- case 'D': case 'd':
- if(this->curDirection != dir_left)
- this->preDirection = dir_right;
- break;
- case 'W': case 'w':
- if(this->curDirection != dir_down)
- this->preDirection = dir_up;
- break;
- case 'S': case 's':
- if(this->curDirection != dir_up)
- this->preDirection = dir_down;
- break;
- }
- frameFlag = false;
- }
>>>采用随机数生成器生成食物的坐标。当生成食物不合理时,会重复生成。(此处随机种子很简单,但效果看起来也无大碍,此外没有处理食物生在死角那种极端情况)
- void Widget::makeFood()
- {
- QRandomGenerator generator;
- quint32 x, y;
- while(true){
- generator.seed(static_cast<quint32>(QTime::currentTime().second()));
- x = generator.generate();
- y = generator.generate();
- x %= x_count;
- y %= y_count;
- if(pSnake->isInSnake(static_cast<int>(x), static_cast<int>(y)) == false)
- break;
- }
- foodPoint.setX(static_cast<int>(x));
- foodPoint.setY(static_cast<int>(y));
- DEBUG_cout(x << y) ;
- }
>>>界面动起来的核心是重绘定时器。每次定时器到点触发timeout信号,都会进行判断游戏是否结束,是否吃到食物,并且会调用addBlock()或makeStep()函数更新一下贪吃蛇的位置,最后重绘界面。每局游戏结束后会直接开始下一轮游戏。(调节定时器时间就可以加快游戏速度,提升难度)
- pTimer = new QTimer(this);
- pTimer->setInterval(150); // 控制重绘的时间间隔,可以控制游戏难度
- connect(pTimer, &QTimer::timeout, this,
- [=](){
- // 如果串口没有连接成功,则一直尝试
- if(pSerialPort->isOpen() == false){
- DEBUG_cout('still try');
- QList<QSerialPortInfo> infoList = QSerialPortInfo::availablePorts();
- // 如果连接上一个串口直接跳出,没有可用串口会在程序中不断循环查找串口并尝试连接
- QSerialPortInfo info;
- foreach(info, infoList){
- pSerialPort->setPortName(info.portName());
- if(pSerialPort->open(QIODevice::ReadWrite))
- break;
- }
- }
- if(this->startFlag == true){
- // 只有每次绘制的时候才会真正修改蛇的前进方向
- this->curDirection = this->preDirection;
- // 判断是否增加长度以及是否游戏结束
- if(this->isGameOver()){
- // 游戏结束后清除标志变量,以进行下一场游戏
- this->startFlag = false;
- pSnake = new Snake(QPoint(x_count / 2, y_count / 2), 5, curDirection);
- makeFood();
- }
- else {
- QPoint point = pSnake->getHeadLocation();
- pSnake->tryGo(&point, this->curDirection); // 尝试前进一步
- if(point == foodPoint){ // 判断有没有吃到食物
- pSerialPort->write('<F>'); // 发送<F>表示吃到食物
- pSnake->addBlock(this->curDirection);
- // 更新食物的位置
- this->makeFood();
- }
- else{
- pSnake->makeStep(curDirection);
- }
- }
- this->update();
- }
- });
- pTimer->start();
>>>游戏界面的绘制都在绘制事件响应函数paintEvent函数内完成。绘制过程很简单,就是把蛇和食物画出来,在游戏没开始时,多绘制一行提示文字。
- void Widget::paintEvent(QPaintEvent *event)
- {
- QPainter painter(this);
- painter.setBrush(QColor('#ff0000'));
- QList<QPoint> snake_body = pSnake->getBody();
- QPoint point;
- foreach(point, snake_body){
- painter.drawRect(point.x() * block_width, point.y() * block_height, block_width, block_height);
- }
- painter.drawEllipse(foodPoint.x() * block_width, foodPoint.y() * block_height, block_width, block_height);
- if(this->startFlag == false){
- QFont font = painter.font();
- font.setPixelSize(block_height * 3);
- painter.setFont(font);
- painter.drawText(this->rect(), Qt::AlignCenter, '按任意键开始游戏');
- }
- }
整个软件主要的实现过程大概如上,打开程序并连接用uFun开发板做好的手柄就可以直接开始游戏,吃到食物蜂鸣器就响一下。制作动图比较麻烦,这里就不配效果图了。 PS:程序内也实现了keyPressEvent函数,也就是说键盘的上下左右和WASD也能控制程序,Q键退出程序。 Qt项目源码:
Snake-Qt.zip(6.38 KB, 下载次数: 5)前天 13:34 上传 点击文件名下载附件 源码 下载积分: e币 -2 下载即用的程序(Snake.exe):
Snake-程序.zip(19.05 MB, 下载次数: 0)前天 13:36 上传 点击文件名下载附件 程序 下载积分: e币 -2
|