ARTICLE DETAIL

资讯详情

深耕郑州网站建设与运营推广的一线实战洞察。

基于Qt与C++的宠物小精灵人机对战游戏:从数据建模到AI博弈的设计实践

基于Qt与C++的宠物小精灵人机对战游戏:从数据建模到AI博弈的设计实践 简介基于Qt与C实现的人机对战宠物小精灵游戏是一款可直接运行的完整项目适合作为计算机专业毕业设计、课程设计或进阶练手。游戏围绕小火龙、妙蛙种子、杰尼龟、皮卡丘四种精灵展开按力量、肉盾、防御、敏捷划分种族特性每种精灵拥有等级、经验、攻击力、防御力、生命值和攻击间隔等属性升级时主属性成长更多策略差异明显。项目设计了精灵基类与虚函数攻击方法并在子类中分别实现能帮助读者深入理解面向对象多态、继承以及Qt界面与游戏逻辑的整合技巧。压缩包共36个文件主体为11个cpp源文件和11个h头文件另有2个ui界面、2个pro工程配置、2个qrc资源、少量说明文档和截图整体约1.88MB工程结构清晰可快速导入Qt Creator编译运行。源码经过严格测试注释完整适合直接参考并能在此基础上扩展联机对战、存档系统、技能特效等功能。目前已有281人学习下载特别适合需要完整可运行项目作为课设或毕设起点的学习者。1. 基于QTC开发的宠物小精灵人机对战游戏为什么它是毕业设计里少见的“性价比之王”把“基于QTC开发的宠物小精灵人机对战游戏源码”这个题目交给学生第一反应往往是被“游戏”两个字带偏直接打开Qt Creator拖按钮。其实它真正考的是三件事把回合制规则转成C数据结构的能力、用Qt把数据呈现到界面的能力、以及给电脑编写行为可调的AI逻辑的能力。这三件事恰好覆盖了C面向对象、Qt信号槽、自定义绘图、状态机和基本算法做出的成品又能现场演示比管理系统和商城类题目更容易讲清楚工作量。我经常把这类项目推荐给两类人一类是C刚入门、想找一个“做完能跑、跑起来能玩”的项目练手的新手另一类是毕业设计题目被分到桌面应用方向、想避开CRUD增删改查的同学。它的核心难度并不在“宠物小精灵”这个题材本身而在于你愿不愿意把战斗规则、宠物属性和界面交互拆成独立的模块。拆得好这个项目四到六周能收工拆得不好光是在按钮回调里改bug就能改到交作业前一晚。2. 数据模型先行宠物小精灵的属性、技能与回合结算怎么设计2.1 先定战斗规则再写界面为什么不能上来就画窗口见过不少同学拿到题目第一件事是打开Qt Designer把宠物小精灵的窗口轮廓拖出来然后发现按钮有了、界面有了却不知道该点哪里。原因很简单还没定义“一只宠物小精灵”在程序里是什么也没定义“一次攻击”会产生什么效果。界面是数据的投影数据没想清楚投影自然空转。我一般建议先花一晚上把游戏规则写成文字。常见做法是模仿回合制RPG那一套己方和对手各持有若干只宠物小精灵每回合选择技能或道具按速度高低决定谁先出手结算伤害和状态血量归零则换下一只全部阵亡的一方输。规则里有三个可以被C直接翻译的东西属性、宠物个体、技能。属性是一个枚举宠物个体是一个结构体技能是结构体里的一个数组。先把这个模型定下来后面的窗口绘制、AI逻辑和存档功能就都有了地基。2.2 用struct和enum把“一只宠物小精灵”变成可计算的C对象Qt开发里最容易被初学者忽略的是Qt本身并没有替代C的类型系统它只是提供了QString、QVector这些容器。宠物小精灵的建模还是得用原生C语法。下面这个头文件是我在类似项目里常用的最小集合// pet.h #ifndef PET_H #define PET_H #include QString #include QVector enum class Element { None, Fire, // 火 Water, // 水 Grass, // 草 Electric // 电 }; struct Skill { QString name; Element type; int basePower; // 基础威力 int accuracy; // 命中率 0~100 }; struct Pet { QString name; // 宠物小精灵名称 Element element; // 属性 int maxHp; int hp; // 当前血量 int atk; int def; int speed; QVectorSkill skills; // 最多带 4 个技能 }; #endif // PET_H这段代码没有用到任何Qt界面类所以可以直接用纯C单元测试去验证。struct Pet里我特意让当前血量和最大血量分开是因为UI上需要显示“23/50”这样的血量条如果只存一个hp回血和扣血时还要自己记上限。技能用QVector而不是固定数组是因为换技能、学新技能时不需要手工维护数组长度这也是Qt容器在游戏数据层最舒服的用法。出场宠物小精灵的数值我一般控制在50到100之间技能威力控制在10到40之间。这个范围不是随意定的数字太小属性克制系数乘上去之后看不出差距数字太大一场战斗两三招就结束玩家还没来得及体验策略就已经赢了。2.3 把回合流程做成状态机给战斗逻辑一个可以先脱离界面的测试环境宠物小精灵类游戏最容易写乱的是战斗流程。如果用按钮回调里的一串if层层嵌套来表达会出现一个到处都能改状态、却没人说得清当前处于哪个阶段的情况。我习惯把回合流程收敛成一个状态机每个阶段用一个枚举值表示框架大致如下// battle_manager.h #ifndef BATTLE_MANAGER_H #define BATTLE_MANAGER_H #include pet.h #include QObject class BattleManager : public QObject { Q_OBJECT public: enum class BattleState { SelectAction, // 等待玩家选择技能 ResolveTurn, // 结算本回合行动 CheckEnd, // 检查胜负 BattleOver // 战斗结束 }; Q_ENUM(BattleState) explicit BattleManager(QObject* parent nullptr); void setParty(const QVectorPet playerPets, const QVectorPet enemyPets); void playerChooseSkill(int petIndex, int skillIndex); void enemyChooseAction(); signals: void stateChanged(BattleState newState); void petHpChanged(int petIndex, bool isEnemy, int newHp); void battleFinished(bool playerWin); private: void resolveTurn(); void checkEnd(); QVectorPet m_playerPets; QVectorPet m_enemyPets; BattleState m_state BattleState::SelectAction; }; #endif // BATTLE_MANAGER_H这个类继承了QObject是因为我想让界面层能订阅它的stateChanged信号这样UI只在状态变化时刷新而不是每帧都重绘。把战斗核心放到一个独立类里还有个额外好处在界面做出来之前我可以先写一段不依赖窗口的测试代码手动调用playerChooseSkill和enemyChooseAction用qDebug打印血量变化。你会注意到这里已经出现了一点Qt MVVM框架的味道BattleManager是模型层它不知道界面上有什么按钮只负责状态和数据的更新界面通过信号订阅来同步显示。如果后续想把项目升级成QML版或者引入更完整的Qt MVVM框架分层这个核心类可以原封不动地搬过去只需要重新写界面层。3. Qt界面设计从工程配置到用QPainter和信号槽把对战跑起来3.1 Qt工程该用qmake还是CMakeMSVC和MinGW怎么选新人在创建Qt工程时最先遇到的不是写代码而是工具链选择。Qt Creator打开新建项目向导会看到“构建系统”和“工具包”两个选择。构建系统我建议直接用qmake除非老师明确要求CMake。qmake在Qt 5.15.2里配置最快添加资源文件、链接模块都自动完成适合课程设计和毕业设计这种需要快速出成果的场景。CMake的优势只在大型跨平台工程里才体现得出来为一个宠物小精灵对战项目引入CMake等于给自己加戏。工具包的选择要看你最终在哪台机器上演示。如果全程用自己的Windows电脑我推荐MSVC 2019 64位版本如果要在老师机器或机房电脑上跑MinGW版本反而更省心因为MinGW运行时是静态链接方案出问题少。下载Qt时常见做法是选官方在线安装器也可以直接找国内镜像站下载离线包注意离线包通常把MSVC和MinGW分开打包装错了会出现在第5章里讲的那个经典报错。VS Code配置C/C环境同样是能写这个项目的配合Qt官方插件也能编译调试但我不建议新手这么做。Qt Creator的调试器集成、UI设计师、帮助文档快捷键这几样东西在独立开发一个带窗口项目时比轻量化编辑器实用得多。3.2 自定义绘图控件用QPainter把精灵球和战斗面板画出来宠物小精灵项目里最提气的一块是战斗界面谁都不想用一排默认按钮糊弄。但放一张图片做背景再叠几个QLabel又显得交互单薄。我通常会做一个自定义QWidget子类负责画精灵球图标、宠物小精灵的Q版轮廓和血量条。核心是重写paintEvent用QPainter画线条和色块。// pet_panel.h #ifndef PET_PANEL_H #define PET_PANEL_H #include QWidget #include QPainter #include QColor #include pet.h class PetPanel : public QWidget { Q_OBJECT public: explicit PetPanel(QWidget* parent nullptr); void setPet(const Pet pet); void setHpRatio(double ratio); protected: void paintEvent(QPaintEvent* event) override; private: Pet m_pet; double m_hpRatio 1.0; }; #endif // PET_PANEL_HsetPet和setHpRatio是两个对外入口界面其他部分只需要调它们不需要关心怎么画。paintEvent里的绘制逻辑按顺序是画一个精灵球圆形底色画中间的横带画血量条背景再按m_hpRatio画前景色条。血量低于30%时把前景色从绿色切换成红色这是一个能让演示效果立刻变得“像游戏”的小细节。注意自定义控件一定要在构造函数里设置setMinimumSize否则窗口布局会把控件压缩成一个小点出现“画面缺失”的翻车现场。如果绘制内容复杂第一帧可能空白这时可以在构造时调用update()强制触发第一次重绘或者直接把setPet放在构造函数参数里。3.3 信号槽串起“出招→结算→刷新界面”一套完整的交互链路当数据和界面都准备就绪就可以把按钮、下拉框和战斗管理器用信号槽连起来。这个链路我建议统一采用“界面点击 → 调用控制器接口 → 战斗管理器发信号 → 界面刷新”的顺序不要让界面直接修改战斗管理器的私有数据。// 在 MainWindow 构造函数中 connect(ui-btnFire, QPushButton::clicked, this, [this]() { int petIndex ui-comboPet-currentIndex(); int skillIndex 0; // 0 对应火系技能 m_battle-playerChooseSkill(petIndex, skillIndex); m_battle-enemyChooseAction(); m_battle-resolveTurn(); refreshAllPanels(); });上面这段代码里界面不读取任何结构体内部字段只通过返回值或信号刷新。refreshAllPanels内部再去BattleManager读取当前血量、胜负状态然后调用PetPanel的setHpRatio。这符合Qt界面设计里“视图不碰模型核心”的惯例代码看起来多了一层调用但实际上把出招、AI行动、结算三个阶段分开了调试时可以很清楚地知道是哪一步出了问题。如果觉得每次手动调用三个步骤太啰嗦可以把这三步合并成BattleManager::startTurn()这样一个槽函数。按钮只管发信号管理器内部决定执行顺序。这样后续增加“进入下一回合倒计时”之类的动画时只需要在startTurn里加延时处理不需要改按钮的lambda。4. 人机AI与数值平衡让电脑不是纯随机出招4.1 基于效用函数的人机AI从“闭眼乱按”到“会换宠、会补血”宠物小精灵对战的对手如果只会随机出招玩家玩几局就会觉得无聊。但要写成“必胜AI”又没意义因为课程设计需要展示的是算法思想不是做一个不可能打赢的电脑。我常用的是效用函数加随机扰动给每个可选技能打分分数高者更可能被选中但保留一定概率选第二高分这样AI既聪明又不至于被玩家摸透。// ai_agent.cpp #include ai_agent.h #include QtGlobal #include cmath int AIController::chooseActionIndex(const Pet actor, const Pet foe, const QVectorSkill skills) { QVectordouble scores; for (const Skill skill : skills) { double typeFactor typeEffect(skill.type, foe.element); double damage estimateDamage(actor, foe, skill, typeFactor); double score damage * (0.7 0.6 * typeFactor); // 当自己残血时把高威力技能评分压低减小无脑硬拼概率 double hpRatio double(actor.hp) / actor.maxHp; if (hpRatio 0.3) { score * 0.6; } scores.append(score); } // 先按分数排序再在 top2 里随机选 QVectorint order; for (int i 0; i scores.size(); i) order.append(i); std::sort(order.begin(), order.end(), [scores](int a, int b) { return scores[a] scores[b]; }); int bestIdx order[0]; if (scores.size() 1 (qrand() % 100) 35) { bestIdx order[1]; } return bestIdx; }typeEffect返回属性克制系数比如火打草是2.0草打火是0.5。estimateDamage这里不需要精确模拟实际伤害只需要一个和真实伤害成比例的估算值因为AI决策不关心个位数的血量误差。随机那一步用qrand() % 100的写法在Qt 5.15里没问题注意在使用前调用qsrand(QTime::currentTime().msecsSinceStartOfDay())做一次种子初始化否则每次启动AI行为都完全相同容易被看出是伪随机。这个“35%概率选第二优技能”的参数我推荐保留在配置文件或宏定义里方便调节难度。改成10%就是困难AI改成70%就是娱乐AI。4.2 伤害公式和速度先手数值参数是宠物小精灵对战手感的关键伤害公式如果不讲究容易出现“一拳秒杀”或者“双方互相刮痧”。我尽量让伤害期望落在目标最大血量的15%到25%之间这样一场战斗大约持续5到8个回合有足够空间展示换宠、残血反杀这些策略。下面是简化版本的核心公式int estimateDamage(const Pet attacker, const Pet defender, const Skill skill, double typeFactor) { double base 2.0 * attacker.atk * skill.basePower / qMax(1, defender.def); base base / 10.0 2.0; // 随机浮动 0.85 ~ 1.15 double random 0.85 (qrand() % 31) / 100.0; return qMax(1, int(base * typeFactor * random)); }basePower是技能威力范围30到40时一个攻击力50的宠物小精灵打防御力40的对手基础伤害大约落在10到20点正好是50点血量的20%到40%。防御力对这个结果的影响是线性的不会出现极端值。typeFactor直接乘在最后属性克制成了胜负手这就逼着玩家去了解和记忆属性关系游戏策略性就出来了。速度先手规则我放在resolveTurn里处理比较双方speed快的那一方先执行伤害如果先手已经打死了对手后手不再反击。这个规则会带来一个问题速度快的宠物小精灵会同时享受“先手”和“攻击机会”两个优势导致某些高速高攻角色过于强势。我的解决办法是把攻击力调低一点或者给技能增加5%的miss率。miss率在命中率accuracy里体现这个参数记得在UI的提示文本里显示否则玩家会觉得“我明明打中了怎么没伤害”。5. 避坑能编译通过不等于能交作业这五条我基本每次都会踩5.1 现象Qt Creator在MSVC工具包下直接报“-1: error: dependent ...\qt\5.15.2\msvc2019_64\include\qtwidgets”之类的依赖错误这是我见过最频繁的Qt环境问题。报错本身的共同点是错误信息带一长串绝对路径路径里能看到qt、5.15.2、msvc2019_64、include这些关键词而且往往是在新装Qt之后第一次编译就出现。原因一般是两个第一种是MSVC工具包安装不完整库路径没被正确添加第二种是打开了另一个Qt版本的工程项目经理里选错了工具包导致头文件路径、库路径和正在使用的编译器不匹配。解决起来先检查“工具 → 选项 → Kits → 构建套件”里当前Kit对应的Qt版本和编译器是否高亮显示为绿色若带黄色感叹号就重新选择Qt版本路径再把项目构建目录整体删除在“构建”菜单里执行“清理全部”最后重新构建。如果仍然报错查看“Qt版本”标签里是否出现了“无”字样的路径八成是Qt安装目录被移动过重新指定到真正安装位置即可。5.2 现象界面上的宠物名字显示成乱码或者源文件里的中文注释在MSVC下直接报错这个问题在Windows上很典型。Qt 5默认源码格式是UTF-8而MSVC编译器在没有BOM的情况下会按本地代码页去读结果就是中文字面量变成“鈥滃”之类的内容严重时直接编译失败。原因不是Qt问题而是编码不匹配。解决有两个方向要么把所有源文件另存为UTF-8 with BOM让MSVC自动识别要么在代码里全用QStringLiteral(皮卡丘)这样的宏Qt官方建议是后者。但QStringLiteral也不能彻底解决注释乱码所以我个人会给整个工程统一设置编码并在pro文件里加上msvc: QMAKE_CXXFLAGS /utf-8这样源代码和注释都能安心用中文写。5.3 现象生成exe后拷到别的电脑双击运行直接弹窗说缺MSVCP140.dll、VCRUNTIME140.dll或者干脆“应用程序无法正常启动”这是典型的发布时没有带上运行库。调试时Qt Creator会同时设置Qt的bin目录和编译器运行库路径程序能跑是环境托了底拷贝exe出去后这些路径全部失效。原因有两个一是Qt动态库没带上二是MSVC运行库没装上。解决方法是使用windeployqt工具在Qt命令行工具里执行“windeployqt 你的exe路径”它会自动把需要的Qt模块、插件、样式表复制到exe同级目录。MSVC运行库方面带上vc_redist.x64.exe让目标机器安装或者更省事的是改用MinGW工具包构建MinGW的运行时不会出现MSVCP140.dll这种依赖。一个血泪经验是别只拷Debug版exeDebug版体积大且额外依赖调试运行库发给老师前记得构建一次Release版。5.4 现象自定义绘图控件在窗口切换后一片空白放大缩小时图形撕裂或闪烁这个坑出现在我写PetPanel的第三版当时为了省事paintEvent里只画了当前状态没有处理更新信号。原因通常有三种控件没有收到update调用、绘制依赖的背景图还没加载完、或者没有启用Qt的自动双缓冲。Qt的QWidget默认就开了双缓冲但自定义控件里如果调用了QPainter::begin(this)之后再画结束后忘了end也会出现异常。解决上先看刷新入口是不是所有修改宠物血量、切换宠物的路径都触发了update();再看是不是把耗时操作放在了paintEvent里比如在paintEvent里读文件第一次绘制会卡到界面假死最后在构造函数里加一句setAttribute(Qt::WA_OpaquePaintEvent)能减少一部分重绘开销。5.5 现象qrc资源文件里的图片设成了路径却显示不出来换成绝对路径也无效很多人在资源管理器里把图片拖进qrc然后写“:/images/pikachu.png”运行时就是找不到资源。原因在于qrc里的“前缀”和文件路径是两回事默认前缀是“/”文件在qrc里显示为“/images/pikachu.png”没错但代码里常常写成了“images/pikachu.png”少了开头的冒号。还有一种是文件路径被加到了“/images”前缀下调用时写的却是另一个前缀。解决方法是点开qrc编辑器看清前缀列写的是什么再对照资源浏览器里的完整路径去复制。更稳的做法是在资源文件上右键选择“复制资源路径”直接把Qt生成的路径粘贴到源码里不要手敲手敲就会踩坑。6. 收尾交作业前的一小时我具体做哪五件事来保证演示不出丑如果明天就要演示我不再改任何战斗公式而是按固定顺序做五件事。第一件是把整个项目重新构建一次Release版在Qt Creator的“项目”模式里把构建套件切到Release缓存目录也指到新文件夹避免Debug和Release的中间文件互相污染。第二件是打开Qt命令行工具执行windeployqt把运行库打全然后把整个文件夹拷到一台没有装Qt的机器上试跑重点看血量条、技能图标、背景音乐资源能不能加载。第三件是过一遍流程连续打完三场战斗故意拖到残血再换宠确认不会出现血量先归零然后还能出招的状态错误同时把窗口拉大缩到各个尺寸看布局有没有崩坏。第四件是给源码做一次“减脂”。我会检查头文件里有没有一堆用不到的#include把整个工程里的调试qDebug删掉或注释把没有注释的函数补齐说明。这步看起来不占工作量但答辩时老师会翻源码清爽的代码观感能抵掉十页PPT。第五件也很重要确保退出程序时能正确保存战斗数据。如果宠物小精灵有养成系统就用QSettings保存当前队伍、血量、等级没有养成系统就直接跳过但一定保证点击关闭按钮时进程不会卡死在后台任务管理器里看不到残留在运行的exe。做完这五件事我还有十分钟我会把QPainter绘制战斗面板那段代码单独截出来看一遍确认它没有在窗口初始化的第一帧就触发了空指针读取。曾经有一次我在构造函数里setPet传入了临时对象临时对象析构后PetPanel里保存的引用失效程序一启动就qt崩溃后来改成传值拷贝才算解决。从那以后自定义控件里的数据我都用值类型保存不在QWidget里存指针引用这是我在宠物小精灵项目上最深刻的一条教训。如果你也打算拿这个题目做毕业设计或者课程设计我的建议是从第2章的数据模型开始别从界面开始。界面会让你产生“我做了很多”的错觉数据模型才会让后面每一步都有迹可循。把BattleManager写成一个不依赖界面也能跑的类你会省掉至少一整个星期的联调时间。希望这篇笔记帮到你祝你也能写出一只能打能画的宠物小精灵。本文还有配套的精品资源点击获取
返回列表