简介:一款基于QT+C++开发的宠物小精灵人机对战游戏源码,专为毕业设计、课程设计与项目开发打造,适合有一定C++基础、希望提升Qt界面开发与面向对象设计的读者。项目以经典宠物小精灵为主题,实现了玩家与电脑的回合制对战,涵盖力量型小火龙、肉盾型妙蛙种子、防御型杰尼龟、敏捷型皮卡丘四种精灵,通过基类虚函数与子类重写体现继承与多态特性。资源包为zip格式,共36个文件,包含11个cpp源文件、11个h头文件、ui界面设计文件、qrc资源文件及png图片素材,整体大小仅1.88MB,结构清晰便于研读。目前已有281人浏览学习,源码经过严格测试,可直接参考并在此基础上扩展,尤其适合学习Qt绘图、事件处理、状态管理及游戏逻辑的同学深入拆解。
1. 为什么拿 QT+C++ 做人机对战游戏:从课程设计到能答辩的项目
如果你正在找 C++ 课程设计或毕业设计的题目,宠物小精灵人机对战是一个很聪明的选择。它不像图书管理系统那样只是增删改查,也不像纯算法题那样难以展示界面,而是把面向对象三大特性、Qt 信号槽、绘图事件、网络通信这些知识点全部串在了一个看得见、玩得起来的项目里。这套源码我在本地完整跑过,客户端能登录、能选择精灵、能进入战斗画面,服务端能处理匹配和战斗结算,读代码的过程中能明显感觉到作者在模块划分上是花过心思的。
更关键的是,这套资源不是那种只有几个文件、一运行就报错的半成品。它有独立的 problem3_client 客户端工程和 problem3_server 服务端工程,还配了 database.cpp 做数据持久化,精灵、玩家、战斗主循环都拆成了单独的类。对准备答辩的人来说,可以从「面向对象设计」「客户端服务器架构」「GUI 事件驱动」三个方向任意切入讲深,内容量足够撑起一篇完整的课程设计报告。
2. 先拆工程结构:client 和 server 为什么必须分开
很多人拿到源码第一反应是直接编译,但我建议你先花十分钟把文件结构过一遍。这套项目分成 problem3_client 和 problem3_server 两个独立的 Qt 工程,这种拆分方式在课程设计里不算常见,但恰恰是它最值得写进报告的地方。
2.1 两个工程各自负责什么
先看客户端 problem3_client 目录下的文件:
problem3_client/ ├── main.cpp # 程序入口,创建 QApplication 和主窗口 ├── mainwindow.h / .cpp # 主窗口类,管理登录界面和战斗界面切换 ├── mainwindow.ui # Qt Designer 设计的界面布局 ├── pokemon.h / .cpp # 精灵类,定义属性和攻击逻辑 ├── pokemonbase.h / .cpp # 精灵基类,虚函数声明攻击方法 ├── player.h / .cpp # 玩家类,记录玩家信息和精灵队伍 ├── definition.h # 公共宏定义和枚举 ├── image.qrc # 图片资源文件,打包精灵图片 └── problem3_client.pro # qmake 工程文件服务端 problem3_server 目录下的文件结构类似,但多了 database.h 和 database.cpp。这就是关键差异:客户端只管界面展示和操作反馈,服务端负责账号校验、对战匹配和结果存储。登录请求从客户端发出去之后,由服务端的 database.cpp 完成真实的账号验证,再把结果返回客户端。
这种架构带来的好处很直接:战斗结果和账号数据的记录都在服务端完成,客户端即使被反复关闭重开,数据也不会丢,因为数据库操作走的是服务端进程。做答辩展示的时候,你可以现场同时开两个程序,然后用数据库文件里的内容变化来证明「客户端和服务端确实分离了」。
2.2 为什么这种拆分值得写进报告
很多课程设计项目会把所有逻辑塞进 mainwindow.cpp,一个文件几千行,看起来代码量很大但架构一塌糊涂。这套项目的拆分方式是:mainwindow 只管界面状态,pokemon 管精灵属性,player 管玩家,database 管持久化。各文件之间的依赖关系是单向的,mainwindow 依赖 player 和 pokemon,但不反向依赖。
你写报告时可以这样描述这个决策:「客户端负责表现层逻辑,服务端负责数据层逻辑,两者通过 TCP Socket 通信,这样做的目的是降低界面代码与业务逻辑的耦合度。」这句话在答辩中能帮你直接回应「为什么这样设计」的问题。
编译顺序上也有讲究。先编译服务端,再编译客户端,因为在本地联调时,需要先启动服务端监听端口,客户端才能连接成功。如果顺序反了,客户端会一直卡在「连接服务器」的状态,这个问题会在后面避坑章节里详细展开。
按 qmake 方式打开工程时,我建议直接用 Qt Creator 打开 problem3_server.pro 和 problem3_client.pro 这两个文件,不要手动建空工程往里拖代码,因为 .pro 文件里已经配好了 QT += core gui network widgets 这些模块,UI 文件和资源文件也已经在 .pro 中声明。如果你用 CMake 新建工程,反而要多写很多 platform 相关的配置。
3. 精灵系统的面向对象设计:基类、虚函数和四种精灵子类
这套项目最核心的学习价值在 pokemon 相关的几个类上。宠物小精灵的属性包括种类、名字、等级、经验值、攻击力、防御力、生命值、攻击间隔,其中种类决定种族特性,而攻击方法由每种精灵各自实现。这正是教科书里「基类定义接口、子类实现行为」的标准案例。
3.1 基类 pokemonbase 的设计思路
先看基类的核心结构:
// pokemonbase.h class PokemonBase { public: PokemonBase(); virtual ~PokemonBase() {} // 虚函数声明攻击方法,子类必须各自实现 virtual void attack(PokemonBase* target) = 0; // 公共属性读写接口 void setHP(int hp) { m_hp = hp; } int getHP() const { return m_hp; } void setAttackPower(int atk) { m_attackPower = atk; } int getAttackPower() const { return m_attackPower; } void levelUp(); // 升级逻辑:主属性增加量相对较多 void gainExp(int exp); // 获取经验值,满级 15 级封顶 protected: int m_level; // 等级,初始 1 级 int m_exp; // 经验值 int m_attackPower; // 攻击力 int m_defense; // 防御力 int m_hp; // 生命值 int m_attackInterval; // 攻击间隔,影响攻击频率 QString m_name; // 精灵名字 QString m_race; // 种族特性描述 };关键点在virtual void attack(PokemonBase* target) = 0;这行。这是一个纯虚函数,意味着 PokemonBase 是一个抽象类,不能直接实例化。你只能创建小火龙、妙蛙种子、杰尼龟、皮卡丘这些具体子类来使用。这个设计的业务含义是:不同种类的精灵攻击方式完全不同,但战斗系统只需要统一地调用attack()接口,不需要关心具体是哪个精灵在攻击。
这种「统一接口、各自实现」的模式在游戏开发里叫策略模式的前置形态。答辩时如果老师问「为什么要用虚函数」,你可以说:「因为战斗逻辑中需要以相同的方式处理不同类型的精灵,如果不用虚函数,就需要在调用处用 if-else 判断精灵类型再分别调用不同函数,这样每加一种新精灵就要改调用代码,违反开闭原则。」
3.2 四种精灵子类的属性和攻击方法
四种精灵的子类设计如下表:
| 精灵名字 | 种族 | 种族特性 | 主属性 |
|---|---|---|---|
| 小火龙 | 力量型 | 高攻击力 | 攻击力 |
| 妙蛙种子 | 肉盾型 | 高生命值 | 生命值 |
| 杰尼龟 | 防御型 | 高防御 | 防御力 |
| 皮卡丘 | 敏捷型 | 低攻击间隔 | 攻击间隔 |
用小火龙的子类实现来对照基类:
// pokemon.cpp 中的一个小火龙实现 class Charmander : public PokemonBase { public: Charmander() { m_name = "小火龙"; m_race = "力量型"; m_level = 1; m_exp = 0; m_attackPower = 15; // 力量型初始攻击力高于其他精灵 m_defense = 5; m_hp = 40; m_attackInterval = 2; // 攻击间隔 2 秒 } void attack(PokemonBase* target) override { int damage = m_attackPower - target->getDefense(); if (damage < 1) damage = 1; // 保证每次攻击至少造成 1 点伤害 target->setHP(target->getHP() - damage); } };注意攻击力的计算方式:damage = 攻击方攻击力 - 防御方防御力,然后做了下限保护。这个细节很值得在报告中展开:如果没有if (damage < 1) damage = 1;,就会出现高防御精灵完全免伤的情况,对战会陷入无限僵局。加了下限保护之后,即使防御极高,每回合也至少掉 1 点血,保证了战斗必然有终结。
攻击间隔这个属性在人机对战中会体现为攻击频率不同。皮卡丘的攻击间隔数值最小,意味着它在相同时间内能出手更多次,补偿了攻击力偏低的劣势。答辩时可以举例:「皮卡丘和杰尼龟对战,杰尼龟每次打出 20 点伤害,皮卡丘每次只打 8 点,但皮卡丘出手频率是杰尼龟的两倍,最终总伤害相当,这就是种族特性平衡的体现。」
3.3 升级机制:主属性加成与满级限制
升级逻辑是展示你对业务理解到位的关键点。每个精灵初始等级为 1,满级十五级,每升一级,精灵对应的属性值会有少量增加,且主属性增加量相对较多。常见的实现方式是每个子类重写 levelUp 或为基类提供虚函数applyLevelUpBonus():
// pokemonbase.cpp 中的升级实现 void PokemonBase::levelUp() { if (m_level >= 15) { m_exp = 0; return; } m_level++; // 通用属性增加 m_hp += 5; m_attackPower += 2; m_defense += 1; // 调用虚函数,让子类增加主属性 applyLevelUpBonus(); } // 小火龙子类中重写 void Charmander::applyLevelUpBonus() override { m_attackPower += 8; // 力量型主属性额外增加 }这里将「通用成长」和「种族专属成长」拆开了。通用成长保证所有精灵的基础数值都能跟上等级提升,种族成长拉大不同精灵之间的特性差异。妙蛙种子升级时生命值多加,杰尼龟升级时防御力多加,皮卡丘升级时攻击间隔缩短。这种设计让每种精灵在十五级时都有鲜明的数值特征,而不是变得千篇一律。
写报告时这段逻辑值一整页。你可以画一张表列出四个精灵在 1 级和 15 级时的五维数值对比,直观展示成长曲线差异,这张表放到答辩 PPT 里效果很好。
4. 主界面与战斗流程:QMainWindow、状态切换和信号槽
windows 程序的核心是事件循环,Qt 程序的骨架是 QMainWindow 配合信号槽。打开 mainwindow.cpp 之后,你会发现整个游戏的界面流转其实是在一个主窗口内部切换不同的界面状态。这个做法在毕业设计中很常见,核心是维护一个当前界面状态的枚举值。
4.1 从登录到战斗:界面状态如何切换
典型流程是:程序启动显示登录界面 → 输入账号密码点击登录 → 连接服务端校验 → 校验通过后切换到精灵选择界面 → 选择精灵后点击开始战斗 → 服务端分配对手 → 客户端进入战斗界面 → 战斗结束显示结果并回到精灵选择界面。
这个状态切换可以用 Qt 的 QStackedWidget 来实现,也可以用动态创建和销毁 QWidget 的方式实现。这套源码用了 QStackedWidget 思路,在 mainwindow.ui 里预置了多个页面。状态切换的核心代码如下:
// mainwindow.cpp 中切换页面的逻辑 void MainWindow::switchToFightPage() { // 切换到战斗界面页 ui->stackedWidget->setCurrentIndex(FIGHT_PAGE_INDEX); // 创建玩家选择的精灵 PokemonBase* myPokemon = createPokemon(selectedPlayer.getSelectedPokemonId()); // 获取服务端匹配到的对手精灵 PokemonBase* enemyPokemon = serverConn->getMatchedPokemon(); // 把双方精灵对象传入战斗控制器 fightController->startBattle(myPokemon, enemyPokemon); }这段逻辑说明两件事:第一,界面切换只是setCurrentIndex一行代码的事,真正的业务逻辑在切换行为发生之后通过调用函数来执行;第二,战斗控制器接收的是两个 PokemonBase 指针,这意味着战斗系统完全不关心具体是哪个精灵参战,只要是四个精灵中的任意一个都能正常战斗。
这也解释了为什么基类要设计为抽象类。战斗控制器只依赖attack()接口和属性读写接口,具体传入的是小火龙还是皮卡丘都不影响战斗系统运行。
4.2 战斗循环实现:攻击间隔与自动对战
人机对战的本质是计时器驱动下的自动攻防。先看战斗逻辑的简化实现:
// 战斗控制器中的核心循环 void FightController::startBattle(PokemonBase* mine, PokemonBase* enemy) { m_myPokemon = mine; m_enemyPokemon = enemy; // 创建两个 QTimer,分别控制玩家和对手的出手 m_myTimer = new QTimer(this); m_enemyTimer = new QTimer(this); // 攻击间隔乘以 1000 换算成毫秒 m_myTimer->setInterval(mine->getAttackInterval() * 1000); m_enemyTimer->setInterval(enemy->getAttackInterval() * 1000); connect(m_myTimer, &QTimer::timeout, this, [=]() { if (m_myPokemon->getHP() > 0 && m_enemyPokemon->getHP() > 0) { m_myPokemon->attack(m_enemyPokemon); updateBattleUI(); // 刷新血量条和战斗日志 } }); connect(m_enemyTimer, &QTimer::timeout, this, [=]() { if (m_myPokemon->getHP() > 0 && m_enemyPokemon->getHP() > 0) { m_enemyPokemon->attack(m_myPokemon); updateBattleUI(); } }); m_myTimer->start(); m_enemyTimer->start(); }这里有个容易被忽略的细节:两个定时器互相独立,谁先触发取决于攻击间隔。皮卡丘的攻击间隔短,所以它的定时器触发频率更高,出手次数也更多。这就是敏捷型的优势在代码层面的落地。
updateBattleUI()负责把精灵血量同步到界面上的血条和文字。实战中还需要处理精灵血量归零后停止计时器的逻辑:
if (m_myPokemon->getHP() <= 0) { m_myTimer->stop(); m_enemyTimer->stop(); QMessageBox::information(this, "战斗结果", "你输了!"); // 切换到精灵选择界面 }注意这里「判断血量 » 0」的条件要放在攻击动作执行之前,避免精灵已经阵亡还继续攻击。这种边角细节如果漏掉,就会出现「精灵血量为 0 还在攻击」的逻辑漏洞,答辩时被老师当场指出会很尴尬。
4.3 绘图与资源管理:QPixmap 加载图片的几种姿势
精灵图片通过 image.qrc 资源文件打包进可执行文件里。Qt 程序在发布时常见的一个问题是图片资源路径写错导致图片加载不出来,正确的加载方式有两种:
// 方式一:通过资源路径加载(推荐) QPixmap pixmap(":/images/charmander.png"); ui->pokemonLabel->setPixmap(pixmap); // 方式二:通过相对路径加载(依赖当前工作目录) QPixmap pixmap("images/charmander.png"); ui->pokemonLabel->setPixmap(pixmap);用方式一时,图片路径前缀的冒号:/不能省,这是 Qt 资源系统的固定语法。用方式二时,图片路径相对的是程序启动时的工作目录,如果你从 Qt Creator 里运行时正常,直接双击 exe 却白屏,多半是当前工作目录变了导致找不到图片。
如果你把图片换成自己的素材,需要重新运行 rcc 资源编译器。在 Qt Creator 中对 image.qrc 右键选择 Add Existing Files 添加新图,然后重新构建即可。不要手动改 build 目录下的 resource 文件,没用,重新 qmake 后会被覆盖。
5. 避坑笔记:我跑这套源码时踩过的六个坑
代码项目拿到手之后,真正花时间的是解决环境问题和逻辑 bug。下面这几条是我实际复现时遇到的,按「现象 → 原因 → 解决」的格式整理出来,你参考时会省很多时间。
5.1 Qt 库版本混用导致链接崩溃
现象:编译时或者启动时出现fatal: cannot mix incompatible Qt library (version 0x50601) with this library之类的报错。
原因:你机器上同时安装了多个 Qt 版本,编译器在链接时把不同版本的 Qt5Core.dll 混用了。常见于系统里既有 Qt 5.15.2 又有 Qt 6.x,或者 Qt 安装路径设置加了多个版本。
解决:打开 Qt Creator 的「工具 → 选项 → Kits → 构建套件」,确认编译器、Qt 版本、CMake 三者指向同一个版本。比如你统一用 Qt 5.15.2 MSVC2019 64bit,那么编译器选 MSVC2019 64bit,Qt version 选 Qt 5.15.2。然后执行「构建 → 清理全部」,删除 build 目录里的所有文件再重新构建。仍然报错就用 Qt 安装目录下的MaintenanceTool把多余版本卸载或取消勾选。
5.2 linuxfb 平台插件找不到
现象:在 Linux 或 ARM 嵌入式环境下运行程序时出现qt.qpa.plugin: Could not find the Qt platform plugin "linuxfb"之类报错,程序崩溃。
原因:程序依赖的 Qt 平台插件路径不对,系统没找到 libqlinuxfb.so。
解决:设置环境变量指定 Qt 平台插件路径,例如:
export QT_QPA_PLATFORM_PLUGIN_PATH=/opt/Qt/5.15.2/gcc_64/plugins/platforms export QT_QPA_PLATFORM=linuxfb ./problem3_client如果你是在 Ubuntu 桌面上运行,把linuxfb换成xcb即可。这个报错在交叉编译到嵌入式板子时出现的概率很高,因为目标板上没有完整的 Qt 安装目录。
5.3 客户端连接不上服务端,一直显示登录失败
现象:先启动客户端再启动服务端,或者只启动了客户端,登录时提示无法连接服务器,和账号密码错误的表现完全不同。
原因:这套架构是 client 通过 TCP Socket 连 server,服务端没启动时客户端请求发出去没人应答。
解决:严格按先服务端、后客户端的顺序启动。在 Qt Creator 中可以先运行 problem3_server,确认控制台输出监听成功的信息后,再运行 problem3_client。如果你要单步调试客户端断点,服务端必须保持运行状态。端口相关的配置在两端都要一致,检查客户端连接函数和服务端监听函数里的端口号是否相同。
5.4 精灵图片显示为空的方框
现象:程序运行正常,但战斗界面的精灵图片没有渲染出来,显示为一个空白区域。
原因:image.qrc中添加的图片路径与代码中引用的资源路径不一致。资源路径的根目录是 .qrc 文件所在目录,而代码里写的是相对于根目录的路径。
解决:在 Qt Creator 左侧资源文件 image.qrc 上双击打开资源编辑器,检查里面的路径前缀。比如 .qrc 里设置的路径前缀是/images,文件名为charmander.png,代码里的引用路径就是":/images/charmander.png"。另外注意图片名不要写成中文,资源系统对非 ASCII 文件名的支持在不同平台上行为不一致,容易出问题。
5.5 直接双击 exe 运行闪退,从 Qt Creator 里运行却正常
现象:发布程序后发给别人,对方双击 exe 程序秒退,但你自己在 Qt Creator 里运行一切正常。
原因:exe 找不到 Qt 的 DLL 和插件。从 Qt Creator 运行时,它会自动添加 Qt 安装目录到 PATH 环境变量;直接双击运行时不会自动配置。
解决:用 windeployqt 工具把依赖打包到 exe 同级目录。假设客户端构建输出目录是build-problem3_client-Desktop_Qt_5_15_2_MinGW_32_bit-Debug,在该目录打开命令行:
windeployqt problem3_client.exe它会自动把 Qt5Core.dll、Qt5Gui.dll、Qt5Widgets.dll 和 platforms 目录复制过来。然后再把这个目录整个打成压缩包发出去。如果你还用了 MySQL 或其他第三方库,记得手动把对应的 DLL 一并放进去,windeployqt 不会处理非 Qt 依赖。
版本特别提醒:windeployqt 也有版本匹配问题。用 MinGW 的 Qt 编译出来的程序,用 MSVC 的 windeployqt 去部署会出兼容问题,务必用同一套 Kit 对应的工具。
5.6 高 DPI 缩放下界面错位文字模糊
现象:在 2K 或 4K 分辨率屏幕上运行时,Qt 窗口被拉伸,按钮和文字显示模糊或者位置对不上。
原因:Qt 5.6 之后高 DPI 缩放默认启用,但有些 Linux 桌面环境或 Windows 缩放设置下,会导致 UI 坐标计算异常。
解决:在 main.cpp 的QApplication创建之前设置环境变量禁用缩放的副作用:
#include <QApplication> int main(int argc, char *argv[]) { #if QT_VERSION >= QT_VERSION_CHECK(5, 6, 0) QApplication::setAttribute(Qt::AA_EnableHighDpiScaling); #endif QApplication app(argc, argv); // ... }如果启用了高 DPI 缩放仍然有问题,可以在 .pro 工程文件中加一行QT += gui确保 GUI 模块完整加载,然后重新构建。
6. 从课程设计到真正的项目:联调验证和三个可深挖的方向
源码能跑起来只是第一步,真正拉开差距的是你怎么验证它的正确性,以及能不能在现有基础上做出自己的扩展。这里给你一套从验证到进阶的行动路线。
6.1 验证项目正确性的完整流程
拿到代码后按照下面的步骤走一遍,确认环境没问题再开始改代码:
# 第一步:用 Qt Creator 打开 problem3_server.pro,先构建并运行服务端 # 构建套件选择 Qt 5.15.2 对应的编译器,Debug 模式 # 第二步:打开 problem3_client.pro,构建并运行客户端 # 如果编译报错找不到头文件,先在 .pro 文件里检查是否包含 $PWD 路径 # 第三步:在客户端登录界面输入测试账号 # 首次使用可以去服务端代码里找预设账号,或者直接到数据库文件里插入数据验证标准分四个维度:第一,登录失败时能否正常弹出错误提示,不会崩溃;第二,选择不同精灵后,战斗界面精灵属性数值是否正确显示;第三,对战过程中血量条是否按攻击频率实时减少,血量为零后是否及时停止所有定时器;第四,把客户端关闭后重新打开,服务端记录的玩家数据是否还在。
用 Debug 模式单步调试时,在攻击方法里设断点,观察damage变量有没有出现负数或异常大值。出现负数说明防御计算有 bug,出现异常大值说明攻击间隔或者升级成长数值有问题。这个过程是答辩时最有说服力的展示素材。
6.2 进阶方向一:把攻击方法改成带技能效果的策略模式
现在的攻击方法只是简单的数值减法。你可以扩展成使用技能的效果:为每种精灵加一个技能列表,技能包含伤害倍率、冷却时间、附加状态。比如杰尼龟的技能是「缩壳」,三回合内防御力提升百分五十;小火龙的技能是「喷火」,两倍伤害但命中率只有百分之八十。
这要求你对 pokemon.h 的接口做扩展,增加useSkill(int skillId, PokemonBase* target)方法,并在攻击方法里加入技能冷却计时。这个改造展示的是你对设计模式的理解,答辩时老师会问你对策略模式和状态模式的理解,你可以结合这段代码展开讲。
6.3 进阶方向二:把对战过程改成同步联机对战
当前的人机对战本质是两个定时器在本地轮流触发攻击逻辑。改成联机对战的核心是引入回合制协议:客户端 A 执行攻击后把伤害结果发给服务端,服务端转发给客户端 B,客户端 B 收到结果后更新界面。你需要定义一套自己的协议格式,比如用 JSON 序列化攻击指令和战斗状态。
这个改造的难点在于状态同步和延迟处理。你可以先不做实时同步,用「回合制 + 等待超时」的简化方案。这也是目前很多课设项目的实用选择,效果稳定且代码量可控。
6.4 进阶方向三:把数据存储从文件改成 SQLite
database.cpp 现在的实现可能是自己写的简单文件读写,你可以引入 QSQLITE 驱动,用标准 SQL 语句管理玩家账号和精灵信息。这能展示你在 Qt 的 SQL 模块方面有实际经验,也方便老师在答辩时随意抽查数据。改造完成后需要处理好数据库连接的线程问题,不要让多个线程同时操作同一个连接。
我做过好几个 Qt 相关的课程设计指导,一个很深的感受是:把一套能跑通的代码彻底研究明白,远比自己从零开始卡在环境配置上高效得多。这套宠物小精灵项目的代码注释虽然不算密集,但类名和函数命名都很规范,跟着调用链读一遍,整个项目的脉络就能梳理清楚。从那以后我每次拿到新源码,都会强制自己先跑通、再画调用链、最后再动手改,强制走完这三步才碰键盘。这个方法帮我避开了大半的伪复现陷阱,希望帮到你。
本文还有配套的精品资源,点击获取