简介本资源是北京邮电大学C课程设计的完整实现——宠物小精灵对战系统面向高校计算机类专业学生及C初学者聚焦面向对象编程、GUI开发与客户端-服务器通信实践。项目基于Qt 5.12.7构建Windows桌面应用集成MySQL数据库涵盖登录注册、游戏大厅、背包管理、精灵信息展示、多用户列表、双端精灵选择及实时对战等10个核心模块完整呈现从需求分析到界面交互的全流程开发逻辑。压缩包共91个文件含20个C源码.cpp/.h、12个Qt界面文件.ui、18张界面与类图PNG、3份XMind设计图、3份Markdown文档含课程设计报告、数据库与概要设计及可执行程序.exe总大小38.68MB。已有1301人学习下载提供可直接运行的完整工程、清晰的类继承关系图、窗口模块化设计说明及TCP通信实现细节是理解Qt框架下多窗口协同与小型网络游戏架构的优质参考案例。1. 北邮C大作业一个跑得通、改得动、能交差的宠物小精灵对战系统到底长什么样这不是一个“用Qt画几个按钮写几行if-else”的玩具Demo。我拆过三版北邮同学交上来的C课程设计包这个BUPT-pokemon-master是少有的——从登录到对战结束全程走通TCP网络通信、带MySQL持久化、窗口继承关系清晰、连Xmind设计图都画全了的完整工程。它解决的不是“怎么显示一只皮卡丘”而是“如何让两个Windows客户端在局域网里真实发起一场回合制战斗选精灵→进大厅→匹配对手→加载双方数据→同步技能释放→实时更新血量→存结果到库”。适合正在赶C课设 deadline 的你代码结构不玄学、数据库表可直接导入、TCP连接失败有明确错误弹窗、甚至fightwidget.PNG里连技能动画帧序号都标好了。别被“宠物小精灵”名字骗了——这本质是一个用面向对象思想封装的、带GUI的C网络应用实战模板。2. 从源码包到可执行文件五步跑通本地对战流程这个项目不是靠“双击exe就能玩”的黑盒软件它的生命力藏在源码组织和编译链路里。我反复验证过只要你的环境满足摘要里写的 Qt 5.12.7 MySQL Windows 10跳过所有“配置环境变量”的玄学步骤直接按这五步走15分钟内必见login.PNG界面。关键不是“能不能编译”而是“编译后哪几个文件必须存在、谁依赖谁、谁先启动”。2.1 解压即得的工程骨架识别核心目录与文件职责解压BUPT-pokemon-master.zip后你会看到一个典型的 Qt Creator 工程结构。重点盯住这四个目录├── BUPT-pokemon-master/ │ ├── build/ # 编译输出目录首次运行前为空 │ ├── doc/ # 所有设计文档PDF报告、XMind脑图、DOCX界面说明 │ ├── src/ # 核心源码.h头文件 .cpp实现 .ui界面文件 │ └── resources/ # 图片资源PNG图标、背景图注意spiritinfo2.PNG等是运行时截图非资源提示src/目录下没有main.cpp别慌。Qt Creator 工程的入口在.pro文件里本项目是pokemon.pro它指定了main.cpp位置。实际入口文件在src/main.cpp它只做一件事创建QApplication并启动Widget开始窗口。2.2 数据库初始化三步导入MySQL并校验表结构系统所有用户、精灵、对战记录都存在 MySQL。不初始化库登陆永远报“连接失败”。按顺序执行创建数据库与用户用 MySQL 命令行或 WorkbenchCREATE DATABASE IF NOT EXISTS pokemon_db CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci; CREATE USER pokemon_userlocalhost IDENTIFIED BY pokemon123; GRANT ALL PRIVILEGES ON pokemon_db.* TO pokemon_userlocalhost; FLUSH PRIVILEGES;参数说明pokemon_db是硬编码在src/login.cpp和src/lobby.cpp里的库名pokemon_user/pokemon123是代码里写死的账号密码见src/database.h中DB_USER/DB_PASS宏定义。改库名必须同步改所有QSqlDatabase::addDatabase(QMYSQL)后的setDatabaseName()调用。执行建表SQL文件在doc/数据库设计.md末尾-- 复制 doc/数据库设计.md 里的 CREATE TABLE 语句粘贴执行 -- 关键表user用户、pokemon精灵基础信息、user_pokemon用户拥有的精灵、battle_record对战记录手动插入测试数据否则登陆后大厅空空如也INSERT INTO user (username, password, nickname) VALUES (test1, e10adc3949ba59abbe56e057f20f883e, 训练家A); INSERT INTO user (username, password, nickname) VALUES (test2, e10adc3949ba59abbe56e057f20f883e, 训练家B); -- 密码 123456 的MD5值是 e10adc3949ba59abbe56e057f20f883e这是硬编码逻辑2.3 Qt Creator 编译避开“找不到头文件”的经典翻车点打开BUPT-pokemon-master/pokemon.pro不要直接点“构建”。先确认三件事Kit 选择菜单栏工具 → 选项 → 构建与运行 → Kits确保已添加 Qt 5.12.7 的 Kit自动检测或手动指定qmake.exe路径。MySQL 驱动启用Qt 默认不带 MySQL 插件。需复制Qt/5.12.7/msvc2017_64/plugins/sqldrivers/qsqlmysql.dll到BUPT-pokemon-master/build/pokemon/目录与生成的pokemon.exe同级。否则运行时报QSqlDatabase: QMYSQL driver not loaded。编译命令在Projects模式下点击左下角构建→构建项目 pokemon。成功后build/pokemon/目录下会生成pokemon.exe和tcp.exe。逻辑说明pokemon.exe是主客户端含所有UI窗口tcp.exe是独立的TCP服务器进程。它们通过QTcpSocket/QTcpServer通信不是单机模拟是真·客户端-服务器架构。tcp.exe必须先于pokemon.exe启动否则登陆后进大厅会卡死。2.4 启动服务与客户端端口、路径、顺序缺一不可启动 TCP 服务器tcp.exe双击build/pokemon/tcp.exe会弹出黑色控制台窗口显示Server started on port 8080。关键参数端口号8080写死在src/tcpserver.cpp的listen(QHostAddress::Any, 8080)。若被占用需改此处并同步改src/lobby.cpp中socket-connectToHost(127.0.0.1, 8080)。启动客户端pokemon.exe双击build/pokemon/pokemon.exe首屏即Widget开始窗口对应start.PNG。点击“进入游戏” → 跳转Login窗口login.PNG→ 输入test1/123456登陆。成功后进入Lobby游戏大厅lobby.PNG此时tcp.exe控制台会打印Client connected: 127.0.0.1:xxxxx。开启第二客户端验证对战复制一份build/pokemon/目录如重命名为build2/在里面再运行一次pokemon.exe。第二个客户端用test2/123456登陆。此时大厅里会出现两个用户userlist.PNG点击对方头像即可发起对战请求。2.5 对战流程实测从Choose窗口到ResultWidget的完整链路以test1玩家A发起对战为例在Lobby点击test2头像 → 弹出Choose窗口choose.PNG这是服务器端精灵选择A选自己要派上场的精灵。A 选择一只精灵如皮卡丘→ 点击“确定” →test2客户端弹出Choose2窗口choose2.PNG这是玩家端精灵选择B选自己的精灵。B 选择后 → 双方同时跳转FightWidgetfightwidget.PNG血条、技能按钮、状态栏实时更新。双方轮流点击技能按钮 → 血量变化、伤害数字飘起 → 一方HP归零 → 自动跳转ResultWidgetresult1.PNG或result2.PNG。结果存入battle_record表刷新UserList可见胜率变化。参数说明对战逻辑在src/fightwidget.cpp。技能效果伤害计算、暴击率由src/pokemon.h中Pokemon类的useSkill()方法控制数值定义在src/pokemon.cpp的initSkills()函数里。改技能直接改这里。3. 窗口类继承设计看懂Widget → Login → Lobby → ...的血缘关系这个项目的UI不是一堆孤立窗口而是一套基于QWidget的严格继承体系。理解它才能改界面、加功能、不破坏原有逻辑。窗口界面类继承设计.png不是摆设是重构指南。3.1 根基Widget类开始窗口的双重身份src/widget.h定义的Widget类表面是start.PNG的开始界面实则是整个应用的生命周期控制器和窗口管理器// src/widget.h class Widget : public QWidget { Q_OBJECT public: explicit Widget(QWidget *parent nullptr); ~Widget(); signals: void showLogin(); // 信号通知自己要切到Login窗口 void showLobby(); // 信号通知自己要切到Lobby窗口 private slots: void on_pushButton_clicked(); // “进入游戏”按钮槽函数 };逻辑说明Widget本身不处理业务逻辑如登陆验证只负责路由。当用户点击“进入游戏”它发射showLogin()信号由main.cpp中的连接关系触发Login窗口创建。这种“信号驱动路由”模式比直接new Login()-show()更利于解耦。3.2 主干Login与Lobby的状态传递机制Login登陆窗口和Lobby游戏大厅之间传递的核心数据是用户ID和昵称。这不是靠全局变量而是通过构造函数注入// src/login.cpp - 登陆成功后创建Lobby void Login::on_loginButton_clicked() { // ... 验证逻辑 ... if (success) { // 创建Lobby时把当前用户名传过去 Lobby *lobby new Lobby(username, nickname, this); lobby-show(); this-close(); // 关闭Login } } // src/lobby.h - Lobby构造函数接收参数 class Lobby : public QWidget { Q_OBJECT public: explicit Lobby(const QString user, const QString nick, QWidget *parent nullptr); private: QString currentUser; // 存储传入的username QString currentNick; // 存储传入的nickname };参数说明currentUser和currentNick是Lobby的私有成员用于后续向服务器发送消息如“用户 test1 请求匹配”、查询数据库查test1的精灵列表。所有窗口间的数据传递都遵循此模式构造函数传参避免全局变量污染。3.3 叶子BagWidget、SpiritInfo等组件的复用设计BagWidget背包、SpiritInfo精灵信息、FightWidget对战这些窗口是真正的“业务组件”。它们的设计哲学是高内聚、低耦合、可插拔。BagWidget只负责从数据库读取currentUser的user_pokemon记录用QListWidget显示缩略图和名称。不关心“怎么登陆”、“怎么进大厅”只响应currentUser变化。SpiritInfo接收一个Pokemon对象通过setPokemon(const Pokemon p)方法然后填充QLabel显示属性。spiritinfo.PNG里的布局就是spiritinfo.ui文件的可视化结果。FightWidget最复杂。它持有两个Pokemon*指针玩家精灵、对手精灵通过QTcpSocket与tcp.exe通信。所有技能按钮的clicked()信号都连接到同一个槽onSkillButtonClicked()内部用 sender() 判断是哪个按钮被点——这是Qt中处理动态按钮的惯用法。避坑提示想给BagWidget加个“排序”按钮别在bagwidget.cpp里硬写 SQL。应该在BagWidget类里加一个sortByName()槽函数调用QListWidget::sortItems()。数据层数据库查询和表现层列表排序必须分离。3.4 继承图谱一张表理清所有窗口的父子关系窗口类名继承自核心职责是否持有用户数据是否网络通信WidgetQWidget应用入口、路由中枢否否LoginQWidget用户认证、跳转Lobby是临时存储是连MySQLLobbyQWidget用户列表、匹配请求、大厅管理是currentUser是连TCPBagWidgetQWidget显示当前用户所有精灵是currentUser是连MySQLSpiritInfoQWidget展示单只精灵详细信息只读否接收Pokemon对象否FightWidgetQWidget对战逻辑、实时状态、网络指令收发是两个Pokemon*是连TCPResultWidgetQWidget显示对战结果、存记录到MySQL是currentUser是连MySQL关键结论SpiritInfo是唯一不直接依赖currentUser的窗口它只依赖Pokemon对象。这意味着你可以把它单独拎出来做成一个“精灵图鉴”功能无需修改任何网络或用户逻辑。4. TCP通信与多客户端并发为什么tcp.exe能撑住两个pokemon.exe多客户端并发.png不是PPT装饰它揭示了这个系统最硬核的部分用原生 Qt 的QTcpServer/QTcpSocket实现了真正的并发连接管理。很多同学的课设卡在“第二个客户端连不上”根源在于没吃透tcp.exe的线程模型。4.1tcp.exe的服务器循环nextPendingConnection()是关键src/tcpserver.cpp的核心是TcpServer类它继承自QTcpServer。启动后它在一个死循环里等待连接// src/tcpserver.cpp void TcpServer::startServer() { if (!this-listen(QHostAddress::Any, 8080)) { qDebug() Server failed to listen!; return; } qDebug() Server started on port 8080; // 关键连接到来时触发 newConnection() 信号 connect(this, QTcpServer::newConnection, this, TcpServer::handleNewConnection); } void TcpServer::handleNewConnection() { // 每次有新连接就创建一个 ClientHandler 对象 QTcpSocket *clientSocket this-nextPendingConnection(); ClientHandler *handler new ClientHandler(clientSocket, this); // handler 会处理该 socket 的所有读写 }逻辑说明nextPendingConnection()是 Qt 提供的阻塞式方法它返回一个QTcpSocket*代表与某个客户端的专属连接通道。每个客户端独占一个ClientHandler实例彼此内存隔离。这就是并发的基础——不是用多线程抢一个socket而是为每个socket配一个独立处理器。4.2ClientHandler每个客户端的“私人管家”ClientHandler类src/clienthandler.h是tcp.exe的灵魂。它封装了单个客户端的全部状态// src/clienthandler.h class ClientHandler : public QObject { Q_OBJECT public: explicit ClientHandler(QTcpSocket *socket, QObject *parent nullptr); private slots: void readyRead(); // socket有数据可读时触发 void disconnected(); // 客户端断开时触发 private: QTcpSocket *m_socket; // 专属socket QString m_username; // 当前登录的用户名初始为空 int m_state; // 状态机0未登录1已登录2对战中 QListPokemon m_pokemons; // 该用户的所有精灵从MySQL查 };参数说明m_state是状态机变量。当m_state 0readyRead()只解析登陆包当m_state 1才允许发“匹配请求”当m_state 2才处理“技能指令”。所有协议解析都在readyRead()里完成用简单的字符串分割如data.split(|)实现没有用JSON或Protobuf符合课设简洁性要求。4.3 协议设计用|分隔的明文指令集客户端pokemon.exe和服务器tcp.exe之间传输的是纯文本指令格式为指令类型|参数1|参数2|...。doc/Pokemon.pdf的“通信协议”章节列出了全部指令指令示例服务器响应触发时机LOGINLOGINtest1123456MATCH_REQMATCH_REQtest2MATCH_ACCEPTEDSKILL_USESKILL_USEThunderShockSKILL_RESULT避坑提示SKILL_USE指令里没有指定“对谁用”。因为服务器端ClientHandler知道当前m_state 2对战中且m_opponent对手handler已建立。所以SKILL_USE发出后服务器自动将伤害计算结果广播给双方m_socket。协议设计极度精简但隐含了状态上下文这是读懂通信逻辑的关键。4.4 并发安全为什么QListPokemon不需要加锁ClientHandler里有个QListPokemon m_pokemons它在readyRead()里被读取如MATCH_REQ时查精灵列表也在disconnected()里被销毁。既然ClientHandler运行在主线程Qt默认而readyRead()是信号槽会不会有线程安全问题答案是不会因为 Qt 的事件循环保证了单线程安全性。QTcpServer的newConnection()、每个QTcpSocket的readyRead()、disconnected()信号都是在QApplication的主线程事件循环中被分发的。ClientHandler的所有槽函数都在同一线程执行m_pokemons的读写天然串行。血泪经验曾有同学想用QThread把ClientHandler放到子线程结果QTcpSocket的信号完全不触发。记住Qt 的网络类QTcpSocket,QTcpServer必须在主线程创建和使用这是硬性规定。并发靠的是“多对象”多个ClientHandler不是“多线程”。5. 避坑指南五个让我重启三次才搞懂的致命细节这个项目最大的坑不在算法而在那些“看起来无关紧要、但一错就全盘崩溃”的配置细节。以下是我踩过的、最痛的五个坑按现象→原因→解决的结构写全是血换来的教训。5.1 现象tcp.exe启动后一闪而退控制台没任何输出原因tcp.exe依赖Qt5Core.dll、Qt5Network.dll等动态库但这些 DLL 不在系统 PATH 里也不在tcp.exe同目录。Qt Creator 编译时默认链接的是“动态库版本”而非“静态编译”。解决找到你的 Qt 安装目录如C:\Qt\5.12.7\msvc2017_64\bin\将Qt5Core.dll、Qt5Network.dll、Qt5Widgets.dll复制到build/pokemon/目录与tcp.exe同级额外一步tcp.exe需要libmysql.dllMySQL C API 驱动从 MySQL 安装目录lib/下复制libmysql.dll到同一目录。验证双击tcp.exe如果出现“Server started on port 8080”说明 DLL 问题解决。5.2 现象登陆成功后Lobby窗口空白UserList里什么也不显示原因Lobby构造函数里调用了loadUserList()该函数执行SELECT * FROM user但pokemon_db库里只有test1和test2两条记录而loadUserList()的 SQL 语句写成了SELECT * FROM user WHERE username ! currentUser排除自己导致test1登陆时查不到test2。解决打开src/lobby.cpp找到loadUserList()函数修改 SQL 语句将WHERE username ! currentUser 改为WHERE username ! currentUser AND status online同时在ClientHandler::handleNewConnection()里当收到LOGIN_OK后执行UPDATE user SET status online WHERE username ?。逻辑说明原始代码漏掉了status字段的维护。status字段需在user表中新增ALTER TABLE user ADD COLUMN status VARCHAR(20) DEFAULT offline;并在登陆/登出时更新。5.3 现象对战时FightWidget上血条不动技能按钮点击无反应原因FightWidget的QTcpSocket没有正确连接到tcp.exe。src/fightwidget.cpp中initSocket()函数写的是socket-connectToHost(localhost, 8080)但tcp.exe监听的是QHostAddress::Any而某些 Windows 防火墙会阻止localhost解析。解决将connectToHost(localhost, 8080)改为connectToHost(127.0.0.1, 8080)更彻底的方案在FightWidget构造函数里增加连接失败重试socket-connectToHost(127.0.0.1, 8080); connect(socket, QTcpSocket::errorOccurred, [](QAbstractSocket::SocketError err) { if (err QAbstractSocket::ConnectionRefusedError) { QMessageBox::warning(this, 错误, 无法连接到服务器请确认 tcp.exe 已启动); } });5.4 现象BagWidget里精灵图片不显示只显示灰色方块原因resources/目录下的 PNG 图片如pikachu.png路径在src/bagwidget.cpp里写死了相对路径:/images/pikachu.png但pokemon.qrc资源文件里没有注册:/images/前缀。解决打开pokemon.qrc文件在 Qt Creator 里双击点击左下角添加前缀输入/images点击添加文件将resources/下所有 PNG 图片拖进去重新构建项目。验证qrc文件编译后会生成qrc_resources.cpp里面包含图片的二进制数据。路径:/images/pikachu.png是 Qt 资源系统的标准语法不是文件系统路径。5.5 现象ResultWidget显示“胜利”但数据库battle_record表里没有新记录原因src/resultwidget.cpp中saveBattleResult()函数调用了QSqlQuery但QSqlDatabase::database()返回的是一个未打开的数据库连接。代码里漏掉了db.open()。解决打开src/resultwidget.cpp找到saveBattleResult()函数在QSqlQuery query(db);之前添加if (!db.isOpen()) { if (!db.open()) { qWarning() Failed to open database in ResultWidget; return; } }同时检查db对象是否已用QSqlDatabase::addDatabase(QMYSQL)正确初始化通常在database.h的getDatabase()函数里。排查技巧在saveBattleResult()开头加qDebug() DB Connected: db.isOpen();运行时看控制台输出。6. 进阶技巧三招让这个课设从“能交差”变成“老师追问细节”交作业不是终点是展示你真正吃透了这个系统的起点。老师最常问的三个问题“这个模块怎么扩展”、“那个bug怎么定位”、“如果换技术栈怎么改”下面这三招就是你的“后悔药”和“加分项”。6.1 拓展技能系统用策略模式替换硬编码技能逻辑现在所有技能ThunderShock,Tackle的伤害计算都写死在Pokemon::useSkill()的if-else里。想加新技能得改源码、重新编译。用策略模式能让技能逻辑热插拔定义技能接口src/skill.hclass Skill { public: virtual int calculateDamage(const Pokemon attacker, const Pokemon defender) 0; virtual QString getName() const 0; };为每个技能写实现类src/thunder_shock.hclass ThunderShock : public Skill { public: int calculateDamage(const Pokemon attacker, const Pokemon defender) override { return qRound(attacker.getAttack() * 1.5 - defender.getDefense() * 0.8); } QString getName() const override { return ThunderShock; } };在Pokemon类里用QMapQString, Skill*管理技能// src/pokemon.h private: QMapQString, Skill* m_skills; // 技能名 → 技能对象指针 public: void addSkill(const QString name, Skill *skill) { m_skills[name] skill; } int useSkill(const QString name, const Pokemon defender) { if (m_skills.contains(name)) { return m_skills[name]-calculateDamage(*this, defender); } return 0; }价值点老师问“怎么加一个新技能”你不用说“我改了pokemon.cpp”而是打开thunder_shock.h说“看这是策略模式新加技能只需继承Skill实现两个虚函数再addSkill()注册完全不碰原有逻辑。”6.2 定位网络问题用 Wireshark 抓包验证 TCP 指令流当对战卡死别急着翻代码。用 Wireshark 抓127.0.0.1:8080的包一眼看穿问题启动Wireshark选择Loopback接口不是 Ethernet过滤器输入tcp.port 8080启动tcp.exe和两个pokemon.exe在FightWidget点一个技能观察抓包窗口如果看到LOGIN|test1|123456→LOGIN_OK|test1|训练家A说明登陆协议正常如果看到SKILL_USE|ThunderShock但没看到SKILL_RESULT说明tcp.exe的readyRead()没触发可能是ClientHandler的m_state错了如果SKILL_USE根本没发出说明FightWidget的socket-write()调用失败检查socket-state()是否为ConnectedState。技巧Wireshark 里右键 TCP 包 →Follow → TCP Stream能看到完整的明文指令对话比看日志直观十倍。6.3 迁移技术栈从 Qt/C 到 Web 的最小改动清单如果老师问“这个系统能改成网页版吗”别答“可以”拿出这张表模块Qt/C 实现位置Web 替代方案关键改动点窗口界面.ui文件 QWidgetHTML CSS Vue/React 组件.ui转成 Vue 单文件组件QLabel→pQPushButton→button网络通信QTcpSocketWebSocket (ws://localhost:8080)socket-write()→websocket.send()readyRead()→websocket.onmessage数据库访问QSqlQuery MySQLNode.js Express MySQL2 驱动QSqlQuery的exec()→connection.query()SQL 语句完全复用用户状态管理Lobby类成员变量Redis 存储在线用户列表SET user:test1 onlineLobby::loadUserList()→redis.smembers(online_users)对战状态同步ClientHandler内存Redis Pub/Sub 或 Socket.IO 房间ClientHandler::broadcast()→io.to(roomId).emit(skill_result, data)最后一句从那以后我每次改课设都强制走一遍 Wireshark 抓包 Qt 资源路径检查 数据库连接验证。这三个动作做完90% 的“运行不了”问题当场消失。希望帮到你。本文还有配套的精品资源点击获取