Qt TCP+MySQL宠物对战系统实战架构与避坑指南
发布时间:2026/10/19 10:04:02 锦皓数字建站

简介本资源是北京邮电大学C课程设计的完整实现——宠物小精灵对战系统面向高校计算机类专业学生及C初学者聚焦面向对象编程、GUI开发与客户端-服务器通信实践。项目基于Qt 5.12.7构建Windows桌面应用集成MySQL数据库涵盖登录注册、游戏大厅、背包管理、精灵信息展示、多用户列表、双端精灵选择及实时对战等10个核心模块完整呈现从需求分析到界面交互的全流程开发逻辑。压缩包共91个文件38.68MB含20个C源码与头文件实现各窗口逻辑、12个.ui界面文件Qt Designer设计、18张PNG界面截图与类图如小精灵类.png、窗口继承设计.png、3份XMind思维导图概要设计/阶段/界面设计及PDF课程设计报告、MD文档说明等结构清晰、注释充分。目前已有1301人学习下载适合用于课程设计参考、QtC综合实训或网络编程入门拓展。1. 为什么一个“宠物小精灵对战系统”要硬上 TCP Qt MySQL 三件套这不是又一个用控制台模拟回合制的 C 小练习——某高校课程设计明确要求必须支持多客户端并发对战、用户状态持久化、界面可交互、战斗过程可视化。我带过三届类似课设最常翻车的不是算法写错而是学生卡在「怎么让两个本地 exe 真正打起来」用文件轮询延迟高、易冲突用共享内存Windows 下权限和同步太玄学用 UDP丢包一多血条跳变、技能失效答辩时演示直接崩。所以这套方案从第一天就锚定 TCP它不快但稳不炫技但能让你在答辩现场当着导师面拉起两个pokemon.exe登录不同账号进大厅、选精灵、点对战、看动画、出结果——全程不闪退、不假死、不连不上。适合所有被「网络通信」「GUI 响应阻塞」「数据一致性」反复暴击的大三同学也适合需要快速交付一套可演示、可截图、可写进报告「系统架构图」的工程实践者。2. 总体架构设计三层结构与四个核心通信通道的职责划分整个系统不是把 C 当胶水粘一堆控件而是按「数据层—逻辑层—表现层」切得非常清楚。Qt 负责画界面、发信号、收事件MySQL 存用户、精灵、背包、战绩TCP 则只干一件事在客户端和服务器之间精准传递五类结构化指令。这种分法让代码改起来有边界——比如换数据库只动DBManager类换 UI 风格只改.ui文件和on_XXX_clicked()槽函数加新技能效果只改BattleEngine的applySkill()方法。没有一处是“改一行崩十处”的黑匣子。2.1 服务端核心单线程 IO 复用 心跳保活机制服务端用QTcpServer监听每个连接由QTcpSocket管理。关键不是“多线程”而是用QEventLoopQTimer实现非阻塞心跳。很多同学一上来就moveToThread结果信号跨线程乱飞、对象生命周期失控。我们用的是更稳妥的方案// server.cpp void GameServer::startHeartbeat() { QTimer *timer new QTimer(this); connect(timer, QTimer::timeout, this, [this]() { for (auto socket : m_clients) { if (socket-state() QAbstractSocket::ConnectedState) { // 发送轻量心跳包4字节长度 PING QByteArray heartbeat; heartbeat.append((char)0x00); // length low heartbeat.append((char)0x04); // length high → 4 bytes heartbeat.append(PING); socket-write(heartbeat); } } }); timer-start(30000); // 30秒一次 }提示心跳不是为了“探测是否在线”而是防止 NAT 超时断连。Windows 防火墙或家用路由器通常 60 秒无流量就回收 TCP 连接30 秒发一次PING服务端收到后回PONG双方都刷新连接状态。这个细节90% 的课设报告里都漏写但答辩时被问“怎么保证长连接不掉”就是加分项。2.2 客户端通信封装PacketBuilder 与 ProtocolHandler 的解耦设计客户端不直接socket-write()原始字节而是通过PacketBuilder构建结构化包再由ProtocolHandler统一发送/解析。协议头固定 8 字节[type:2][length:2][seq:4]后续是 JSON 序列化 payload。例如登录请求// client/packetbuilder.cpp QByteArray PacketBuilder::buildLoginPacket(const QString username, const QString password) { QJsonObject obj; obj[username] username; obj[password] password; obj[timestamp] QDateTime::currentMSecsSinceEpoch(); QJsonDocument doc(obj); QByteArray payload doc.toJson(QJsonDocument::Compact); QByteArray packet; packet.append((char)(PACKET_TYPE_LOGIN 8)); // type high packet.append((char)(PACKET_TYPE_LOGIN 0xFF)); // type low packet.append((char)(payload.size() 8)); // length high packet.append((char)(payload.size() 0xFF)); // length low packet.append(QByteArray((char*)m_seq, 4)); // seq, network byte order packet.append(payload); m_seq; // 全局递增用于服务端去重 return packet; }为什么这么设计type字段让ProtocolHandler::handlePacket()能直接switch(type)分发到handleLogin()/handleBattleAction()等方法避免字符串解析开销seq是客户端自增序列号服务端缓存最近 100 条seq重复收到即丢弃解决 TCP 重传导致的指令重复执行比如连点“攻击”按钮只生效一次length为 2 字节最大支持 64KB payload够传精灵属性、技能列表、战斗日志等全部数据又不会因过大导致socket-readAll()内存暴涨。2.3 数据库设计三张核心表与外键约束的实际价值MySQL 表不是为“看起来规范”而存在而是为解决具体问题。本系统只用三张主表但每张都直击痛点表名关键字段解决什么问题usersid,username,password_hash,created_at用户注册/登录凭证password_hash用SHA256(usernamesaltpassword)避免明文存密码被导师抽查扣分pokemonid,user_id,name,hp,max_hp,attack,defense,level,exp,is_active一个用户可拥有多只精灵is_active1标识当前出战精灵切换时只需UPDATE ... SET is_active0 WHERE user_id?不用删重建battle_logsid,attacker_id,defender_id,winner_id,log_json,created_atlog_json存完整战斗过程JSON 数组方便后期导出分析也避免为每回合建一张表注意pokemon.user_id是外键关联users.id且设ON DELETE CASCADE。这意味着——用户注销时DELETE FROM users WHERE id?其所有精灵自动清除。不用手写循环删也不怕漏删导致“幽灵精灵”出现在别人背包里。3. 界面模块实现从 Widget 继承链到信号槽的精准绑定Qt 界面不是拖控件完事而是靠清晰的继承关系和信号流来保证可维护性。整个 UI 层共 10 个窗口类全部继承自QWidget但按职责分三级基础容器 → 业务窗口 → 功能弹窗。这种结构让“改一个按钮位置不影响战斗逻辑”成为可能。3.1 窗口继承体系BaseWidget → AuthWidget → GameWidget 的三层抽象BaseWidget所有窗口的基类统一处理showEvent()中的焦点重置、closeEvent()中的连接关闭、keyPressEvent()中的 ESC 退出逻辑AuthWidget继承BaseWidget专管登录/注册含Login和Register两个子 widget通过QStackedWidget切换避免新建窗口导致资源泄漏GameWidget继承BaseWidget是游戏大厅及后续所有界面的父容器内部用QTabWidget管理BagWidget、UserList、Lobby等标签页点击标签不销毁旧界面只hide()/show()提升切换速度。// gamewidget.cpp void GameWidget::on_tabChanged(int index) { QWidget *current ui-tabWidget-currentWidget(); if (current ui-bagTab) { // 切换到背包时强制刷新数据不依赖定时器 emit requestRefreshBagData(); // 发信号给主逻辑层 } else if (current ui-lobbyTab) { emit requestUpdateLobbyUsers(); // 同理 } }设计说明这里没用QTimer::singleShot(0, ...)延迟刷新因为课设环境不稳定延时可能被调度器吞掉。而是用信号通知BattleController主类去查数据库、更新模型、再发dataUpdated()信号给视图——信号链越短越不容易断。3.2 对战界面 FightWidgetQGraphicsView 自定义精灵动画的轻量实现FightWidget不用 QML 或第三方动画库纯用QGraphicsViewQGraphicsPixmapItemQTimeLine。原因很实际课设不允许额外依赖且QTimeLine的帧精度足够模拟“攻击—闪避—受伤”三阶段。// fightwidget.cpp void FightWidget::startAttackAnimation(PokemonItem *attacker, PokemonItem *defender) { QTimeLine *timeline new QTimeLine(300, this); // 300ms 动画 timeline-setFrameRange(0, 100); QGraphicsItemAnimation *anim new QGraphicsItemAnimation; anim-setItem(attacker); anim-setTimeLine(timeline); // 攻击者前冲x 从 0 → -80 anim-setPosAt(0.0, attacker-pos()); anim-setPosAt(1.0, QPointF(attacker-x() - 80, attacker-y())); connect(timeline, QTimeLine::finished, []() { // 动画结束触发伤害计算 calculateDamage(attacker, defender); // 然后启动受击抖动动画略 }); timeline-start(); }参数说明300ms是实测值——小于 200ms 看不清动作大于 500ms 显得拖沓-80是像素位移适配 800×600 主窗口比例QGraphicsItemAnimation比QPropertyAnimation更轻量不依赖Q_OBJECT宏避免在 item 类里加大量 boilerplate。3.3 背包界面 BagWidgetQListView 自定义 Delegate 的性能取舍BagWidget显示用户所有精灵用QListView而非QTableWidget因为后者每行都是独立 widget10 只精灵就创建 10 个QHBoxLayout内存占用翻倍。我们用QStandardItemModelQStyledItemDelegate// bagdelegate.cpp void BagDelegate::paint(QPainter *painter, const QStyleOptionViewItem option, const QModelIndex index) const { if (!index.isValid()) return; painter-save(); QStyleOptionViewItem opt option; initStyleOption(opt, index); // 绘制背景色选中态/普通态 if (opt.state QStyle::State_Selected) { painter-fillRect(opt.rect, QColor(173, 216, 230, 180)); // 浅蓝 } else { painter-fillRect(opt.rect, opt.palette.base().color()); } // 绘制精灵图标从 resources.qrc 加载 QPixmap icon QPixmap(QString(:/icons/%1.png).arg(index.data(Qt::UserRole1).toString())); painter-drawPixmap(opt.rect.left() 10, opt.rect.top() 5, icon.scaled(40,40)); // 绘制文字名称 等级 HP QString text QString(%1 Lv.%2 [%3/%4]) .arg(index.data(Qt::DisplayRole).toString()) .arg(index.data(Qt::UserRole2).toInt()) .arg(index.data(Qt::UserRole3).toInt()) .arg(index.data(Qt::UserRole4).toInt()); painter-drawText(opt.rect.left() 60, opt.rect.top() 20, text); painter-restore(); }为什么不用QTableViewQTableView默认支持排序、编辑、多列头但本系统背包只要“看”和“点选”多出来的功能全是负担QListView的 delegate 可以完全控制每一像素比如在 HP 不足 30% 时文字自动变红图标加血条 overlay——这些在 table view 里要重写paint()十几处而 list view 只需改一个paint()函数。4. 关键模块避坑指南五个必踩、但文档从不写的实战雷区做这个系统时我和 A 同学前后重装了 7 次 Qt 环境不是因为代码错而是被以下问题反复暴击。这些不是“理论错误”是 Windows Qt Creator MySQL 组合下100% 会遇到的物理级障碍。4.1 现象mysql.h报错 “cannot open source file”但 include 路径明明加了原因Qt Creator 的 Kit 配置里qmake版本和MySQL的libmysql.dll编译版本不匹配。Qt 5.12.7 官方预编译版用 MSVC2017 编译而你下载的 MySQL Connector/C 是 VS2019 编译的libmysql.lib链接时符号不识别。解决下载 MySQL 官网的MySQL Connector/C 6.1.11最后支持 VS2017 的版本在.pro文件中用绝对路径指定LIBS C:/mysql-connector-c-6.1.11-win32/lib/libmysql.lib将libmysql.dll复制到pokemon.exe同目录而非system32——Windows 加载 DLL 优先查当前目录。4.2 现象QTcpSocket连接服务器后readyRead()死活不触发原因服务端write()后没调用socket-flush()数据卡在 Qt 的缓冲区TCP 层没真正发出去。尤其在 Windows 下QIODevice::WriteChannel默认启用缓冲write()只是把数据塞进 buffer。解决所有socket-write()后立刻加socket-flush()更稳妥的做法在QTcpSocket构造后调用socket-setSocketOption(QAbstractSocket::LowDelayOption, 1)禁用 Nagle 算法强制小包立即发出。4.3 现象QGraphicsView动画卡顿CPU 占用飙到 30%原因QTimeLine默认updateInterval是 0意味着“尽可能快刷新”但 Qt 的事件循环跟不上导致paint()被疯狂重入UI 线程锁死。解决显式设置timeline-setUpdateInterval(16);≈60FPS在paint()开头加if (!option.state QStyle::State_Enabled) return;跳过禁用态绘制。4.4 现象QSqlDatabase多次addDatabase()导致“database name already in use”原因Qt 的QSqlDatabase是静态对象池addDatabase(QMYSQL, myconn)的第二个参数是 connection name如果多次调用同一名字第二次就会失败。很多教程直接写addDatabase(QMYSQL)省略名字结果全局只有一个 unnamed connection多线程访问直接崩溃。解决每个需要 DB 的类如UserManager,PokemonManager创建自己命名的 connectionQSqlDatabase db QSqlDatabase::addDatabase(QMYSQL, db_ QUuid::createUuid().toString());使用完后调用QSqlDatabase::removeDatabase(db_xxx)否则下次addDatabase会报错。4.5 现象QTimer在moveToThread()后timeout()信号不触发原因QTimer必须在它所属的线程中启动且该线程必须运行QEventLoop。把QTimer创建在主线程moveToThread()到子线程却不调用thread-exec()timer 就永远睡着。解决根本原则不要把 QTimer 移到子线程。心跳、刷新等定时任务全放主线程用QTimer::singleShot(0, ...)做异步调度如果真要后台定时如日志轮转用std::threadstd::this_thread::sleep_for()绕过 Qt 事件循环限制。5. 从开发到答辩一份能过审、能演示、能讲清技术点的交付 checklist答辩不是考试是向导师证明“这系统是我亲手搭起来的每个模块我都懂它为什么这样设计”。所以最后一关不是写完代码而是准备好三样东西可稳定演示的二进制包、一页纸讲清架构的汇报图、以及被问住时的兜底话术。下面是我每次带学生过答辩前强制他们做完的六件事。5.1 演示包必须包含的四个文件及其作用文件名是否必须作用导师最常检查点tcp.exe✅服务端可执行文件双击即启动监听127.0.0.1:8080检查进程管理器里是否有tcp.exe端口是否被占用pokemon.exe✅客户端无需安装解压即用让学生当场双开登录两个账号进大厅截图config.ini✅存数据库连接信息host/user/pass/db避免硬编码查看文件内容确认没写root/123456这种弱口令demo.mp4⚠️强烈建议60 秒录屏登录→选精灵→发起对战→打出暴击→显示胜利导师没时间等你现场操作视频是第一信任背书提示demo.mp4不用高清用 OBS 录 720p 即可重点是流程完整、字幕标出关键步骤如“此时发送 BattleStart 包”。我见过太多学生现场演示时因杀毒软件拦截pokemon.exe导致白屏一段视频直接救场。5.2 架构图不能只画框要标出三个“数据流向箭头”很多同学交的架构图是这样的[Client] → [Server] → [MySQL]这等于没画。合格的架构图必须标出真实数据载体。我在课程设计报告.md里要求学生手绘或用 draw.io以下三处Client → Server 箭头旁标注TCP Packet: {type:0x01, len:128, seq:1001, payload:{...}}Server → MySQL 箭头旁标注SQL: UPDATE pokemon SET hphp-25 WHERE id88 AND user_id123Server → Client广播箭头旁标注JSON Array: [{action:damage,target:pikachu,value:25},{action:animate,id:spark}]为什么有效导师扫一眼就知道你不是抄的——只有真写过PacketBuilder和BattleEngine的人才敢把seq:1001这种细节写进图里。而且这三个标注正好对应答辩时最常问的三个问题“协议怎么设计的”、“数据库怎么更新的”、“动画怎么同步的”。5.3 答辩话术被问“为什么不用 WebSocket”时的标准回答这个问题几乎必问。别慌按这个结构答既显思考又不贬低自己技术“WebSocket 确实更适合实时对战但它需要 HTTP 握手、消息分帧、心跳保活全套机制而我们的课设周期只有 3 周。TCP 自定义协议虽然要自己写封包/解包但好处是可控性强比如战斗指令必须严格顺序执行我们就用seq字段做服务端去重比如想限制每秒最多发 2 条指令防刷就在服务端加个QMapquint64, QTime记录每个客户端上次发包时间。这些在 WebSocket 里反而要绕路实现。所以选型不是‘高级好’而是‘能闭环交付好’。”这句话里埋了三个得分点提到具体技术细节seq、QMap、给出量化约束3 周、2 条/秒、落脚到工程目标闭环交付。比说“老师我没学过 WebSocket”高明十倍。5.4 报告里的“数据库设计”章节必须出现的两行 SQL很多报告写“采用 MySQL 存储用户信息”然后贴一张 ER 图。这不够。必须在文档里写出两条真实执行过的 SQL并解释为什么这么写-- 1. 创建 users 表时password_hash 字段用 VARCHAR(64)因为 SHA256 输出是 64 字符十六进制 CREATE TABLE users ( id INT PRIMARY KEY AUTO_INCREMENT, username VARCHAR(32) UNIQUE NOT NULL, password_hash VARCHAR(64) NOT NULL, created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP ); -- 2. 查询用户所有精灵时用 LEFT JOIN 而非 INNER JOIN确保用户没精灵时仍返回空列表避免前端空指针 SELECT p.id, p.name, p.hp, p.max_hp, p.level FROM users u LEFT JOIN pokemon p ON u.id p.user_id WHERE u.username alice;血泪经验导师翻报告就盯这种“带解释的 SQL”。你写了他就知道你真连过数据库、跑过查询、修过 null 值 bug。从那以后我每次带课设都强制学生在README.md顶部加一行✅ 已验证双客户端对战 10 次0 次断连3 次暴击2 次闪避1 次平局不是为了凑数而是把“测试”这件事变成可读、可验、可答辩的实体。希望帮到你。本文还有配套的精品资源点击获取
锦
锦皓数字建站
深耕本土企业品牌数字化升级,专注原创端正雅致商务官网,从视觉设计到稳定运维全程保驾护航。