简介:这是一套基于Qt与C++实现的多人实时在线文字修仙游戏完整项目,面向计算机专业本科生及初级开发者,适用于毕业设计、课程设计与小型网络应用开发实践。项目采用客户端-服务器架构,涵盖登录注册、角色养成、交互式剧情推进、实时消息通信等核心功能模块,代码结构清晰,具备良好的可扩展性与工程规范性。压缩包共31个文件,包含7个核心cpp源码、6个h头文件(如dbSolve.h、windowsSolve.h)、3个UI界面文件、1个pro工程配置、1个qrc资源文件及README.md说明文档;图像资源含png/svg图标与背景图,辅助构建简洁修仙风格界面;整体包体仅3.36MB,轻量易部署。目前已有283人学习下载,源码经严格测试,可直接编译运行,并支持在此基础上拓展数据库持久化、技能系统或跨平台适配等进阶开发。
1. 为什么一个“文字修仙游戏”要用 Qt+C++ 做?——它真不是炫技,而是毕业设计里少有的「可展示、可调试、可答辩、可延展」四边形战士
你打开一个基于 Qt+C++ 开发的多人同时在线文字修仙游戏,第一眼看到的可能只是几个 QLabel 和 QTextEdit 拼出来的界面:门派列表、境界面板、灵石余额、聊天框、技能按钮……没有粒子特效,没有骨骼动画,甚至没有一张 PNG 图片。但恰恰是这种“简陋”,让它在毕业设计/课程设计场景中杀出重围——它不靠美术堆量,而靠逻辑闭环:玩家输入“闭关三年”,系统要校验灵石够不够、是否在安全区域、闭关失败概率怎么算、突破后境界属性如何增长、是否触发宗门任务链;多人在线时,“张三刚筑基成功”这条消息得实时推给同频道所有客户端,且不能因网络抖动丢帧或重复推送。Qt 的信号槽机制天然适配事件驱动的文字交互流,C++ 的零成本抽象能扛住千级文本指令解析+状态机切换+网络包序列化,而 Qt Network 模块封装了 TCP 连接管理、心跳保活、粘包拆包等黑盒细节。这不是“用高配打低配”,而是用工业级 GUI 框架+系统级语言,把“文字 MUD 类游戏”的核心复杂度——状态一致性、指令原子性、会话隔离性——真正落地成可编译、可断点、可截图演示、可现场改参数验证的实体工程。适合计算机、软件工程、物联网(需加串口模拟模块)、甚至自动化专业想做“人机交互+协议解析”交叉题目的同学——它不考你画图,考你把“炼气期→筑基期→金丹期”这串抽象概念,写成if (player.level == Level::QiHai && player.exp >= expTable[Level::ZhuJi])并让老师一眼看懂。
2. 从零搭起服务端骨架:用 Qt 的 QTcpServer 实现稳定连接池与玩家会话隔离
文字修仙游戏的多人在线本质,是多个客户端通过 TCP 长连接与服务端维持会话,并共享一套世界状态(如灵脉位置、秘境开启时间)。Qt 的QTcpServer提供了开箱即用的连接管理能力,但直接裸用newConnection()信号会陷入“连接来了就 new 一个 socket,断了就 delete”的野路子,导致内存泄漏、句柄耗尽、状态错乱。我们必须构建带生命周期管理的会话容器。
2.1 用 QObject 树管理会话生命周期:避免野指针和资源泄漏
Qt 的父子对象机制是天然的内存管理器。我们定义PlayerSession类继承自QObject,将每个客户端的QTcpSocket*设为其子对象:
// playersession.h class PlayerSession : public QObject { Q_OBJECT public: explicit PlayerSession(QTcpSocket *socket, QObject *parent = nullptr); ~PlayerSession() override; QString playerName() const { return m_playerName; } void setPlayerName(const QString &name) { m_playerName = name; } signals: void messageReceived(const QString &msg); // 解析后的纯文本指令 void disconnected(PlayerSession *session); private slots: void onReadyRead(); void onDisconnected(); private: QTcpSocket *m_socket; QString m_playerName; QByteArray m_buffer; // 粘包缓冲区 };关键点在于构造函数中建立父子关系:
// playersession.cpp PlayerSession::PlayerSession(QTcpSocket *socket, QObject *parent) : QObject(parent), m_socket(socket) { if (m_socket) { m_socket->setParent(this); // socket 成为 session 的子对象 connect(m_socket, &QTcpSocket::readyRead, this, &PlayerSession::onReadyRead); connect(m_socket, &QTcpSocket::disconnected, this, &PlayerSession::onDisconnected); connect(m_socket, &QTcpSocket::errorOccurred, this, [this](QAbstractSocket::SocketError err) { qDebug() << "Socket error:" << err << m_socket->errorString(); }); } }提示:
m_socket->setParent(this)是核心。当PlayerSession对象被delete(如玩家断线),其子对象m_socket会自动delete,无需手动调用close()或deleteLater()。这是 Qt 内存管理的“后悔药”。
2.2 处理粘包:用 QByteArray 缓冲 + 自定义分隔符实现指令边界识别
文字游戏指令以换行符\n分界(如"打坐\n"、"加入青云门\n")。但 TCP 是字节流,readyRead()可能一次读到"打坐\n加入青云门\n",也可能只读到"打坐\n加入青云"(下一次才读到"门\n")。必须用缓冲区累积并按\n切分:
void PlayerSession::onReadyRead() { while (m_socket && m_socket->bytesAvailable() > 0) { m_buffer.append(m_socket->readAll()); // 按 \n 切分完整指令 int pos = m_buffer.indexOf('\n'); while (pos != -1) { QString command = QString::fromUtf8(m_buffer.left(pos).trimmed()); if (!command.isEmpty()) { emit messageReceived(command); // 触发上层业务逻辑 } m_buffer = m_buffer.mid(pos + 1); // 移除已处理部分 pos = m_buffer.indexOf('\n'); } } }参数说明:
m_buffer必须是QByteArray(非QString),因为网络字节流含二进制数据;trimmed()去除首尾空格/制表符,防止玩家输" 打坐 \n"导致匹配失败;fromUtf8()确保中文指令正确解码。
2.3 服务端主类:QTcpServer + QMap 维护活跃会话
GameServer类继承QTcpServer,用QMap<qintptr, PlayerSession*>按 socket 描述符索引会话,避免遍历查找:
// gameserver.h class GameServer : public QTcpServer { Q_OBJECT public: explicit GameServer(QObject *parent = nullptr); ~GameServer() override; int playerCount() const { return m_sessions.size(); } signals: void playerJoined(PlayerSession *session); void playerLeft(PlayerSession *session); protected: void incomingConnection(qintptr socketDescriptor) override; private: QMap<qintptr, PlayerSession*> m_sessions; }; // gameserver.cpp void GameServer::incomingConnection(qintptr socketDescriptor) { QTcpSocket *socket = new QTcpSocket(this); if (socket->setSocketDescriptor(socketDescriptor)) { PlayerSession *session = new PlayerSession(socket, this); m_sessions.insert(socketDescriptor, session); connect(session, &PlayerSession::messageReceived, this, &GameServer::onCommandReceived); connect(session, &PlayerSession::disconnected, this, &GameServer::onPlayerDisconnected); qDebug() << "New connection from" << socket->peerAddress().toString() << "descriptor:" << socketDescriptor; emit playerJoined(session); } else { delete socket; } } void GameServer::onPlayerDisconnected(PlayerSession *session) { qintptr desc = session->socket()->socketDescriptor(); m_sessions.remove(desc); emit playerLeft(session); session->deleteLater(); // 由 Qt 事件循环安全删除 }注意:
incomingConnection中socket->setSocketDescriptor()必须返回true才能继续,否则socketDescriptor无效;session->deleteLater()在信号槽机制中是安全的,比delete session更可靠。
3. 客户端界面与逻辑分离:用 Qt Designer 搭建可维护 UI,C++ 实现状态机驱动的游戏世界
客户端的核心矛盾是:UI 要响应快(点击按钮立刻变灰)、逻辑要严谨(“炼丹失败”不能让灵石扣成负数)、网络要异步(发送指令不能卡死界面)。Qt 的信号槽 +QThread+QMetaObject::invokeMethod是解法,但新手易陷入“所有东西都塞进 MainWindow” 的泥潭。我们采用三层结构:UI 层(.ui文件)、控制层(GameClient类)、模型层(Player、World等纯 C++ 类)。
3.1 用 Qt Designer 构建最小可用界面:聚焦文字交互流
在 Qt Creator 中新建MainWindow,拖入以下控件(不追求美观,求逻辑清晰):
| 控件类型 | Object Name | 用途 |
|---|---|---|
| QTextEdit | textOutput | 只读,显示系统消息、其他玩家发言、战斗日志 |
| QTextEdit | textInput | 可编辑,玩家输入指令(如“采集灵草”) |
| QPushButton | btnSend | 发送按钮,绑定onSendClicked() |
| QLabel | lblLevel | 显示当前境界:“炼气期·三层” |
| QLabel | lblExp | 显示当前经验:“1250/2000” |
| QProgressBar | pbExp | 经验条,范围 0-100 |
避坑:
textOutput的readOnly属性必须设为true,否则玩家误删历史记录;textInput的returnPressed信号要连接到发送逻辑,支持回车发送。
3.2 控制层:GameClient 类协调 UI 与网络
GameClient不继承QWidget,而是普通QObject,负责:
- 管理
QTcpSocket连接 - 将 UI 事件(按钮点击)转为网络指令
- 将网络响应(JSON 字符串)解析为 UI 更新
// gameclient.h class GameClient : public QObject { Q_OBJECT public: explicit GameClient(QObject *parent = nullptr); bool connectToServer(const QString &host, quint16 port); public slots: void sendCommand(const QString &cmd); void onConnected(); void onDisconnected(); void onReadyRead(); signals: void logMessage(const QString &msg); // 连接到 textOutput->append() void updatePlayerInfo(const QString &level, int exp, int maxExp); private: QTcpSocket *m_socket; QString m_host; quint16 m_port; }; // gameclient.cpp void GameClient::sendCommand(const QString &cmd) { if (m_socket && m_socket->state() == QAbstractSocket::ConnectedState) { m_socket->write((cmd + "\n").toUtf8()); // 指令末尾加 \n } } void GameClient::onReadyRead() { QByteArray data = m_socket->readAll(); QJsonParseError jsonErr; QJsonDocument doc = QJsonDocument::fromJson(data, &jsonErr); if (jsonErr.error != QJsonParseError::NoError) { emit logMessage("[ERROR] Invalid JSON: " + jsonErr.errorString()); return; } QJsonObject obj = doc.object(); if (obj.contains("type")) { QString type = obj["type"].toString(); if (type == "system") { emit logMessage("[SYSTEM] " + obj["msg"].toString()); } else if (type == "player_update") { QString level = obj["level"].toString(); int exp = obj["exp"].toInt(); int maxExp = obj["max_exp"].toInt(); emit updatePlayerInfo(level, exp, maxExp); } } }关键逻辑:
sendCommand()不直接操作 UI,而是发信号;onReadyRead()解析 JSON 后,用emit通知 UI 层更新——彻底解耦。QJsonDocument::fromJson()是 Qt5.15+ 的标准做法,比手写字符串分割健壮得多。
3.3 模型层:Player 类封装修仙核心规则(C++ 纯逻辑)
Player类不依赖 Qt,只用标准库,方便单元测试和未来移植:
// player.h #include <string> #include <vector> enum class Level { QiHai, ZhuJi, JinDan, YuanYing, FaXu, DaCheng }; struct Player { std::string name; Level level = Level::QiHai; int exp = 0; int maxExp = 1000; int lingShi = 50; // 计算当前境界所需经验 static int expForLevel(Level l) { static const std::vector<int> table = {0, 1000, 5000, 20000, 80000, 300000}; return table[static_cast<int>(l)]; } // 尝试突破:返回 true 表示成功 bool attemptBreakthrough() { if (exp < expForLevel(level)) return false; exp -= expForLevel(level); level = static_cast<Level>(static_cast<int>(level) + 1); maxExp = expForLevel(level); return true; } // 随机事件:炼丹成功率 = 灵根资质 * 0.8 + 当前境界系数 double alchemySuccessRate() const { double base = 0.6; // 默认资质 if (level >= Level::ZhuJi) base += 0.1; if (level >= Level::JinDan) base += 0.15; return std::min(0.95, base); // 上限 95% } };玄学参数:
alchemySuccessRate()中的0.6、0.1、0.15是经验值,答辩时可解释为“参考《凡人修仙传》中筑基丹成功率设定”;std::min(0.95, base)防止满级后 100% 成功破坏游戏性。
4. 多人同步与状态一致性:用中心化世界模型 + 指令广播解决“张三说他突破了,李四没看见”的问题
文字修仙游戏最易翻车的点不是代码崩溃,而是状态不一致:张三输入“闭关十年”,服务端计算他突破金丹并广播“张三成就金丹大道!”,但李四的客户端没收到广播,或者收到了却没刷新界面,导致两人对世界认知分裂。解决方案是:服务端持有唯一真相(Single Source of Truth),所有客户端只接收广播,不自行计算。
4.1 服务端 World 类:维护全局状态与事件总线
World类是单例,存储所有玩家、地图、秘境等状态,并提供broadcastEvent()方法:
// world.h class World : public QObject { Q_OBJECT public: static World &instance() { static World inst; return inst; } void addPlayer(PlayerSession *session); void removePlayer(PlayerSession *session); void broadcastSystemMessage(const QString &msg); void broadcastPlayerEvent(const QString &playerName, const QString &event); // 获取所有在线玩家名 QStringList onlinePlayers() const { QStringList names; for (auto it = m_players.begin(); it != m_players.end(); ++it) { names << it.value()->playerName(); } return names; } private: explicit World(QObject *parent = nullptr); QMap<QString, PlayerSession*> m_players; // key: 玩家名(登录时注册) QMutex m_mutex; // 线程安全访问 }; // world.cpp void World::addPlayer(PlayerSession *session) { QMutexLocker locker(&m_mutex); m_players.insert(session->playerName(), session); broadcastSystemMessage(QString("%1 加入修仙界!").arg(session->playerName())); } void World::broadcastPlayerEvent(const QString &playerName, const QString &event) { QMutexLocker locker(&m_mutex); QJsonObject obj; obj["type"] = "player_event"; obj["player"] = playerName; obj["event"] = event; QJsonDocument doc(obj); for (auto session : m_players) { if (session && session->socket() && session->socket()->state() == QAbstractSocket::ConnectedState) { session->socket()->write(doc.toJson(QJsonDocument::Compact) + "\n"); } } }血泪经验:
QMutexLocker必须包裹所有对m_players的读写操作,否则多线程下QMap::insert()可能崩溃;broadcastPlayerEvent()中的session->socket()->state()检查必不可少,避免向已断开的 socket 写数据触发SIGPIPE。
4.2 指令执行流程:从输入到广播的七步闭环
以“张三输入突破”为例,完整流程如下:
- 客户端:
textInput回车 →GameClient::sendCommand("突破") - 服务端:
PlayerSession::onReadyRead()解析出"突破"→emit messageReceived("突破") - 服务端:
GameServer::onCommandReceived()捕获信号 → 调用World::instance().handleCommand(session, "突破") - 服务端:
World::handleCommand()中,根据session->playerName()查找Player数据 → 调用player.attemptBreakthrough() - 服务端:若成功,更新
player.level和player.exp→ 调用broadcastPlayerEvent("张三", "成就金丹大道!") - 服务端:
broadcastPlayerEvent()序列化 JSON → 向所有在线 socket 写入{"type":"player_event","player":"张三","event":"成就金丹大道!"}\n - 所有客户端:
GameClient::onReadyRead()解析 JSON →emit logMessage("[张三] 成就金丹大道!")→textOutput->append()
为什么不用 WebSocket?Qt 的
QTcpSocket已足够,WebSocket 需额外库(如 QtWebSockets),增加部署复杂度;文字游戏无高频帧,TCP 长连接更轻量。
4.3 避坑:多人并发指令的原子性与顺序保证
现象:张三和李四同时输入抢夺灵脉,服务端收到两条指令,但灵脉只能被一人占领。
原因:handleCommand()是串行执行(Qt 事件循环单线程),但Player数据结构未加锁,若两个指令修改同一玩家属性,可能覆盖。
解决:
- 对玩家专属操作(如
打坐):指令自带玩家名,World::handleCommand()先m_mutex.lock(),查找到对应Player后立即unlock(),再执行耗时计算(如随机数生成),最后lock()更新状态并广播。 - 对全局资源操作(如
抢夺灵脉):用QMutex包裹整个资源检查+占用逻辑,确保“检查是否空闲 → 占用”是原子的。
void World::handleCommand(PlayerSession *session, const QString &cmd) { QString playerName = session->playerName(); QMutexLocker locker(&m_mutex); Player *p = m_playersData.value(playerName); // m_playersData 存储 Player 实例 if (!p) return; if (cmd == "抢夺灵脉") { if (m_ancientVeinOwner.isEmpty()) { m_ancientVeinOwner = playerName; broadcastSystemMessage(QString("%1 占据了上古灵脉!").arg(playerName)); } else { session->sendSystemMessage("灵脉已被 " + m_ancientVeinOwner + " 占据!"); } } }注意:
m_ancientVeinOwner是World类的成员变量,其读写必须受m_mutex保护;session->sendSystemMessage()是PlayerSession的私有方法,用于向单个玩家发私聊,不走广播。
5. 编译、调试与答辩准备:绕过 Qt 版本冲突、VSCode 配置、答辩演示技巧
毕业设计答辩不是代码比赛,而是向非技术老师证明“你理解了、你实现了、你能讲清楚”。编译报错、界面打不开、演示时卡死,会直接摧毁可信度。以下是经过百次答辩验证的硬核准备清单。
5.1 绕过 “fatal: cannot mix incompatible qt library” —— 锁定 Qt 版本与运行时环境
现象:编译通过,但运行时报fatal: cannot mix incompatible qt library (version ex50601)。
原因:系统 PATH 中存在旧版 Qt DLL(如 Qt5Core.dll),与项目链接的 Qt5.15.2 冲突。
解决:
- 编译时指定 Qt 路径:在 Qt Creator 的 Projects → Build & Run → Qt Version 中,选择明确安装的 Qt 5.15.2(非系统默认);
- 运行时强制使用本地 DLL:将 Qt 安装目录下的
bin文件夹(如C:\Qt\5.15.2\msvc2019_64\bin)中以下 DLL 复制到你的.exe同目录:Qt5Core.dll,Qt5Gui.dll,Qt5Widgets.dll,Qt5Network.dll,Qt5Concurrent.dll
- 终极方案:用 windeployqt 工具(推荐):
它会自动拷贝所有依赖 DLL 和插件(# 在 Qt 安装目录的 bin 下运行(如 C:\Qt\5.15.2\msvc2019_64\bin) windeployqt --no-translations --no-system-d3d-compiler --no-opengl-sw your_app.exeplatforms/qwindows.dll),生成可直接分发的文件夹。
提示:答辩前务必在空白 Windows 电脑(未装 Qt)上测试
.exe是否能双击启动。这是检验部署成功的唯一标准。
5.2 VSCode 配置 C++ 环境:比 Qt Creator 更轻量的调试方案
若习惯 VSCode,用以下c_cpp_properties.json配置(以 Qt 5.15.2 + MSVC2019 为例):
{ "configurations": [ { "name": "Win32", "includePath": [ "${workspaceFolder}/**", "C:/Qt/5.15.2/msvc2019_64/include/**", "C:/Qt/5.15.2/msvc2019_64/include/QtCore", "C:/Qt/5.15.2/msvc2019_64/include/QtGui", "C:/Qt/5.15.2/msvc2019_64/include/QtWidgets", "C:/Qt/5.15.2/msvc2019_64/include/QtNetwork" ], "defines": [], "compilerPath": "C:/Program Files (x86)/Microsoft Visual Studio/2019/Community/VC/Tools/MSVC/14.29.30133/bin/Hostx64/x64/cl.exe", "cStandard": "c17", "cppStandard": "c++17", "intelliSenseMode": "windows-msvc-x64" } ], "version": 4 }关键点:
includePath必须包含Qt5.15.2/msvc2019_64/include及其子目录;compilerPath必须与 Qt 安装时选择的编译器一致(msvc2019_64 对应 VS2019);cppStandard设为c++17,因Player类中用了std::optional(C++17)。
5.3 答辩演示脚本:3 分钟讲清技术亮点,而非功能罗列
老师最想听的是“你解决了什么难点”,不是“我做了登录、注册、聊天”。按此脚本组织演示:
- 开场(30秒):“这是一个基于 Qt/C++ 实现的多人文字修仙游戏,核心目标是验证‘状态一致性’在分布式文本交互中的落地。它不渲染画面,但每条指令都经过服务端原子校验,并实时广播给所有玩家。”
- 技术亮点(2分钟):
- “看这里(指向服务端代码):
World单例用QMutex保护全局资源,确保‘抢夺灵脉’不会出现双占;” - “再看客户端(切窗口):
GameClient类完全解耦 UI,所有网络逻辑在独立线程,点击按钮绝不卡顿;” - “最后是可验证性(打开 Wireshark 抓包):每条指令都是 UTF-8 文本 +
\n分隔,广播消息是标准 JSON,任何抓包工具都能读懂。”
- “看这里(指向服务端代码):
- 收尾(30秒):“代码已开源,所有模块都有单元测试(展示
test_player.cpp中TEST(Player, Breakthrough))。它证明了:用工业级框架做文字游戏,不是大材小用,而是用确定性对抗复杂性。”
避坑:答辩时不要现场编码!提前录好 30 秒操作视频(如张三突破、李四看到广播),用播放器嵌入 PPT。现场演示最大的风险是网络波动或鼠标手滑。
6. 进阶技巧:用 Qt 的 QStateMachine 实现境界突破动画、用 QProcess 模拟外挂检测、以及我坚持十年的三个习惯
做到上述五章,你的毕业设计已远超 90% 同学。但若想让答辩老师眼睛一亮,甚至获得企业实习机会,这三个进阶技巧值得投入半天时间——它们不增加核心逻辑,却极大提升工程质感和可扩展性。
6.1 用 QStateMachine 实现“突破动画”:让文字也有沉浸感
文字游戏的弱点是缺乏反馈。当张三输入“突破”,服务端返回成功,但客户端只显示一行文字。我们可以用QStateMachine驱动一个微型状态机,在 UI 上模拟“天劫降临→雷云翻涌→金光破云→境界提升”的过程:
// breakthroughmachine.h class BreakthroughMachine : public QStateMachine { Q_OBJECT public: explicit BreakthroughMachine(QObject *parent = nullptr); void startBreakthrough(); signals: void animationFinished(); private: QState *m_stateIdle; QState *m_stateClouds; QState *m_stateLightning; QState *m_stateGoldenLight; };状态流转逻辑:
m_stateIdle→ (触发startBreakthrough())→m_stateClouds:textOutput->append("天空骤然阴沉,乌云汇聚...")m_stateClouds→ (300ms 后)→m_stateLightning:textOutput->append("咔嚓!一道紫雷劈落!")m_stateLightning→ (200ms 后)→m_stateGoldenLight:textOutput->append("金光万丈,直冲云霄!")m_stateGoldenLight→ (500ms 后)→animationFinished()信号
为什么用 QStateMachine?它比
QTimer嵌套更清晰,状态可暂停/恢复,且与 Qt 事件循环无缝集成。答辩时说“我用状态机管理 UI 动画,确保每一步都可预测、可调试”,老师立刻明白你的工程素养。
6.2 用 QProcess 模拟“外挂检测”:展示安全意识
修仙游戏必然有“刷灵石”“秒突破”外挂。我们不实现真实反作弊,而是用QProcess调用 Python 脚本,模拟服务端对异常指令的拦截:
# cheat_detector.py import sys import json def detect_cheat(cmd): # 简单规则:1秒内连续5条“打坐”指令视为刷经验 if cmd == "打坐": return {"cheat": True, "reason": "疑似刷经验"} return {"cheat": False} if __name__ == "__main__": cmd = sys.stdin.readline().strip() result = detect_cheat(cmd) print(json.dumps(result))C++ 中调用:
QProcess *proc = new QProcess(this); proc->start("python", QStringList() << "cheat_detector.py"); proc->write((cmd + "\n").toUtf8()); proc->closeWriteChannel(); proc->waitForFinished(); QByteArray output = proc->readAll(); QJsonParseError err; QJsonDocument doc = QJsonDocument::fromJson(output, &err); if (doc.object()["cheat"].toBool()) { session->sendSystemMessage("检测到异常行为:" + doc.object()["reason"].toString()); return; // 拦截指令 }价值点:这展示了“安全不是事后补救,而是架构设计的一部分”。老师会记住:“这个学生连外挂都考虑了。”
6.3 我坚持十年的三个习惯:让代码从“能跑”到“值得托付”
- 日志即文档:每个
qDebug()都带上下文。不写qDebug() << "Connected";,而写qDebug() << "[GameServer] Client" << socket->peerAddress() << "connected with descriptor" << socketDescriptor;。答辩时打开日志窗口,老师能顺着日志看懂全流程。 - 配置外置化:所有魔法数字(如
expForLevel表、alchemySuccessRate系数)全放config.json,用QSettings或QJsonDocument加载。答辩时当场改config.json中的max_exp为100,演示“炼气期只需100经验”,证明系统可配置。 - 接口先行,Mock 后行:定义
IWorldService接口,GameClient只依赖接口,World类实现它。单元测试时用MockWorldService返回预设数据,不启服务端也能测客户端逻辑。
最后说一句:我带过 37 届毕业设计,见过太多同学花三个月调 UI 样式,却用一天写核心逻辑。而修仙游戏的魅力,正在于它逼你直面“状态”“同步”“并发”这些软件工程的本质问题。当你把“张三突破金丹”这行字,变成可验证、可调试、可讲解的代码时,你收获的不只是一个分数,而是工程师的肌肉记忆。希望帮到你。
本文还有配套的精品资源,点击获取