ARTICLE DETAIL

资讯详情

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

基于Qt/C++的宠物小精灵人机对战:双工程、多态与完整战斗链路解析

基于Qt/C++的宠物小精灵人机对战:双工程、多态与完整战斗链路解析 简介基于QT与C实现的宠物小精灵人机对战游戏完整项目源码主要面向需要完成毕业设计、课程设计或进行项目开发的院校学生与开发者。压缩包共36个文件大小1.88MB以11个cpp源文件、11个h头文件、2个ui界面、2个pro工程文件为主体辅以png图片、qrc资源与md说明文档目录结构清楚便于在Qt Creator中直接载入和修改。目前已有281人学习下载。项目采用精灵基类加子类的面向对象设计涵盖小火龙力量型高攻、妙蛙种子肉盾型高血、杰尼龟防御型高防、皮卡丘敏捷型低攻击间隔四种精灵内置等级、经验、攻防血量及攻击间隔等属性并实现了1至15级的升级成长机制通过虚函数为每种精灵定制攻击方法。代码经过严格测试人机对战流程完整适合作为课设或毕设的起点也可在其基础上扩展精灵种类、技能特效或网络对战功能。1. 基于QTC的宠物小精灵人机对战两个工程、四种精灵、一条完整战斗链路做毕业设计的人最怕什么不是需求看不懂是下载一堆源码之后发现「截图好看、编译不过、跑起来直接黑屏」。这份基于QTC开发的宠物小精灵人机对战游戏源码是少见的客户端、服务端都齐的Qt项目problem3_client 管界面交互problem3_server 管战斗逻辑和存档包里的 login.png、fight.png、fail.png 和 readme.md 都能对上号。它不是单文件 demo而是带 .pro 工程、.ui 界面文件、image.qrc 和 res.qrc 的完整项目适合拿它改毕业设计底子的人也适合想拆开看客户端/服务端、虚函数多态、SQLite 存档怎么拧在一起的课程设计。四种精灵——小火龙、妙蛙种子、杰尼龟、皮卡丘——分配了力量、肉盾、防御、敏捷四条路线正好落进 C 多态的考核点。下文从工程结构、精灵类设计、对战链路拆到编译排错结尾给一个答辩前值得做的存档校验技巧。跟着走完这个包大概率能顺利落地。2. QT工程结构拆解problem3_client 与 problem3_server 各管什么2.1 两个.pro文件的分工界面端放client逻辑端放server打开压缩包第一件事是先看清目录。根目录下是 readme.md、README.md 和三张截图真正住进代码的是 problem3_server 与 problem3_client 两个子目录每个目录里都有一份 .pro 工程文件。这种 client/server 分离的布局在 Qt 游戏毕设里非常常见客户端只负责发指令和渲染服务端负责状态计算与数据落盘两边通过 TCP 端口通信。.pro 文件决定了这个工程用哪些 Qt 模块、编译出什么目标。以服务端为例常见做法是这样组织QT core gui network sql greaterThan(QT_MAJOR_VERSION, 4): QT widgets TARGET problem3_server TEMPLATE app SOURCES main.cpp mainwindow.cpp pokemonbase.cpp pokemon.cpp player.cpp database.cpp HEADERS mainwindow.h definition.h pokemon.h player.h pokemonbase.h database.h FORMS mainwindow.ui第一行的QT core gui network sql是关键network 支撑服务端与客户端的 TCP 通信sql 支撑后面的 SQLite 存档。如果这两个模块没挂进来代码里用了 QTcpSocket 和 QSqlDatabase 时编译可能侥幸通过链接阶段必然报 undefined reference。客户端那边通常不需要 sql只要 core、gui、widgets 和 network 就够改动时记得保持 client 不碰 server 的数据库逻辑把存档留在服务端。2.2 服务端SQLite落盘玩家表与精灵表怎么建目录清单里有一对 database.h / database.cpp对应的是服务端的数据库封装。Qt 做轻量存档基本都用 QSQLITE 驱动不需要额外装数据库软件一个 .db 文件就能把玩家和精灵数据全部装下。登录界面截图的背后就是服务端拿着用户名密码去查这张表。常见的设计是两张表一张 players 存账号一张 pokemons 存精灵快照。迁移这种项目时我一般会先建出下面的结构CREATE TABLE players ( id INTEGER PRIMARY KEY AUTOINCREMENT, username TEXT UNIQUE NOT NULL, password TEXT NOT NULL ); CREATE TABLE pokemons ( id INTEGER PRIMARY KEY AUTOINCREMENT, player_id INTEGER NOT NULL, kind TEXT NOT NULL, name TEXT NOT NULL, level INTEGER DEFAULT 1, exp INTEGER DEFAULT 0, hp INTEGER NOT NULL, attack INTEGER NOT NULL, defense INTEGER NOT NULL, attack_interval REAL NOT NULL, FOREIGN KEY(player_id) REFERENCES players(id) );players 表的username TEXT UNIQUE用来防止重复注册pokemons 表里 player_id 与 players.id 是多对一关系——一个玩家可以带多只精灵。level、exp、hp、attack、defense对应战斗结算时每次要回写的字段attack_interval用 REAL 存秒后面战斗循环靠它控制出手节奏。要注意 QSqlDatabase 的连接属于创建它的线程多线程读写同一个连接时 QSQLITE 会直接报 database is locked这在后面排查登录失败时非常常见。2.3 客户端到服务端的指令流TCP通信的常见写法两个进程要对话最直接的方式是 TCP。客户端登录时组装一个 JSON 指令包发出去服务端按 cmd 字段路由处理。JSON 的好处是字段可读、调试方便Qt 里用 QJsonDocument 处理它很轻量。发送端的典型写法// 构造登录指令包 QJsonObject obj; obj.insert(cmd, login); obj.insert(username, ui-usernameEdit-text()); obj.insert(password, ui-passwordEdit-text()); QByteArray payload QJsonDocument(obj).toJson(QJsonDocument::Compact); socket-write(payload); socket-flush();服务端收到后用QJsonDocument::fromJson(payload)解析取出 cmd 分发到对应处理函数。注意toJson(QJsonDocument::Compact)这个参数Compact 会把 JSON 压成一行字符串否则默认的 Indented 格式带换行和缩进客户端在同一个 TCP 包里一次读不完就会出现半包、粘包问题。发送完不要立刻 close要给对端留出读取时间否则会碰到「服务端还没收完包连接就断了」的诡异现象。3. 精灵类设计基类pokemon.h与四个子类的攻击差异化3.1 基类怎么定义属性字段与虚函数attack()项目正文里有一句话很关键设计一个精灵的基类并在基类中用虚函数声明攻击方法在子类中实现。这是标准的 C 多态考核点。基类 pokemon.h 通常持有种类、名字、等级、经验值、攻击力、防御力、生命值、攻击间隔这些字段同时声明一个纯虚的攻击接口。骨架长这样class Pokemon { public: // 用枚举固定种族数据库里 kind 字段与之一一对应 enum Species { CHARMANDER, BULBASAUR, SQUIRTLE, PIKACHU }; virtual ~Pokemon() {} virtual QString attack(Pokemon *target) 0; // 纯虚函数子类必须实现 int level() const { return m_level; } int hp() const { return m_hp; } protected: int m_level 1; // 初始等级固定为1 int m_exp 0; // 当前经验值 int m_hp; // 生命值 int m_attack; // 攻击力 int m_defense; // 防御力 double m_attackInterval; // 攻击间隔秒 };为什么攻击方法要用纯虚函数因为不同精灵的攻击表现和计算公式完全不同——小火龙和杰尼龟打同一个目标伤害系数就不该一样。基类只负责定契约传入目标指针、返回攻击文案具体怎么扣血交给子类。virtual ~Pokemon()是为了多态删除时的析构安全很多初学者会漏掉它后面一旦用std::shared_ptrPokemon存不同子类析构不全就会内存泄漏。3.2 四个子类实现小火龙、妙蛙种子、杰尼龟、皮卡丘四种精灵的数值定位在项目正文里写得很清楚小火龙力量型高攻击力妙蛙种子肉盾型高生命值杰尼龟防御型高防御皮卡丘敏捷型低攻击间隔。落实到代码就是构造函数里给初始属性赋值attack() 里用差异化的伤害公式。下面的数值是我按属性风格给的示意项目里实际数值以源码为准class Charmander : public Pokemon { public: Charmander() { m_hp 90; m_attack 60; // 主属性攻击力偏高 m_defense 30; m_attackInterval 2.5; } QString attack(Pokemon *target) override { // 力量型的伤害攻击力减半倍防御再加一点随机浮动 int damage m_attack - target-m_defense / 2; damage qMax(1, damage QRandomGenerator::global()-bounded(-5, 6)); target-m_hp - damage; return QString(小火龙使出火花造成 %1 点伤害).arg(damage); } };这里有两个容易说漏的点。第一如果直接m_attack - m_defense一旦攻击小于目标防御伤害会变成负数所以最后必须用qMax(1, ...)兜底保证每次攻击至少扣 1 点血。第二target-m_defense能被直接读取是因为子类是同一个基类的派生类protected 成员在派生类里可见答辩老师问「不同精灵之间为什么能改对方的血量」答案就在这里。皮卡丘的 attack() 方向反过来单次伤害低但攻击间隔短单位时间内出手次数多这就是「敏捷型低攻击间隔」在逻辑上的体现。3.3 升级与属性增长从1级到15级的数值节奏项目正文规定初始等级为 1、满级 15 级每次升级属性少量增加主属性增加较多。升级判定的标准位置在战斗结算之后经验值超过当前等级需求就升级。对 15 级封顶的小体量游戏我建议用下面这种快速判定bool checkLevelUp(Pokemon *p) { int needExp p-level() * p-level() * 50; // 1级升2级需50点2级升3级需200点 if (p-exp() needExp p-level() 15) { p-levelUp(); return true; } return false; }level() * level() * 50的曲线会让后期升级越来越慢正好匹配满级 15 级的边界设计。levelUp() 内部按种族决定属性增量小火龙攻击力 7、其他属性 3妙蛙种子生命值 7、其他 3。这个「主属性多涨一点」的细节就是项目正文那段描述的代码落点。升级后经验扣减有两种处理直接清零或减去当前门槛值继续累积后者更常见可以让玩家在高等级时保留部分溢出经验。答辩时老师如果问「种族差异怎么体现」你能直接指出构造函数初始值和 levelUp() 两个位置比只背概念有说服力得多。4. 人机对战实战登录、战斗、结算的完整代码链路4.1 登录按钮背后的槽函数与连接状态判断从 login.png 能看出客户端有独立的登录界面。登录按钮点击后第一件事不是发指令而是确认 socket 状态。最容易犯的错是上来就 write没等连接建立完成write 的数据直接丢进黑匣子。我一般会先做状态检查再决定是否 connectToHostvoid MainWindow::on_loginButton_clicked() { if (socket-state() ! QAbstractSocket::ConnectedState) { socket-connectToHost(127.0.0.1, 8888); if (!socket-waitForConnected(3000)) { QMessageBox::warning(this, 错误, 连接服务端失败); return; } } // 连接正常后再发登录指令 QJsonObject obj; obj.insert(cmd, login); obj.insert(username, ui-usernameEdit-text().trimmed()); obj.insert(password, ui-passwordEdit-text()); socket-write(QJsonDocument(obj).toJson(QJsonDocument::Compact)); socket-flush(); }waitForConnected(3000)是阻塞等待 3 秒在单次登录动作里可接受如果后面要做实时战斗通信就不建议再等了改用QAbstractSocket::Connected信号的槽函数更稳妥否则界面线程会被卡死。用户名做trimmed()能避免用户手滑多打空格导致 SQL 查不到记录这类细节排查起来很隐蔽建议从第一版就加上。4.2 战斗主循环用攻击间隔决定出招顺序对战的核心在服务端。经典做法是维护双方的「出手倒计时」由 QTimer 周期性触发每帧把双方的nextActionTime减去流逝的时间谁先减到 0 谁先行动。攻击间隔越小冷却越短出手次数越多「皮卡丘低攻击间隔」这个属性在实战里就有了意义。示意逻辑void ServerBattle::runTurn(Player usr, Player ai) { // 双方都到了行动点才进循环进循环前先减冷却 usr.nextActionTime - usr.pokemon-attackInterval(); ai.nextActionTime - ai.pokemon-attackInterval(); while (usr.nextActionTime 0 ai.nextActionTime 0) { // 同时到点时攻击间隔小的一方优先出手 if (usr.nextActionTime ai.nextActionTime) { usr.pokemon-attack(ai.pokemon); usr.nextActionTime usr.pokemon-attackInterval(); // 进入下一次冷却 } else { ai.pokemon-attack(usr.pokemon); ai.nextActionTime ai.pokemon-attackInterval(); } // 每次攻击后立刻判断血量避免出现负数血量还在继续打 if (usr.pokemon-hp() 0 || ai.pokemon-hp() 0) break; } }这段代码最绕的点是nextActionTime的语义它是个倒计时出手后要加上自己的攻击间隔作为「下一次出手的筹备时间」而不是清成 0。两边同时到点时先手给攻击间隔小的一方等于变相奖励敏捷型精灵。被攻击方的血量判断必须放在每次攻击之后立刻做否则会出现「血量已经是负数还在继续攻击」的笑话式 bug。提示QTimer 的触发间隔不要小于 50ms否则频繁调用 runTurn 会让服务端 CPU 占用拉满单机演示时甚至会把另一端的客户端界面卡顿。攻击间隔本身是秒级单位50ms 的精度已经足够。4.3 结算与存档经验值回写与升级判定战斗结束走结算分支玩家赢精灵获得经验玩家输提示失败并保留原存档等待重开。经验值回写和之前的 checkLevelUp 是配套的void ServerBattle::finish(bool playerWin, Player usr, Player ai) { if (playerWin) { // 基础经验加等级加成对手等级越高赢方拿得越多 int gain 50 ai.pokemon-level() * 10; usr.pokemon-addExp(gain); checkLevelUp(usr.pokemon); db-savePokemon(usr.pokemon); // 最新属性回写数据库 } else { db-savePokemon(usr.pokemon); // 失败也存档只是不涨经验 emit battleLost(); } }50 level * 10是我在类似项目里常用的经验公式目的很单纯鼓励玩家去挑战高等级对手而不是反复刷同一级。回写数据库必须放在属性变化之后、界面提示之前因为界面一旦弹窗玩家可能直接关程序没来得及写库就等于白打。服务端的存档最好用事务包着先 begin再 update再 commit任何一步出错都能 rollback避免半截存档污染整张表。5. 编译运行避坑记录QT 5.15.2与MSVC环境下最易翻车的五个点5.1 现象qmake 报 dependent 错误编译链直接断掉现象是编译输出第一行就是:-1: error: dependent ..\..\..\qt\5.15.2\msvc2019_64\include\qtwidgets...后面跟着 qtwidgetsglobal.h 找不到。这不是代码问题根源在于 QtCreator 生成临时 Makefile 时按相对路径去解析 Qt 安装目录工程放在很深的目录里比如D:\学习资料\毕业设计\2025届\Qt\宠物小精灵\源码包\problem3_server相对路径算不到头就断了。解决分两步把工程移动到D:\PokeGame这种二到三层的浅目录然后在 QtCreator 里执行「清除」再重新「构建」。如果还不行手动删除工程目录下的.qmake.stash和Makefile后再重建。改 .pro 里的路径是没用的qmake 缓存不认这招我已经帮别人排过很多次了。5.2 现象中文攻击文案在界面上变成乱码战斗界面上小火龙明明应该打出「使用火花造成 XX 点伤害」结果显示成一张乱码脸。原因通常是源文件编码与编译器解析编码不一致MSVC 默认按本地代码页GBK读取源文件而 QtCreator 默认保存为 UTF-8两边一岔中文字符串字面量就读串了。最省事的做法是在 .pro 工程文件里加一行编译选项QMAKE_CXXFLAGS /utf-8让 MSVC 强制按 UTF-8 读源码。另一个办法是把所有 .h/.cpp 用 QtCreator 的「编辑 → 选择编码」统一转成 UTF-8 with BOM。我一般两种都用一手强制编译器一手统一源文件编码双保险。注意转换要全项目批量做不要手动重新输入翻译否则乱码会以「乱码的样子」重新写进源码等于白改。5.3 现象QTcpSocket 链接阶段报 undefined reference代码里明明写了socket-write(...)编译不报错link 阶段却蹦出undefined reference to QTcpSocket::write。区别就在 .pro 文件那一行如果QT core gui network里的 network 没加Qt 的头文件可能因为 include 顺序碰巧能找到但链接库时必然缺符号。把 network 补进去重新 qmake 再构建就好。同样的坑在 sql 模块上也会上演代码里用了 QSqlDatabase忘了QT sql链接报的是一堆 QSqlDatabase 相关 undefined reference。建议拿到这种 client/server 项目的第一步先对着源码里所有 Q 开头的外部类把需要的 Qt 模块在 .pro 里逐个对齐能省掉大半链接错误。5.4 现象登录总是失败SQLite 文件根本没生成fail.png 截图对应的是登录失败。第一反应是看服务端运行目录里有没有数据库文件。QtCreator 默认开 shadow build构建目录和源码目录不是同一个地方如果代码里写的是相对路径game.db它创建的数据库就在调试构建目录下而不是源码目录。换一台电脑或手动清理构建目录后数据库被删掉登录自然全部失败。正确做法是启动时用QDir::currentPath()拼出可执行文件所在目录再拼文件名或者干脆在服务端初始化时固定一个绝对路径并保证目录存在。另一个隐藏坑两个线程同时对同一个 db 连接做读写时QSQLITE 会报 database is locked要么保证所有数据库操作在同一条线程要么每次访问都新建连接用完再关。5.5 现象exe 永远不更新构建产物去了旧路径改完代码重新构建跑到的还是旧版本 exe界面没有任何变化。先看 QtCreator 左下角是不是显示「已在运行」是的话停掉再构建再看「工具 → 选项 → 构建和运行 → 构建目录」里 shadow build 目录指向哪里确认现在跑的就是当前构建目录下的 exe。最容易踩的是工程自带的.pro.user文件。它记录了上一台机器上的构建目录和 Qt 版本路径连 qmake 的输入文件都缓存了。换电脑后不删它QtCreator 会沿用旧配置构建产物永远跑去旧路径。拿到这份源码包后第一件事就是把problem3_server.pro.user、problem3_client.pro.user全部删掉让 QtCreator 在本地重新生成。这个习惯能避掉九成「改了没反应」的鬼打墙现象。6. 答辩前加一道防线给精灵存档做SHA-256校验这种带 SQLite 存档的游戏项目答辩现场最容易翻车的不是功能而是「老师随手打开数据库把精灵等级从 1 改成 15再跑一遍游戏发现满级了」。本地明文数据库没有任何防护想让项目显得更有完成度低成本做法是给精灵表加一个校验字段存档时对关键属性算 SHA-256读档时比对对不上就拒绝加载。表结构里加一列 checksumALTER TABLE pokemons ADD COLUMN checksum TEXT;存档时把精灵的 id、level、exp、hp、attack、defense、attack_interval 拼成一个字符串再哈希QString raw QString(%1:%2:%3:%4:%5:%6:%7) .arg(pokemon-id()) .arg(pokemon-level()) .arg(pokemon-exp()) .arg(pokemon-hp()) .arg(pokemon-attack()) .arg(pokemon-defense()) .arg(pokemon-attackInterval()); QString checksum QString(QCryptographicHash::hash( raw.toUtf8(), QCryptographicHash::Sha256).toHex()); db-updateChecksum(pokemon-id(), checksum);读档时按同样的拼接顺序重新算一遍跟库里存的 checksum 比对不一致就提示「存档疑似被修改」。这里有个关键坑字段拼接顺序、分隔符、是否带 id必须写死并保持一致。id 是精灵唯一标识把它加进校验串就不会出现「两只精灵交换属性后仍能通过校验」的漏洞。类似的低成本增强还能做两个一是把战斗出招顺序打成绩单日志演示时能讲清楚战局二是把数据库连接串切到 QSQLITE 的 WAL 模式减少并发读写锁冲突。这些都不是大改却能让答辩老师觉得你对 Qt、C 和数据库的掌控不止于「能跑」。说句实在话我第一次拿到这种带双工程的包也踩过.pro.user的坑对着旧构建目录找了半天才明白 exe 为什么不动。从那以后我每次接手 Qt 项目都会强制走一遍「删 .pro.user → 移浅目录 → 清缓存重建 → 验证存档文件路径」四部曲毕业论文级的 Qt 项目基本就没再被环境折腾过。项目易翻车的地方大多不在代码而在你没把运行环境讲明白——希望这份实验笔记帮到你。本文还有配套的精品资源点击获取
返回列表