QT5/C++项目:基于QT的跨平台网络对战象棋(二)(推荐★★★★)
QT5/C++项目:基于QT的跨平台网络对战象棋(二)(推荐★★★★)
文章目录
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
本篇副标题:
基于QT的跨平台网络对战象棋项目之界面设计、功能需求分析、框架设计、模块设计
本篇博客讲了什么or解决了什么问题?
分析和思考了基于QT的中国象棋游戏的的需求分析、功能个模块设计、细节设计等实现
项目简介:
为毕业需要一份毕业设计;提高自己对于C/C++语言的熟悉和运用**;**加强跨平台QT的框架熟悉,和熟悉掌握完整的项目开发流程;写一些个人而言比较大的项目,用于面试时候的底气和经验;规范编码格式,和逻辑思维锻炼。
项目特色:
- 开源
- 免费
- 使用 QT 开发界面美观
- 无广告和内置付费充值 VIP 等
- 可以支持基本所有的主流系统的版本分布
- 大学自学的语言,实现项目,检验自己的学习效果
- 提供网友和和我一样的自学者一个经验和心力路程的分享
补充:仔细想一想,你用的手机或电脑有没有一款软件,至少启动页没有广告、或者付费等行为,没有广告和付费,所以这款软件超级良心的好不好。 且开源意味着免费,互相学习,接收大众的监督。
实现功能:
该项目主要功能模块分为玩家与自己对战、玩家与电脑AI对战、多人网络对战、对战计时、关于作品信息。实现了在单机或联网状态下,无论是单人还是多人,无论使用系统是否相同,均可以实现象棋游戏功能。
责任描述:
该项目从0开始写的代码,最开始的从类的架构设计,到功能模块的编码的实现,到游戏的测试和游戏其他平台的移植,均由自己独立完成。亦发布在博客、bilibili和github上提供详细展示。
其他:
无
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
项目演示效果:
视频演示:
Qt5_ChinessChess 基于QT的跨平台网络象棋对战演示
图片演示:
开发平台环境:
windows 10 专业版、Qt Createor 4.7.1、Enigma+Virtual+Box+7.80、VMware Workstation Pro、GitHub Desktop
[外链图片转存失败(img-yAjQNuv6-1563446690699)(data:image/gif;base64,R0lGODlhAQABAPABAP///wAAACH5BAEKAAAALAAAAAABAAEAAAICRAEAOw==)]
项目下载地址:
源码:https://github.com/touwoyimuli/2019_01_Qt5_ChinessChess
成品:https://github.com/touwoyimuli/2019_01_Qt5_ChinessChess/releases
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
项目思路分析:
需求以及实现:
(1)需求一:界面美观,操作简易。
需求实现:界面整体分为三大板块;其中主界面大概以7:3分布其中主要的两大板块。第三板块“关于作者”以另外的小的界面单独显示。且只有三个点击按钮。和鼠标点击棋子不规则时候会无反应。符合下棋规则棋子才会移动,故操作简单。
(2)需求二:下载容易,支持多种主流的操作系统平台
需求实现:使用QT来开发,本就支持许多的跨平台。然后当某一平台成功之后,只需要在其他平台也重新按照其平台流程发布一遍即可以。
(3)需求三:免安装,程序体积小
需求实现:采用动态编译,将所需要的库函数文件已打包压缩成一个绿色的运行程序以实现上面需求。
(4)需求四:没有广告或者付费充值内容
需求实现:个人兴趣开发作品,开发此款作品的一个理念是非盈利和开源。
(5)需求五:易于下载,且一定是正版发布
需求实现:将其在作品和源码在github上面发布,确保用户下载一定是本人发布版本,而非是旁人可能**之后的修改版。
功能流程图:
具体的功能流程图如下图所示:
(1)玩家和自己、电脑AI对战功能流程示意图
(2)多人网络游戏战功能流程示意图
系统流程图:
具体的系统流程图如下:
总体设计
设计思路框架
对于这个基于QT的中国象棋游戏项目的项目,因QT本身提供的良好的封装机制,使得它的模块化非常高,可重用性良好;鉴于此优点和大学期间学过的“软件设计”一门课,使得在开发时候也选取模块化设计,实现充分的代码的重复利用性;且尽量使得程序代码之间的组合,达到高内聚、低耦合的效果。
其中中国象棋的整体结构如下图所示:
由上面的功能结构图可知,其中大致分为三个部分:①多种游戏模式部分,②对战计时部分,③作品信息部分。而这其中每一个部分都是有细分为其他的多个子部分,其中每一部分的详细框架结构如下。
(Ⅰ)多种游戏模式部分,其本质计时下象棋对战部分,是整个项目的最核心区域地方。其中又是细分为三大子模块[4]:
(一)玩家和自己对战:包含有棋子和基础的棋盘类,从而实现玩家和自己对战的详细游戏规则。
(二)玩家和AI对战:继承前面的玩家和自己对战类,然后做相应的重载和扩充里面的功能哈数,修改成自己的玩家和AI对战的详细游戏规则。
(三)多人网络对战:同样是继承前面的玩家和自己对战类,然后做相应的重载和扩充里面的功能哈数,修改成多人网络对战的详细游戏规则。
(i)服务器端:还有相应的Socket套接字网络编程,给客户端发送自己下棋的步骤信息。
(ii)客户端:也有相应的Socket套接字网络编程,给服务器端端发送自己下棋的步骤信息。
(Ⅱ)对战计时部分,其核心本子就是一个独立运行的计时器功能部分。用于游戏时间的计时。
(Ⅲ)作品信息部分,其核心本质就是关于作者部分,即就是一个独立的Dialog作品详细介绍部分。
其中一开始,先是设计一个单纯的棋子类Class:ChessPieces,其中只包含每一个棋子的基础信息;
然后后面新的棋子结构体Struct:POS来包含这个Class:ChessPieces,可以被后面设计的棋盘类所调用,进行初始化。
紧接着就是结构体Struct:POS的作用是被后面的 棋盘类【Class:ChessBoard 玩家和自己对战类】;以及后来继承它的的两个子类 【Class:MachineGame 玩家和AI对战类 和 Class:NetworkGame 双人网络对战类】)来进行调用。
其中在各自三种棋盘类【Class:ChessPieces、 Class:MachineGame、Class:NetworkGame 】里面分别详细定义和重载自己所需要的功能函数。比如将里面的玩家A和玩家A对战规则;继承之后修改为玩家A电脑AI对战的规则;又或者是继承重载之后,修改为玩家A和玩家B进行跨网络对战的规则。
到此为止,整个游戏棋盘的核心下象棋的基础框架就出来了。接下来就是两个独立的模块,在逻辑上面没有太大关联。对于计时模块,利用QT提供的一些控件模块。来完成该相应的计时部分的功能。最后就是关于作者部分,单独的显示一个窗口出来,做一些特效出来,添加一些链接或者文字和图片等,来展示此作品的一些详细信息。
界面UI设计
整个界面是选用QT自带的Dilog界面和Widget界面背景。其中象棋绘画的部分,是调用系统底层的API函数来绘画的,通过相应的接口来处理进行一些特效,比如颜色和透明度的处理。对于计时器部分,添加好透明背景,作为相应的皮肤,且选用素材图片都是可以免费商用的素材。最后就是关于作者部分界面设计,就是采用的小界面小弹框模式的界面设计,来显示相关作者和作品信息。
数据存储设计
对于这个里面的相关数据的储存,因为所用到的数据量比较小,所以就直接去掉了采用关系型数据库的考虑,采用数据结构和容器来存储临时的数据。结构简单,且可以加深自己对于数据结构的灵活的运用。
类的继承关系图
具体各个类的详细设计预作用
其中主要是有如上图的几个类,其中每详细类的作用如下:
a) 项目架构名称:**ChineseChess **设计作用:**作为一个总的解决方案。其解决方案下面的项目总名称,有着对开发的项目的最简洁的称呼说明。 类视图: **说明:**每一个项目都首先创建的,作为一个文件夹,里面用来存放所有项目的.h、.cpp、.pro、图片等其他一系列的资源。
b) **类名:**POS **设计作用:**作为一个单独的Struct结构体,用来存放棋子的一个三要素,棋子的名称和棋盘里面横纵坐标。
类视图: **说明:**是一个Struct结构体。
c) **类名:**AboutAuthor **设计作用:**作为项目里面,展示关于作者和作品详细信息,属于一个独立的模块。
类视图: **说明:**是一个Class类。且里面主要是空白,其中主要信息是在一同创建的AboutAuthor.ui里面。
d) **类名:**ChessBoar **设计作用:**其是整个项目最核心的一个类,也是整个项目里面最为复杂的一个类。其中包含有多个结构体和类,棋子类等。其所负责主要功能是绘画棋盘和棋子,对所有棋子进行初始化,定了各个棋子的下棋规则制定。同时完成游戏模式之一的玩家与自己对战模式。
类视图: **说明:**是一个Class类。作为后面的MachineGame玩家与电脑AI对战和NetworkGame多人网络对战的父类。
e) **类名:**ChessPieces **设计作用:**作为单独设计的棋子类,包含棋子的颜色、唯一ID属性、是否死亡等状态属性。
类视图: **说明:**是一个Class类。和ChessBoar类是包含关系。
f) 类名:ChessStep **设计作用:**棋子步骤类,主要是使用后面的容器而准备的。只有基本棋子击杀状态、现在棋盘行列值和目标前往的行列值。
类视图: **说明:**是一个Class类。
g) **类名:**ChooseMainWindow **设计作用:**作为点击游戏开始,就会弹出来的一个选择窗口,关于选择哪一个游戏模式。然后设计最为选择游戏主要窗口。 类视图: **说明:**是一个Class类。
h) **类名:**MachineGame **设计作用:**作为游戏的玩家与电脑AI对战的模式的实现。 类视图: **说明:**是一个Class类。也是ChessBoar派生类。
i) **类名:**NetworkGame **设计作用:**作为游戏的多人网络对战的模式的实现。 类视图: **说明:**是一个Class类。也是ChessBoar派生类。其中若是在不同电脑之间测试。记得修改网络协议的,所连接的服务器的IP;当然若是发现端口被占用,亦可以修改成一个大于数值(范围是从0 到65535),建议大于6000。
**类名:**SelectGameMode **设计作用:**选择游戏模式的Dialog界面。 类视图: **说明:**是一个Class类。是一个手写的带.ui文件,而非使用设计师设计的。
详细功能结构模块设计
整体的功能结构图设计如下
选择游戏方式
整体思路:
作为最主要、最核心的基类,也是最开始就单独保持运行的玩家和自己对战的游戏模块。要包含有棋子类,在创建和绘画棋盘的时候,要在构造函数里里面对32颗棋子进行初始化赋值。
步骤:
(1)绘画棋盘:使用系统自带的API函数接口,调用画家Qpainter、画笔drawLine。以及相关的设备上下文等,来绘画初步的函数图像。
(2)绘画棋子:对棋子进行初始化赋值,使得他们分别固定在对应的棋盘位置
(3)棋盘行列值和屏幕之间的像素值之间进行切换:创建调用的功能函数,为后面的点击屏幕和点击棋子和点击棋盘具体位置的,之间进行转换
(4)象棋轮流下:设置BOOL变量值,当属于红方或者黑方下了一步棋之后,就通过限制使得其对方下载,继续进行游戏,才能够接着下棋
(5)制定象棋的具体规则:要设置每一种棋子对应的走法规则。
(6)屏幕重绘:当每点击一次棋盘或者棋子之后,和发生鼠标键盘交互事件的时候,就会调用一次系统屏幕的刷新,来进行,屏幕的重绘。
(7)判断谁胜谁负:专门写一个功能函数,看看红方或者黑方的所有棋子均无路可走或者己方的将被击杀,则会判定对方胜利,自己失败,从而结束游戏。
局部核心代码:
class ChessBoard : public QWidget
{
Q_OBJECT
public:
explicit ChessBoard(QWidget *parent = 0);
~ChessBoard();
bool isDead(int id);
int getStoneId(int row, int col);
int getStoneCountAtLine(int row1, int col1, int row2, int col2); //车 炮 的功能辅助函数 判断两个点是否在一个直线上面,且返回直线之间的棋子个数
void whoWin(); //谁胜谁负
bool isChecked(QPoint pt, int& row, int& col); //是否选中该枚棋子。pt为输入参数; row, col为输出参数
public:
QPoint center(int row, int col); //象棋的棋盘的坐标转换成界面坐标
QPoint center(int id);
virtual void paintEvent(QPaintEvent *); //绘画棋盘
void drawChessPieces(QPainter& painter, int id); //绘画单个具体的棋子
virtual void mousePressEvent(QMouseEvent *); //鼠标点击事件
virtual void clickPieces(int checkedID, int& row, int& col);
//象棋移动的规则[将 士 象 马 车 炮 兵]
bool canMove(int moveId, int killId, int row, int col);
bool canMoveJIANG(int moveId, int killId, int row, int col);
bool canMoveSHI(int moveId, int killId, int row, int col);
bool canMoveXIANG(int moveId, int killId, int row, int col);
bool canMoveMA(int moveId, int killId, int row, int col);
bool canMoveCHE(int moveId, int killId, int row, int col);
bool canMovePAO(int moveId, int killId, int row, int col);
bool canMoveBING(int moveId, int killId, int row, int col);
}
运行效果:
玩家和AI对战模块
整体思路:
提过上面的Class:ChessBoard类已经实现了两方进行象棋的轮流博弈。所以我们在这里的类通过继承Class:ChessBoard即可以了。接下来就是通过重载鼠标和键盘点击是事件,分别来判断是玩家还是电脑系统AI来进行选择棋子和走棋子。
步骤:
(1)重载鼠标点击事件:判断是玩家下棋还是电脑机器AI来进行下棋。
(2)计电脑AI的走法:
(a)计算出所有可能移动棋子的方法:当轮到AI选择棋子和移动棋子的时候。首先判断出AI所有可以移动的棋子,通过void getAllPossibleMoveStepAndNoKill ( Qvector <ChessStep*>& steps )来计算出获得所有可能的移动步骤(不击杀对方棋子)、以及通过 void getAllPossibleMoveStep (Qvector < ChessStep > & steps)来计算出获得所有可能的移动步骤(击杀对方棋子)的走法,然后通过void saveStep(int moveID, int checkedID, int row, int col, QVector<ChessStep>& steps)来保存这里面的所有可能走法, 以及每一个可以移动棋子的可能走法,将其全部走法都保存在容器QVector<ChessStep*>& steps里面[7]。
(b)假设移动棋子:移动可走的算法里面的某具体的一个棋子的走法
©计算当局面分:通过你自己自定义的“有利局面”,用一个分数本来表示有利的大小。这里面具体怎么计算这个有利局面的局面分,就占到了很大一部分比例关于AI有多么的“聪明”。
(d)撤回当前移动的棋子:通过悔棋步骤,撤回该棋子。
(3)AI移动最有利一步棋子:通过遍历可走算法的保存下来的多个可能,然后计算每一个可能得局面分,选择局面分最大的一步走法作为电脑AI最佳的走法[8]。
(4)轮流博弈下棋:在玩家和电脑AI之间,轮流下棋,直到棋局出现胜负局面, 以终止游戏运行。
局部核心代码:
判断是玩家游戏还是AI游戏,并且进行轮流循环:
void MachineGame::clickPieces(int checkedID, int &row, int &col)
{
if(m_bIsRed) //红方玩家时间
{
chooseOrMovePieces(checkedID, row, col);
if(!m_bIsRed) //黑方紧接着进行游戏
{
machineChooseAndMovePieces();
//ToDo: 机器 黑方时间
}
}
}
获得最好的移动步骤:
ChessStep* MachineGame::getBestMove()
{
int maxScore = -10000;
ChessStep* retStep = NULL;
//------------------------
//有可击杀的红棋子就走击杀红棋子最优的一步
// 1.看看有那些步骤可以走
QVector<ChessStep*> steps;
getAllPossibleMoveStep(steps); // 黑棋吃红棋的所有可能的步骤
//------------------------
//没有可击杀的红棋子就走最后的一步
QVector<ChessStep*> stepsAndNoKill;
getAllPossibleMoveStepAndNoKill(stepsAndNoKill); // 黑棋移动所有可能的步骤(不吃红棋子)
//2.试着走一下
for(QVector<ChessStep*>::iterator it = steps.begin(); it!=steps.end(); it++)
{
ChessStep* step = *it;
fakeMove(step);
int score = calcScore(); //3.计算最好的局面分
unFakeMove(step);
if(score > maxScore)
{
maxScore = score;
retStep = step;
}
}
if(retStep != NULL)
return retStep;
//2.试着走一下
//从这种不击杀红棋子,只是单纯移动黑棋steps里面,随机抽选一种进行下棋
int nStepsCount = stepsAndNoKill.count();
qsrand(QTime(0,0,0).secsTo(QTime::currentTime())); //随机数种子, 0~MAX
int temp =qrand()% nStepsCount;
QVector<ChessStep*>::iterator it = stepsAndNoKill.begin();
retStep = it[temp];
if(retStep == NULL)
whoWin();
//4.取最好的结果作为参考
return retStep;
}
运行效果:
双人网络对战(服务器端)模块
整体思路:
同样是继承上面的思路,首先通过继承类Class:ChessBoard,然后改写部分虚函数,和添加一些额外的扩展来实现双人对战的类Class:NetworkGame。然后将自己所选中的棋子和走法通过TCP协议,Soctck套接字来传输相关的这些信息,然后对方刷新他自己的棋盘界面。然后下完棋之后仍然通过网络Soctck套接字将棋子和下法传输过来,再然后刷新自己的界面。这样往返交替循环下棋,达到局域网里面通信的下棋要求。
步骤:
1.创建服务器的QtcpServer的m_tcpServer的套接字:制定使用QhostAddress :: Any的协议族,和端口。绑定主机和选择通信协议和接接口
2.监听指定网段:监听网络里面其他客户机器,是否发来连接请求
3.连接请求的客户机:通过connect()连接服务器和客户机,建立通信连接
局部核心代码:
服务器端创建网络连接:
NetworkGame::NetworkGame(bool isServer)
{
m_tcpServer = NULL;
m_tcpSocket = NULL;
if(isServer) //作为服务器端
{
m_bIsTcpServer = true;
m_tcpServer = new QTcpServer(this);
m_tcpServer->listen(QHostAddress::Any, 9999);
connect(m_tcpServer, SIGNAL(newConnection()),this, SLOT(slotNewConnection()));
}
else //作为客户端
{
.......
}
}
运行效果:
双人网络对战(客户端)模块
整体思路:
整体思路和服务器无差别,只不过是将客户端将消息发送到服务器。
步骤:
1.创建套接字 :创建客户端的QtcpSocket套接字的m_tcpSocket。
2.连接服务器:因为只有一台电脑设备,这里是固定服务器地址为127.0.0.1。连接这一台电脑。
3.进行网络对战:进行游戏联机对战,一直到游戏运行结束。
局部核心代码:
客户端进行网络连接:
NetworkGame::NetworkGame(bool isServer)
{
m_tcpServer = NULL;
m_tcpSocket = NULL;
if(isServer) //作为服务器端
{
……
}
else //作为客户端
{
m_bIsTcpServer = false;
m_tcpSocket = new QTcpSocket(this);
m_tcpSocket->connectToHost(QHostAddress("127.0.0.1"), 9999);
connect(m_tcpSocket, SIGNAL(readyRead()), this, SLOT(slotRecv()));
}
}
运行效果:
说明:
这里客户端设置的是连接局域网里面的127.0.0.1,端口为9999服务器
若在两台实体客户机(可以是运行的系统不同)里面运行该程序。则是需要修改
对战计时模块
整体思路:
利用Qt自带控件QLCDNumber来充当计时的显示屏,QpushButton按钮来作为启动开始和结束的开关。点击开始按钮,计时器后台开始工作。然后每隔一秒钟就刷新一下显示界面。使得看起来计时器是在工作。再点击一下暂停来停止,点击一下重置就会清零计时器的显示时间为00:00,可以开始下一轮的计时。
步骤:
1.拖拉控件制作:拖拽设计界面的控件,做好布局的显示准备
2.定义槽函数:定义各个按钮需要的对应功能,设计成为槽函数[9]
3.连接信号与槽:使用connnect()连接控件发射的信号,和实施对应的槽函数,已完成相应的功能。
局部核心代码:
初始化计时器:
m_timer = new QTimer; //初始化定时器
m_timeRecord = new QTime(0, 0, 0); //初始化时间
m_bIsStart = false; //初始为还未计时
connect(m_timer,SIGNAL(timeout()),this,SLOT(updateTime()));
刷新时间槽函数
void ChessBoard::updateTime()
{
*m_timeRecord = m_timeRecord->addSecs(1);
ui->lcdNumber->display(m_timeRecord->toString("hh:mm:ss"));
if(m_bIsStart == false)
{
ui->pushButton_start->setText("开始");
}
else if(m_bIsStart == true)
{
ui->pushButton_start->setText("暂停");
}
}
开始、暂停计时槽函数
void ChessBoard::on_pushButton_start_clicked()
{
if(!m_bIsStart) //尚未开始 开始计时
{
m_timer->start(1000);
}
else //已经开始,暂停
{
m_timer->stop();
}
m_bIsStart = !m_bIsStart;
}
运行效果:
关于作者模块
整体思路:
单独的设计一个继承于Widget界面的窗口,然后在里面写上一些想要显示的汉字文字,然后使用一个Qtextbrowser来承接这些,因为可以使得内容信息显示处于多行显示,和外加添加一些连链接、图片等。最后设置对应控件和背景图的层次关系,以及透明度等,就相当于设计好了带的皮肤,给人眼前一亮的感觉。
步骤:
1.拖拽相关控件
2.在控件里面写好相关的内容
3.设置槽函数
4.连接控件发出的信号和对应的槽函数,施行相应的功能。
局部核心代码:
初始化作者类:
m_pAbout = new AboutAuthor(); //关于作者
关于作者槽函数
void ChessBoard::on_pushButton_about_clicked()
{
m_pAbout->setWindowTitle("关于作者");
m_pAbout->show();
}
运行效果:
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
其他细节设计
多界面之间的切换实现
主要就是一开始点击运行程序。提供一个游戏选择模式窗口,当选择结束之后;在编码部分,将选中的对象设置为生存状态,至少要保证其在运行期间创建的变量对象一直都在,当结束的时候,再来释放该对象分配的许多内存。这里需要注意的是,推荐是使用New一个产生的变量,而不是设置为static的全局变量,且还要将其显示为.show()状态,也不要忘记将次之前已经选择的,不需要的窗口对象使用delete
释放掉,且外加.hide()隐藏起来。这样子就可以实现在多个界面之间窗口的来回切换
添加界面透明皮肤
其实这个功能,是属于看起来困难,但是实施起来比较非常容易的一个步骤,大概简单的就是:在设计师界面,右键点击属性,插入一张图片,设置为置低下一层即可。
给运行程序添加一个自定义图标
这一功能的实现,有比较多的简单方法,这里我挑选一个比较·简单的方法说明我的实现。首先选择自己需要的个性化图片或者Logo的图标,然后采用在线格式转换没设置成.icon的格式图片,一般推荐大小为32x32或者64x64大小的。然后将其图片文件复制命令拷贝好到项目的根目录下面,然后利用记事本新建一个名为logo.rc的文件,logo.ico为转换后的图标名称,内容如下:
IDI_ICON1 ICON DISCARDABLE “log0.ico”;紧接着就是在工程文件夹中新建一个images目录,将logo.ico、logo.rc放入文件夹中。再打开QT工程,将logo.ico、logo.rc添加进工程。然后在工程文件(*.pro)中加入一行:RC_FILE=images/logo.rc最后一步重新建工程,这时QT程序就有了一个漂亮的外观了[11],程序快捷方式效果如下图所示:
//+++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
上一篇博文:
下一篇博文:
上一篇: Android 开源项目分类汇总