基于QT与C++的中国象棋游戏开发实战:从MVC架构到AI算法

发布时间:2026/8/3 12:09:24
基于QT与C++的中国象棋游戏开发实战:从MVC架构到AI算法 1. 项目概述为什么选择QT与C来复刻中国象棋如果你对桌面应用开发感兴趣或者想找一个能串联起C面向对象编程、图形界面设计和游戏逻辑的实战项目那么用QT和C来开发一个中国象棋游戏绝对是一个“黄金级”的练手选择。这不仅仅是因为象棋规则本身逻辑清晰、复杂度适中更因为通过这个项目你能把学校里学的那些看似枯燥的C类、继承、多态、STL容器以及QT的信号槽、绘图事件、控件布局等知识全部串联起来变成一个看得见、摸得着、能运行、能对战的完整程序。我当初选择这个项目就是厌倦了控制台的黑白世界。QT框架提供了强大的跨平台图形界面能力让你能用C写出媲美现代软件的界面而象棋的棋盘、棋子、走法规则恰好是检验你数据结构设计和算法逻辑的绝佳试金石。这个项目麻雀虽小五脏俱全它涉及界面渲染QT绘图、事件处理鼠标点击、核心算法棋规判定、对象建模棋子类甚至还能扩展到网络对战Socket通信和AI对战搜索算法。对于想找C/QT方向工作的同学来说这样一个完整的项目经历写在简历上比空谈“熟练掌握C”要有说服力得多。接下来我会带你从零开始拆解整个开发过程。我会重点分享我在实现过程中踩过的坑、做出的设计抉择以及那些教科书里不会写的调试技巧。我们的目标是最终得到一个界面美观、逻辑正确、代码结构清晰并且具备一定可扩展性的中国象棋程序。2. 整体架构与核心模块设计在动手写第一行代码之前花点时间思考整体架构是至关重要的。一个混乱的架构会让后续的添加功能比如悔棋、网络对战变得举步维艰。我的设计核心是“模型-视图-控制器MVC”的变体在QT项目中这通常演化为数据模型、界面视图和业务逻辑分离。2.1 核心类设计整个项目我主要设计了以下几个核心类ChessPiece棋子类这是所有棋子的基类。它应该包含棋子的基本属性颜色红方或黑方、类型车、马、炮等、在棋盘上的位置行列坐标、是否已被吃掉的状态。更重要的是它需要有一个纯虚函数比如bool canMoveTo(int targetRow, int targetCol, const ChessBoard board)用于判断该棋子能否移动到目标位置。然后为每种棋子车、马、象、士、将、炮、兵创建派生类重写这个移动规则函数。这是多态性的经典应用。ChessBoard棋盘类这是游戏的核心数据模型。它不应该直接处理界面绘制而是专注于维护游戏状态。其核心成员是一个二维数组或std::vectorstd::vectorstd::shared_ptrChessPiece来表示9x10的棋盘格点存储着棋子对象的指针空指针表示无子。它还应该记录当前行棋方红先黑后、棋局状态进行中、红胜、黑胜、和棋以及一个走法历史记录列表用于实现悔棋。GameController游戏控制类这是连接数据ChessBoard和界面MainWindow的桥梁是业务逻辑的集中地。它负责响应用户的界面操作如点击棋子调用ChessBoard和ChessPiece的规则进行逻辑判定并更新棋盘数据最后通知界面重绘。它也是处理游戏流程开始、结束、悔棋、重新开始的总指挥。MainWindow主窗口类由QT的QMainWindow派生而来主要负责界面展示。它包含一个用于绘制棋盘和棋子的自定义Widget例如ChessWidget以及一些按钮开始、悔棋、认输等。它的职责是接收鼠标事件将其转化为逻辑坐标后传递给GameController并根据控制器的指令更新界面。2.2 数据流与信号槽QT的灵魂是信号与槽Signals Slots我们用它将各个模块松耦合地连接起来。用户交互流用户在ChessWidget上点击鼠标 -ChessWidget捕获鼠标事件计算点击的棋盘格 -ChessWidget或MainWindow发出一个自定义信号如pieceClicked(int row, int col)-GameController的对应槽函数接收该信号 - 控制器调用ChessBoard的接口处理走法逻辑 - 逻辑处理后控制器发出另一个信号如boardStateChanged()-ChessWidget接收到状态变化信号调用update()触发重绘从最新的ChessBoard数据中读取并绘制棋盘棋子。设计心得尽量避免在视图类ChessWidget里直接操作模型数据ChessBoard也避免在模型类里直接调用界面更新。所有修改都通过控制器中转。这样做的好处是假设未来我想把界面从QT换成其他库或者为游戏添加一个命令行界面我只需要替换视图部分核心的ChessBoard和GameController逻辑几乎不用改动。这就是关注点分离带来的好处。3. 核心实现细节与难点攻克有了架构蓝图我们来深入几个最关键、也最容易出错的实现细节。3.1 棋盘与棋子的绘制绘制是给用户的第一印象。我们使用QT的QPainter在ChessWidget的paintEvent函数中完成。棋盘绘制计算好棋盘左上角的起始坐标根据棋盘格大小比如60像素绘制9条竖线和10条横线。使用QPainter::drawLine即可。楚河汉界可以用QPainter::drawText来绘制文字。棋子绘制这是重点。我推荐使用贴图而非纯色绘制效果更好。为红黑两方的7种棋子准备14张透明的PNG图片。在绘制时遍历ChessBoard中的二维数组。对于每个非空的位置根据棋子的颜色和类型选择对应的图片。计算该棋子在屏幕上的圆心坐标centerX startX col * gridSize gridSize/2。使用QPainter::drawPixmap将图片绘制在指定区域可以以圆心为基准进行绘制。踩坑提示1抗锯齿与高质量渲染直接绘制线条和图片可能会有锯齿。务必在QPainter对象使用前设置渲染提示painter.setRenderHint(QPainter::Antialiasing); // 抗锯齿 painter.setRenderHint(QPainter::SmoothPixmapTransform); // 平滑像素图变换这能让你的棋盘和棋子边缘看起来非常平滑质感提升一个档次。棋子选中效果当用户点击一个己方棋子时需要高亮它。我常用的方法是在棋子图片外围绘制一个半透明的圆环。在绘制棋子图片后判断如果该棋子是被选中的棋子则用QPainter设置一个半透明的笔刷QBrush(QColor(255, 255, 0, 100))和较粗的画笔绘制一个比棋子略大的圆。3.2 象棋规则的精确定义与实现这是项目的逻辑核心必须严谨。每种棋子的移动规则都在其派生类的canMoveTo函数中实现。这里以几个易错点为例马的走法马走日与蹩马腿 马可以走“日”字。但关键在于如果马要前往的某个目标点其前进方向上的“相邻点”马走日的第一步落脚点有任意棋子则不能走俗称“蹩马腿”。// 伪代码示例判断马从(r1,c1)到(r2,c2)是否合法 int dr abs(r2 - r1); int dc abs(c2 - c1); if (!((dr 2 dc 1) || (dr 1 dc 2))) { return false; // 不是日字格 } // 检查蹩马腿 int blockRow, blockCol; if (dr 2) { // 竖向走日 blockRow (r1 r2) / 2; blockCol c1; } else { // 横向走日 blockRow r1; blockCol (c1 c2) / 2; } if (board.getPieceAt(blockRow, blockCol) ! nullptr) { return false; // 马腿被蹩 } return true;炮的走法炮打隔山子 炮移动时路径上必须无子和车一样。但吃子时目标位置和目标位置与炮之间必须有且仅有一个棋子即“炮架”。// 伪代码判断炮的移动/吃子 int piecesBetween countPiecesBetween(r1, c1, r2, c2, board); ChessPiece* targetPiece board.getPieceAt(r2, c2); if (targetPiece nullptr) { // 移动 return piecesBetween 0; } else { // 吃子 return piecesBetween 1; }将/帅的走法与“王不见王” 将帅只能在九宫内移动每次一步。此外还有一个特殊规则如果双方将帅在同一直线上且中间没有任何棋子则不允许形成“对脸”或称为“王不见王”此时先手方可以直接获胜或判负规则可自定义。这个规则需要在GameController进行走法验证时作为一个全局规则额外检查。踩坑提示2坐标系统一内部逻辑数组下标和界面绘制像素坐标一定要建立清晰的映射关系并封装成函数。我定义棋盘逻辑坐标是(row, col)row从0到9从上到下col从0到8从左到右。鼠标点击的像素坐标(x, y)需要通过转换函数pixelToLogic(x, y)得到逻辑坐标(r, c)再进行逻辑判断。反之绘制时需要logicToPixel(r, c)。混乱的坐标系统是BUG的主要来源之一。3.3 游戏状态管理与流程控制GameController是游戏的大脑它需要处理回合切换每次成功走子后切换当前行棋方从红方切换到黑方或反之。胜负判定在每次走子后检查是否擒获了对方的将/帅。更复杂的判定还包括长将、长捉等竞赛规则作为初级项目可以只实现“将死”判定。走法历史记录为了实现悔棋需要记录每一步的完整信息。我定义了一个MoveStep结构体包含起始位置、目标位置、被吃掉的棋子如果有。使用一个std::vectorMoveStep来保存历史。悔棋时从末尾弹出一步并根据记录的信息恢复棋盘状态。游戏重置将ChessBoard的二维数组重置为初始布局清空历史记录当前行棋方设为红方。4. 功能扩展与性能优化思路一个基础版本完成后你可以考虑以下扩展让项目更加出彩4.1 网络对战功能这是从单机游戏迈向联机游戏的关键一步。核心是引入网络模块采用客户端-服务器C/S或点对点P2P架构。C/S架构开发一个简单的服务器负责匹配玩家、转发棋步、校验规则可选。两个客户端分别连接服务器只负责界面和发送/接收棋步。服务器可以用C配合Boost.Asio或QT自身的QTcpServer/QTcpSocket实现。P2P架构一方作为主机另一方连接主机的IP和端口。棋步数据直接在两个客户端间通过Socket传输。QT的QTcpSocket可以胜任。协议设计你需要设计一个简单的应用层协议来传递消息。例如可以用字符串格式如MOVE:2,7,4,7表示从(2,7)移动到(4,7)CHAT:Hello表示聊天信息。更规范的做法是使用二进制协议或JSON。实操心得网络同步与状态验证网络对战的难点在于状态同步和防作弊。一个简单的策略是权威服务器或主机权威。即只有服务器或主机进行完整的规则校验客户端只做初步校验如是否轮到本方走子和发送意向。主机校验通过后再将合法的棋步广播给双方双方根据这个权威指令更新本地棋盘。这样可以避免因客户端被篡改而导致的规则破坏。4.2 集成简单AI对手实现一个“人机对战”模式会极大提升项目的技术含量。这涉及到基本的搜索算法。评估函数首先你需要一个函数来评价当前棋盘局面对于某一方的好坏。最简单的评估可以基于棋子价值车9马4.5炮4.5象/士2兵1将无穷大和棋子位置加分。搜索算法从最简单的极大极小搜索Minimax开始。AI黑方试图最大化评估值而假设玩家红方会最小化评估值。算法递归地模拟未来几步的所有可能走法选择对AI最有利的。性能优化纯Minimax搜索深度非常有限。必须引入Alpha-Beta剪枝来大幅减少需要搜索的节点数。即使这样搜索深度可能也只能达到4-6层但对于初级AI已经足够。实现步骤为AI生成所有合法走法。对每个走法模拟执行调用Minimax(深度-1)递归搜索。在搜索叶子节点深度为0或游戏结束时调用评估函数。选择评估值最优的走法执行。// 极小极大算法伪代码框架 int minimax(ChessBoard board, int depth, bool isMaximizingPlayer) { if (depth 0 || gameIsOver(board)) { return evaluate(board); // 评估函数 } if (isMaximizingPlayer) { int maxEval -INFINITY; for (Move move : generateAllMoves(board, BLACK)) { // AI是黑方 board.makeMove(move); int eval minimax(board, depth - 1, false); board.undoMove(move); maxEval max(maxEval, eval); } return maxEval; } else { int minEval INFINITY; for (Move move : generateAllMoves(board, RED)) { // 玩家是红方 board.makeMove(move); int eval minimax(board, depth - 1, true); board.undoMove(move); minEval min(minEval, eval); } return minEval; } }4.3 性能与内存优化对于象棋游戏性能瓶颈主要出现在AI搜索时。除了Alpha-Beta剪枝还可以考虑走法生成优化预计算每种棋子在空棋盘上所有可能的移动偏移量在实际生成时再结合棋盘状态判断合法性比每次都从头计算快。置换表使用哈希表存储已经搜索过的棋盘局面及其评估结果避免重复搜索。这需要为棋盘状态生成一个唯一的Zobrist哈希值。智能指针管理在ChessBoard中使用std::shared_ptrChessPiece来管理棋子对象可以方便地处理棋子的创建和销毁如被吃掉时避免内存泄漏。注意避免循环引用。5. 开发环境搭建与调试技巧工欲善其事必先利其器。一个顺手的开发环境能事半功倍。5.1 QT与C环境配置QT安装直接从QT官网下载开源版本或安装器。对于初学者建议安装带有QT Creator IDE的版本它集成了设计、编码、调试、编译于一体对QT项目支持非常好。版本选择上QT 5.15 LTS或QT 6.2都是稳定可靠的选择。编译器在Windows上安装QT时会自动勾选安装MinGW编译器。你也可以使用MSVCVisual Studio编译器这需要你事先安装Visual Studio即使是Build Tools版本。在Linux上使用系统的g即可。在macOS上使用Xcode的命令行工具。项目管理QT使用.pro文件qmake或CMakeLists.txtCMake来管理项目。对于新手QT Creator创建的qmake项目更简单直观。在.pro文件中你需要添加QT core gui等模块。VSCode配置如果你偏爱VSCode可以安装C扩展和QT Configure扩展。配置c_cpp_properties.json中的包含路径使其指向你的QT安装目录下的include文件夹。配置tasks.json来调用qmake和make进行构建。调试则需要配置launch.json。5.2 调试与问题排查实录在开发过程中你一定会遇到各种奇怪的问题。以下是我遇到的一些典型问题及解决方法问题1程序运行时崩溃报错“pure virtual method called”现象在操作过程中程序突然崩溃。排查这个错误通常意味着你在基类的构造函数或析构函数中调用了虚函数。在构造/析构期间对象的虚函数表可能还未完全建立或已被销毁此时调用虚函数会导致未定义行为。解决绝对不要在构造函数和析构函数中调用虚函数。如果需要在初始化时进行多态操作可以考虑使用“初始化函数”并在构造完成后调用。问题2界面刷新不正常棋子有残影或绘制错位现象移动棋子后原位置有残留图像或者新位置绘制不正确。排查这通常是paintEvent函数逻辑问题或刷新区域计算错误。解决确保在paintEvent中使用QPainter绘制时是绘制整个棋盘和所有棋子。不要尝试只绘制“脏矩形”除非你非常确定。在棋子移动后确保同时调用update()来触发整个棋盘区域的重绘或者计算新旧位置的两个矩形区域调用update(rect)进行局部更新。检查你的坐标转换函数logicToPixel是否正确特别是行列索引与像素坐标的对应关系。问题3鼠标点击事件不灵敏或响应区域错误现象点击棋子边缘没反应或者点击空白处却选中了棋子。排查鼠标事件判断逻辑有问题。解决在mousePressEvent中你获取的是鼠标点击的像素坐标(x, y)。你需要将其转换为棋盘逻辑坐标(r, c)。然后你需要判断这个(r, c)是否在棋盘范围内0r10, 0c9。最后判断该位置是否有棋子。关键点判断是否有棋子应该使用逻辑坐标去查询ChessBoard模型而不是依赖于你绘制的图像区域。因为绘制可能有偏移但模型数据是精确的。问题4发布程序后在其他电脑上运行缺少DLL现象在自己电脑上运行正常打包发给别人后无法启动提示缺少Qt5Core.dll等。解决这是QT程序发布的常见问题。你需要将程序依赖的QT动态库DLL文件复制到可执行文件同级目录。最稳妥的方法是使用QT自带的命令行工具windeployqtWindows或macdeployqtmacOS。在构建目录下运行windeployqt your_program.exe它会自动扫描依赖并复制所有必要的库和资源文件。对于Linux通常需要告知用户安装对应的QT运行时库。开发这样一个项目最大的收获不是最终那个可以运行的游戏而是在解决上述一个个具体问题的过程中对C面向对象设计、QT框架机制、以及算法逻辑实现的深刻理解。从棋盘数据结构的定义到QT信号槽的巧妙连接再到马蹩腿、炮翻山这些具体规则的代码实现每一步都充满了挑战和乐趣。当你最终看到两个人在你亲手编写的程序中对弈时那种成就感是无与伦比的。这个项目完全可以作为你C/QT学习之路上的一个里程碑它的代码和设计思路也能为你今后开发更复杂的桌面应用打下坚实的基础。