QT:完整的人机五子棋设计(一)棋盘

1、前言

QT Creator5.9.9

近段时间学习了QT的一些设计基础,忍不住设计了个五子棋小游戏项目进行实战,从最开始的创建,到最后的整个游戏安装包,经过磕磕绊绊,最终结果还算满意。当然作为新手菜鸟,肯定存在一些问题,如果你恰好看到这篇文章,若有看到不当的地方,欢迎提及。

先来看下游戏界面整体效果:

image

实现的功能有:与电脑对弈(简单的AI操作)、每步15秒倒计时、玩家信息显示、下棋等的提示音、悔棋,重新开始,退出按键功能、简易的信息提示、界面拖动

代码已经托管至GitHub,有需要可以自己下载,传送门:https://github.com/fuxiai/FiveChessGame

2、实现步骤

1、先设计一个棋盘类,即棋盘部件,能够展现棋盘,鼠标移动提示下棋点,点击下棋等功能,并位该类提供必要的接口。

2、设计整个游戏窗口,通过QT设计模式,进行ui设计,如标题 、玩家信息、按键位置、棋盘位置等,为这些功能提供部件支持和布局,然后在代码当中对每个部件进行处理。

3、设计游戏运行的逻辑,如开始游戏、游戏先后手、对弈过程、结果判定、悔棋、重新开始与退出等功能。

4、对游戏界面进行美化,如添加背景,图片信息,修改按键样式,在最后去除窗口边框。

5、设计圆圈进度条部件类,为游戏对弈添加进度条时间提示。

6、添加系统提示音,如开始游戏声音、落子声音、结束声音。

7、升级电脑AI下棋算法。

8、为工程添加动态库支持,使用inno setup软件打包成一个安装包程序。

2.1棋盘的实现

2.1.1创建部件

整一个棋盘就是一个窗口部件,直接创建一个app项目继承于QWidget控件,此处不使用ui设计,以便最终可以得到该部件的类文件,方便移植到总窗口中。

image

image

工程创建完成后,就得到一个窗口啦!

2.1.2定义棋盘各类边界和信息

此处定义的棋盘为15x15大小,此大小使用宏定义或定义成变量,后边会经常使用。还需要定义多个间隔变量,如棋盘与边界的距离、棋格的大小,这样后期直接初始化中可以随意修改而不影响棋盘架构。定义枚举enum ChessType {noChess, blackChess, whiteChess};代表棋子类型,定义一块ChessType类型的二维数组空间chessInfo[15][15];该空间代表当前棋盘上存在的棋子信息。在构造函数中为定义的变量赋初值,然后根据下图示的变量信息设置窗口的大小,并固定窗口大小。

image

image

2.1.3绘制棋盘

QWidget部件具有绘画事件virtual void paintEvent(QPaintEvent *event),是个虚函数,我们的类是由QWidget类继承而来,能够对绘画事件进行重写,当发生重新绘制或者更新窗口等绘制事件就会响应。

2.1.3.1重写接口

类定义中添加:

  1 protected:
  2 	void paintEvent(QPaintEvent *e);
  3 

接口实现内容:

A、定义一个画家。

B、定义一只画笔,给画笔设置样式(颜色、线条宽度、线条类型),并把画笔给画家。

C、调用画家的drawPixmap方法绘制棋盘背景图片。

D、通用画家的drawLine方法依次画横竖各15条线组成棋盘网格。

E、查看当前棋盘上棋子信息,当存在棋子时,以网格坐标为准推导绘制位置,绘制相应的棋子。

  1 void ChessBoard::paintEvent(QPaintEvent *e)
  2 {
  3     QPixmap chessBoardPic(QString(":/images/chessBoard.jpg"));
  4     QPixmap blackChessPic(QString(":/images/black.png"));
  5     QPixmap whiteChessPic(QString(":/images/white.png"));
  6     QPainter painter(this);
  7     QPen pen;
  8     pen.setColor(Qt::black);
  9     pen.setWidth(2);
 10     pen.setStyle(Qt::SolidLine);
 11     painter.setPen(pen);
 12     painter.drawPixmap(0, 0, this->width(), this->height(), chessBoardPic);
 13 
 14     for(int i = 0; i < chequerNumOfLine; i++) {
 15         for(int j = 0; j < chequerNumOfLine; j++) {
 16             painter.drawLine(topW, topH + j * chequerSide,
 17                              topW + (chequerNumOfLine-1) * chequerSide, topH + j * chequerSide);
 18             painter.drawLine(topW + j * chequerSide, topH,
 19                              topW + j * chequerSide, topH + (chequerNumOfLine-1) * chequerSide);
 20         }
 21     }
 22     for(int i = 0; i < chequerNumOfLine; i++) {
 23         for(int j = 0; j < chequerNumOfLine; j++) {
 24             if (chessInfo[i][j] == blackChess) {
 25                 painter.drawPixmap(topW + i * chequerSide - chessSide/2, topH + j * chequerSide - chessSide/2,
 26                                    chessSide, chessSide, blackChessPic);
 27             }
 28             if (chessInfo[i][j] == whiteChess) {
 29                 painter.drawPixmap(topW + i * chequerSide - chessSide/2, topH + j * chequerSide - chessSide/2,
 30                                    chessSide, chessSide, whiteChessPic);
 31             }
 32         }
 33     }
 34     //this->update();
 35 }
painterEvnet()

2.1.3.2添加资源文件

在我们项目当中,需要用到许多的图片文件,需要把图片放置到工程文件夹下,然后为工程添加资源文件,就能通过相对路径的方式调用文件。

添加资源文件方法:

新建一个资源文件夹(文件->新建)。

image

然后工程目录下会生成资源文件夹

image

选择红框中的.qr文件,又键选中Open in Editor进入编辑模式添加文件。

image

资源文件调用时的相对路径是以:开头的,或着当不识别时,可以在以qrc:开头。

当我们paintEvent事件完成后,此时我们通过修改初始化时棋盘信息的内容就可以看到棋子的显示啦。

2.1.4鼠标捕获

我们下棋是用鼠标点击,当鼠标移动到下棋点的时候,需要让鼠标改变样式(此处由箭头变成手指),来框定是否是下棋点。具体方法是重写鼠标移动事件virtual void mouseMoveEvent(QMouseEvent *event)。当我们移动鼠标的时候就会产生该事件。鼠标移动事件默认是需要按住鼠标才会产生事件的, 所以需要修改相关属性。

打开鼠标的跟随属性:

  1 this->setMouseTracking(true); // 在初始化时,打开鼠标跟踪,则在窗口上只要移动鼠标就 //会产生鼠标移动事件。

代码实现:

  1 void ChessBoard::mouseMoveEvent(QMouseEvent *e)
  2 {
  3     // 去除边界无效跟踪
  4     if ((e->x() < (topW - rangeCenter) || e->x() > (topW * 2 + chequerSide * (chequerNumOfLine-1) - rangeCenter)) ||
  5          e->y() < (topH - rangeCenter) || e->y() > (topH * 2 + chequerSide * (chequerNumOfLine-1) - rangeCenter)  ) {
  6         this->setCursor(Qt::ArrowCursor);
  7         dropChessEnable = false;
  8         return;
  9     }
 10     // 根据每个落棋点改变光标格式
 11     int inChequerSideX = (e->x() - topW) % chequerSide;
 12     int inChequerSideY = (e->y() - topH) % chequerSide;
 13     dropW = (e->x() - topW) / chequerSide;
 14     dropH = (e->y() - topH) / chequerSide;
 15     if ((inChequerSideX < rangeCenter || inChequerSideX > (chequerSide - rangeCenter)) &&
 16         (inChequerSideY < rangeCenter || inChequerSideY > (chequerSide - rangeCenter))) {
 17         this->setCursor(Qt::PointingHandCursor);
 18         dropChessEnable = true;
 19         // 落点位置进一格
 20         if (inChequerSideX > (chequerSide - rangeCenter)) {
 21             dropW++;
 22         }
 23         if (inChequerSideY > (chequerSide - rangeCenter)) {
 24             dropH++;
 25         }
 26     } else {
 27         this->setCursor(Qt::ArrowCursor);
 28         dropChessEnable = false;
 29     }
 30 }
mouseMoveEvent(QMouseEvent *e)

2.1.5下棋操作

通过2.1.4步的操作,我们已经能得到是否可以下棋的标志dropChessEnable和当前下棋的实时网格坐标(dropW,dropH)。那么同理的,鼠标按键按下也有对应的事件函数能够重载--

virtual void mousePressEvent(QMouseEvent *event)。

当我们被允许下棋的时候,根据当前实时网格坐标,直接根据当前的棋子类型(黑/白),直接修改棋盘信息chessInfo[dropW][ dropH]内容给即可。每次下完后判断当前是否五连。

代码实现:

  1 void ChessBoard::mousePressEvent(QMouseEvent *e)
  2 {
  3     if (dropChessEnable == false || dropedFlag == false) {
  4         return;
  5     }
  6     if(e->button() == Qt::LeftButton) {
  7         if (chessInfo[dropW][dropH] == noChess) {
  8             chessInfo[dropW][dropH] = currentChess;
  9             emit chessDroped(dropW, dropH);
 10             dropedFlag = false;
 11             if(isGameOver(dropW, dropH)) {
 12                 emit boardGameOver();
 13             }
 14             this->update();
 15         }
 16     }
 17 }

到此,我们的棋盘就实现了,能够显示并且下棋。当然要想用到主窗口当中,为保证棋盘的封装性,还需要提供各种接口供主窗口调用,例如获取和修改棋盘信息,获取棋盘的相关规格信息,修改一些需要处理的标志位……;还需要添加下棋和游戏结束判定的信号供给主窗口的槽函数响应。

posted @ 2020-07-23 18:43  小懒虫alex  阅读(3895)  评论(0编辑  收藏  举报